目录
1.战团匹配算法
原文:https://www.gameres.com/827195.html
游戏中存在跨服战等需求,将一些队伍组成固定规模战团,然后战团之间战斗,战团匹配过程应当尽量高效、快速,同时实现战力均衡。为了实现尽可能公平均衡的战团匹配,直观的做法当然是搜索所有队伍组成固定规模的战团,然而其时间复杂度为指数级,以极高的计算代价换取最优匹配显然是不现实的,因此,战团匹配问题也成为制约服务器性能瓶颈问题。
为此,本文利用压桶法实现了快速组成固定规模为K的战团,在尽量保证匹配战团的战力均衡前提下,将战团匹配的时间复杂度降为O(n)。具体上,当有玩家或队伍(实际中,队伍人数为1到5)申请组成战团时,将其加入队列尾部。然后在游戏的心跳中遍历队列,如果当前遍历的玩家或队伍的人数加上桶里的人数小于等于K,则将当前玩家或队伍加入桶,并从队列中删除;否则新建一个桶,将玩家或队伍加入新桶中。在战团匹配时,找出所有人数为K的桶,将桶里所有玩家的战力值的和设为对应桶的权重值,并以此对所有的满的桶进行排序,每次选择战力值最接近的两个战团进行战力均衡的调整,然后传送到战斗服务器进行战斗。
然而,由于压桶法是为了节省时间而选择的贪心算法,可能会遗漏能组成战团的玩家或队伍,因此在压桶法后,本文采用查表法对剩余的玩家和队伍再次尝试组成固定规模的战团。首先在服务器启动时做些预处理,群举所有能组成两个固定规模战团的组合(队伍人数为一的队伍数目,队伍人数为二的队伍数目,...,队伍人数为五的队伍数目),并存在set里,该步骤是全局唯一的,并且只需要做一次。然后统计剩下的玩家和队伍,计算人数为一、二、...、五的队伍数目。如果剩下的玩家或队伍队伍人数为一到五的队伍数目均大于set中某个元素,则set中该元素表示能组成两个固定规模战团的组合,并从剩下的玩家或队伍中删除。此步骤重复进行,直到剩下的玩家或队伍无法组成两个固定规模的战团。
通过上述方法,本文实现尽可能合理的战团组成,在此之后,为了尽可能实现两个战团战力均衡,本文进一步设计战团玩家调整方法。当两个固定规模的战团组成后,通过查询预先生成的表,调整两个战团里的玩家,使得两个战团的战力尽可能相等,增强战斗刺激性。具体上,首先群举所有的能组成两个固定规模战团的可能,并存在map里,map的key为字符串,key唯一标识战团的构成(队伍人数为一的队伍数目,队伍人数为二的队伍数目,...,队伍人数为五的队伍数目),map的value为一个vector容器,vector中存储当前key拆分成两个固定规模战团的所有可能。当调整两个战团的战力时,通过map查询当前两个战团能重组成的所有两个战团的可能,然后遍历所有的可能,找出两个战团战力最接近的组合。
自己的思考
原文中介绍的算法很精彩,考虑的情况也比较复杂。我自己想了下fifaol3这款游戏的匹配对战方式,应该是比较简单的,尝试分析一下。
玩家点击“排位赛”或者“友谊赛”按钮后会进入对战房间,在房间中可以邀请好友加入,因此在发起对战匹配请求前,我方阵容的情况只有三种:1人、2人、3人。实际上我们可以在服务器维护3个桶分别记录玩家数是1、2、3人的队伍,其中key为队伍中玩家的昵称组合,value为队长的战力(即前场中场后场的能力值,只需要队长的数据是因为对战时使用的是队长的球员阵容)。当玩家发起对战请求时,就在对应的桶里找到战力均衡的两支队伍发起对战。(当然还需要对玩家的排位等级做一些加权影响,因为**级的玩家和业余级的玩家对战的话会索然无味,影响游戏体验)。
上面介绍的是最简单的情况,即玩家只进行与自己队伍人数相同的匹配。比较复杂的是房间中只有1人,但是他选择的是2v2或者3v3对战的情况等,这就涉及到不同人数的队伍之间进行临时组队再进行匹配的算法。思考如下:
同样维护3个桶队列,分别表示选择1v1、2v2、3v3的房间,那么这三种房间的人数上限分别为2、4、6。例如当1个单独玩家选择参加3v3时,遍历3v3的桶队列,若发现有一个房间的人数已经为5(为什么会出现5,看下一句话),就直接凑成6人加入,若没有就将此玩家加入桶队列。在每次遍历时,可以将一些零散的房间内的玩家拼在一个房间内,供下一次别人匹配来使用,如将1、1、2人的房间拼凑成一个4人的房间。当然这是一种贪心策略,可能会造成有的玩家等了好久都匹配不上的情况,但是游戏中确实存在等了2分钟都没匹配上的情况-_-。(当然房间中还允许有4个观战者,这种情况先不考虑啦)
2.服务器内存优化
本部分仍然转自https://www.gameres.com/827195.html
2.1 内存统计
在对内存优化之前,需要先确定程序每个模块的内存分配。程序的性能有perf、gprof等分析工具,但内存没有较好的分析工具,因此需要自行统计。在linux下/proc/self(pid)/statm有当前进程的内存占用情况,共有七项:指标vsize虚拟内存页数、resident物理内存页数、share 共享内存页数、text 代码段内存页数,lib 引用库内存页数、data_stack 数据/堆栈段内存页数、dt 脏页数,七项指标的数字是内存的页数,因此需要乘以getpagesize()转换为byte。在每个模块结束后统计vsize的增加,即可知该模块占用的内存大小。
在面向对象开发中,内存的消耗由对象的消耗组成,因此需要统计每个类的成员变量的占用内存大小。使用CLion或者visual studio都可以导出类中定义的所有成员变量,然后在gdb使用命令:
p ((unsigned long)(&((ClassName*)0)->MemberName)),即可打印出类ClassName的成员变量MemberName相对类基地址的偏移,根据偏移从小到大排序后,变量的顺序即为定义的顺序,根据偏移相减即可得出每个成员变量大小,然后优化占用内存大的成员变量。
2.2 内存泄露
内存泄露是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,导致内存一直增长。虽然有valgrind等工具可以检查内存泄露,但valgrind虚拟出一个CPU环境,在该环境上运行,会导致内存增大、效率降低,对于大规模程序,基本无法在valgrind上运行。因此需要自行检查内存泄露,glibc提供的内存管理器的钩子函数可以监控内存的分配、释放。如图2.2.2、2.2.3所示,分别为钩子函数的分配内存和释放内存。因为服务器启动时需要预先分配很多内存,比如内存池,这些内存是在服务器停止时才释放,因此为了避免这些内存的干扰,在服务器启动之后才能开始内存泄露的统计。
首先申请固定大小的vec_stack,记录所有分配的内存,如果有释放,则从vec_stack中删除,最后vec_stack中的元素即为泄露的内存,vec_stack必须为固定大小,否则vector扩容中会有内存分配,也不可以用map,map的红黑树旋转也会有内存分配,会造成干扰;然后通过图2.2.1所示的my_back_hook记录原有的malloc、free;并通过图2.2.2所示的my_init_hook将malloc、free换成自定义的钩子函数。
每次分配内存时,都会进入自定义钩子函数my_malloc_hook中,如图2.2.2所示。在my_malloc_hook中首先通过my_recover_hook将malloc恢复成默认的,否则会造成死递归,然后通过默认的malloc分配大小为size的空间,为了分线程统计内存泄露,还需要对线程号做判断,在stTrace.m_pAttr记录内存分配的地址,m_nSize记录大小,m_szCallTrace记录调用栈,如果vec_stack已满,需要根据m_nSize从大到小排序,如果当前分配内存大于vec_stack记录的最小的分配内存,则替换;如果未满,则直接加入vec_stack,在my_malloc_hook结束时,将malloc替换成自定义的malloc。
每次释放内存时,都会进入自定义钩子函数my_malloc_free中,如图2.2.3所示。在my_malloc_free中首先通过my_recover_hook将free恢复成默认的,否则会造成死递归,对线程号判断,然后在vec_stack中删除对应的分配,并将free替换成自定义free。
图2.2.1
图2.2.2
图2.2.3
3.高时效的UDP
摘自《2018腾讯移动游戏技术评审与实践案例》
游戏对网络有实时性要求高,带宽要求小的特点。TCP 虽然提供了可靠传输,但是内置的复杂拥塞控制算法并不是专为实时性优化的,也没有提供较好的方法让业务定制化。CF 手游实现了自定义的RUDP 协议方案,使用UDP 保证协议实时性,同时通过自定义重传策略兼顾可靠性,取得了很好效果,主要技术要点包括:
- 数据传输分类
CF 手游中并非所有数据都要求可靠,按游戏逻辑需要,只有不到50%的协议有可靠性需求,RUDP 中只对这部分协议提供可靠性保证。而非可靠包则保证尽快到达,以满足游戏的实时性需求。对玩家移动状态等信息,由业务层定时冗余重传。
客户端与服务的的交互时序如下:
图CF 手游网络数据交互时序
- 快速发送
发送方不维护发送窗口,不等待前面包是否ack,有数据需要发送时立即发送。
- 快速重传
CF 手游使用比TCP 更高精度,响应速度更快的重传策略以保证实时性。
- 带宽优化
主要思路是有损服务和降低不必要开销。CFM 持续进行了多轮流量优化,包括:
1.MTU 设计为500+字节,应用逻辑保证数据包大小不超过MTU,避免拆包。
2.减小包头,8 字节。
3.小包合并。同一帧发往同一个目标的多个小数据包合并为大包,减少包数量。
4.降低服务端帧率和位置精度,但不影响玩家体验。
4.DOS攻击和DDOS攻击
DOS:是Denial of Service的简称,即拒绝服务,不是DOS操作系统,造成DoS的攻击行为被称为DoS攻击,其目的是使计算机或网络无法提供正常的服务。最常见的DoS攻击有计算机网络带宽攻击和连通性攻击。
DDOS:分布式拒绝服务(DDoS:Distributed Denial of Service)攻击指借助于客户/服务器技术,将多个计算机联合起来作为攻击平台,对一个或多个目标发动DDoS攻击,从而成倍地提高拒绝服务攻击的威力。
DDoS攻击它的原理说白了就是群殴,用好多的机器对目标机器一起发动DoS攻击,但这不是很多黑客一起参与的,这种攻击只是由一名黑客来操作的。这名黑客不是拥有很多机器,他是通过他的机器在网络上占领很多的"肉鸡",并且控制这些"肉鸡"来发动DDoS攻击,要不然怎么叫做分布式呢。还是刚才的那个例子,你的机器每秒能发送10个攻击数据包,而被攻击的机器每秒能够接受100的数据包,这样你的攻击肯定不会起作用,而你再用10台或更多的机器来对被攻击目标的机器进行攻击的话,那结果就可想而知了。
事实上DOS的攻击方式有很多种,比如下面的常见的:
- 1. SYN-Flood
DDoS究竟如何攻击?目前最流行也是最好用的攻击方法就是使用SYN-Flood进行攻击,SYN-Flood也就是SYN洪水攻击。SYN-Flood不会完成TCP三次握手的第三步,也就是不发送确认连接的信息给服务器。这样,服务器无法完成第三次握手,但服务器不会立即放弃,服务器会不停的重试并等待一定的时间后放弃这个未完成的连接,这段时间叫做SYN timeout,这段时间大约30秒-2分钟左右。若是一个用户在连接时出现问题导致服务器的一个线程等待1分钟并不是什么大不了的问题,但是若有人用特殊的软件大量模拟这种情况,那后果就可想而知了。一个服务器若是处理这些大量的半连接信息而消耗大量的系统资源和网络带宽,这样服务器就不会再有空余去处理普通用户的正常请求(因为客户的正常请求比率很小)。这样这个服务器就无法工作了,这种攻击就叫做:SYN-Flood攻击。
- 2.IP欺骗DOS攻击
这种攻击利用RST位来实现。假设现在有一个合法用户(1.1.1.1)已经同服务器建立了正常的连接,攻击者构造攻击的TCP数据,伪装自己的IP为1.1.1.1,并向服务器发送一个带有RST位的TCP数据段。服务器接收到这样的数据后,认为从1.1.1.1发送的连接有错误,就会清空缓冲区中建立好的连接。这时,如果合法用户1.1.1.1再发送合法数据,服务器就已经没有这样的连接了,该用户就必须从新开始建立连接。
攻击时,伪造大量的IP地址,向目标发送RST数据,使服务器不对合法用户服务。
- 3.带宽DOS攻击
如果你的连接带宽足够大而服务器又不是很大,你可以发送请求,来消耗服务器的缓冲区消耗服务器的带宽。这种攻击就是人多力量大了,配合上SYN一起实施DOS,威力巨大。不过是初级DOS攻击。
- 4.自身消耗的DOS攻击
这是一种老式的攻击手法。说老式,是因为老式的系统有这样的自身BUG。比如Win95 (winsock v1), Cisco IOS v.10.x, 和其他过时的系统。
这种DOS攻击就是把请求客户端IP和端口弄成主机的IP端口相同,发送给主机。使得主机给自己发送TCP请求和连接。这种主机的漏洞会很快把资源消耗光。直接导致当机。这中伪装对一些身份认证系统还是威胁巨大的。
上面这些实施DOS攻击的手段最主要的就是构造需要的TCP数据,充分利用TCP协议。这些攻击方法都是建立在TCP基础上的。还有其他的DOS攻击手段。
- 5.塞满服务器的硬盘
通常,如果服务器可以没有限制地执行写操作,那么都能成为塞满硬盘造成DOS攻击的途径,比如:
发送垃圾邮件。一般公司的服务器可能把邮件服务器和WEB服务器都放在一起。破坏者可以发送大量的垃圾邮件,这些邮件可能都塞在一个邮件队列中或者就是坏邮件队列中,直到邮箱被撑破或者把硬盘塞满。
让日志记录满。入侵者可以构造大量的错误信息发送出来,服务器记录这些错误,可能就造成日志文件非常庞大,甚至会塞满硬盘。同时会让管理员痛苦地面对大量的日志,甚至就不能发现入侵者真正的入侵途径。
向匿名FTP塞垃圾文件。这样也可以塞满硬盘空间。