Raknet研究

[TOC]

游戏性能优化,需要从多个层面进行。美术资源层,降低不必要的网格顶点数、使用可硬件加速的贴图格式和合并渲染队列等。策划需求层,需要掌握技术基本原理,规避技术难度高、风险大的游戏需求、使用“障眼法”掩盖技术缺陷等。程序技术层面,尽可能不改变需求下实现更优的算法、同时使用严格的测试用例(如同步坐标还原测试)校对核心算法的准确性。而在本文旨在考虑的网络层性能忧化,除了考虑数据流量大小和更忧化封/解包算法外,还可以从OSI七层网络协议中寻找优化,如网络加速等。其中较容易操作的是网络传输协议,协议代表有TCP、UDP和SCTP。SCTP有良好的特性,但该协议起步晚目前尚未广泛使用,大多应用程序开在TCP和UDP中作出选择。

UDP更适合实时类游

任何技术都有优缺点,是场景选择技术,而不是技术带着场景走。TCP提供的发送窗口、可靠性、延时ACK、Nagle算法和慢启动与拥塞控制等功能,在特定场景下有时反而成为负担。但是如果数据逻辑有严格的顺序逻辑关系,就应该使用TCP而不是Udp。UDP的网络传输更快,这非常适合实时类型游戏。以下是来自互联网,有关TCP与RUDP在弱网络掉包的情况下传输性能测试,横轴表示RTT(往返时间)、纵轴表完成输出占比量。        右上角的图例尤其突显RUDP在高掉包率网络环境下的优势,RUDP在[50-150]毫秒内完成约70%的数据传输量,而TCP完成的传输量较为平均地分布在各个延时区。再依据另外3个数据图,更能反映出RUDP相比TCP在更短的延时内完成了大部分数据量的传输 总结出TCP以下特性,减弱其传输性能

  • 发送窗口
    应用层调用Send时,数据并没有立即送出,只是先将数据拷贝到发送缓存区,此后由发送窗口机制分组送出。
  • 延时ACK
    TCP同时发送多个分组并且会累积ack,以太网最大的数据帧空间是1518字节,而一个ack占用空间约60字节,只占总空间的4%。为了提高宽带利用率,以及降低大量ACK包造成网络拥堵,TCP延迟40ms发送,如果这段时间内有数据发送到对端,则捎带发送ack
  • Nagle算法
    TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块,算法要求一个TCP连接上最多只能有一个未被确认的未完成的分段,在该分段ack到达之前不能发送其他的分段。        假如client发送一个http请求个server。这个请求时1600byte,MSS是1460byte。那么就会分成两个TCP包,第一个1460byte,剩下的140byte放在第二个包。第一个包发送到server时,由于server开启了delay ack,所以没有立即ack,又因为server没有收到完整的http请求包,所以也没有立即进行http response,这就导致ack会一直等到40毫秒的delay时间。其实如果client立即发送第二个包,server收到后立即做出http response也不会有问题。问题时client启动了Nagle算法,第一个包没有收到ack,第二个包就不会立即发送出去。两边相互等m,这就是性能问题的核心原因。解决办法是向TCP套接字设置选项TCP_NODELAY关闭Nagle
  • 慢启动与拥塞控制
    慢启动与拥塞控制都是为防止过多的数据注入到网络,造成网络拥堵。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次灵气都能正确的接受确认。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小cwnd。然而不断增大的发送窗口会增大网络负载,所以设置一个慢开始门限值ssthresh,当cwnd > ssthresh时,使用拥塞控制算法,停用慢启动算法。

市场上的UDP网络库

  • Kcp 一个可靠传输算法,对现有项目入侵性低。作者是网易大牛,该库在网易游戏内部得到过验证。
  • Enet 一个提供可靠传输、连接管理和多通道传输等特性。著名游戏《英雄联盟》使用了该库
  • Photon 一个完整的游戏服务器解决方案,提供多个平台sdk接入,对unity3d友好
  • Raknet 老牌商用网络库,高性能udp传输,包含大量游戏服务端工具集。

Raknet

官网主页可以了解到,Raknet初始是为多人对战游戏而设计的网络库,之后得到不断完善并转向商用。2014年宣布在BSD协议下开源(可以自由的使用,修改源代码)。 Raknet除了支持可靠和多通道传输,还包含游戏在应用的通用功能,如http收发、语音收发、NAT穿透、email发送和信息加密等。Unity3d 4.x的版本接入了Raknet,但Unity3d接入得不并完善1Unity3d 4.x网络底层使用了raknet 3.7.31

优点:(源自互联网2,匹配之后的压测会有更好的结论)

  • 高性能 在同一台计算机上,Radnet可以实现在两个程序之间每秒传输25,000条信息;
  • 支持Window、Android和iOS平台
  • 多个传输通道,提高带宽利用率
  • 信息加密传输

缺点:(源自互联网2,匹配之后的压测会有更好的结论)

  • Raknet理论上可以支持多个客户端和服务器之间每秒4W个消息的ping-pong测试。但是不稳定,如果某些原因导致消息堆积,则会严重影响发送和接受的响应时间,会达到秒级。
  • Raknet如果消息超过承受的极限,底层的逻辑上导致不断会恶化卡的现象,表现出现吃内存,底层线程陷入循环,执行效率下降。
  • 目前的Raknet版本不支持发送线程,虽然有发送线程的宏,但是打开后编译不过,还未具体继续研究下去。
  • 压测过程中不同的客户端连接数对应能支撑的消息数量也会有明显的差异,主要差距来自轮询过程中会raknet底层会进行组包的大大的减少了实际发包的数量

Send操作

在所有网络库里,使用最频率非send接口莫属。Raknet里,它定义在文件RakPeerInterface.h,由子类RakPeer实现,以下是Send接口签名,其中涉及几个重要概念:发包优先级、传输模式和传输通道。

virtual uint32_t Send( const char *data, 
                    const int length, 
                    PacketPriority priority, 
                    PacketReliability reliability, 
                    char orderingChannl, 
                    const AddressOrGUID systemIdentifier, 
                    bool broadcast, uint32_t forceReceiptNumber=0)=0;
  • data
    数据缓存指针

  • length
    数据缓存长度

  • PacketPriority(传输优先级)

PacketPriority 说明
IMMEDIATE_PRIORITY 最高优先级,内部不缓存、不合并数据包立即发送
HIGH_PRIORITY 每发两次IMMEDIATE_PRIORITY, 发送1次HIGH_PRIORITY
MEDIUM_PRIORITY 每发两次HIGH_PRIORITY, 发送1次MEDIUM_PRIORITY
LOW_PRIORITY 每发两次MEDIUM_PRIORITY, 发送1次LOW_PRIORIT

注意:HIGH_PRIORITY及以下类型,Raknet内部会缓存数据包10ms后再发送 由此可猜,为了提高带宽使用率,Raknet内部缓存数据包,除非使用IMMEDIATE_PRIORITY

  • PacketReliability(传输模式)
    假设对端的发送序列[1,2,3,4,5],以下列出各个模式下可能收收到的序列:
PacketReliability 说明 可收到的序列
UNRELIABLE 不可靠乱序 [5, 1, 6]
UNRELIABLE_SEQUENCED 不可靠但部分按序 [5] (6掉包, 1,2,3,4迟到达被丢弃)
RELIABLE 可靠乱序 [5,1,4,6,2,3]
RELIABLE_ORDERED 可靠完全按序 [1,2,3,4,5,6]
RELIABLE_SEQUENCED 可靠部分按序 [5,6] (1,2,3,4迟于5到达被丢弃)
  • OrderingChannel(传输通道)
    在游戏战斗玩法中,会允许英雄移动的同时释放技能。假设我们使用可靠UDP发送角色位置和技能数据包。不过单纯的可靠UDP,会要求只有在前面的数据接收成功后,才允许后面的数据继续发送。所以角色位置和技能数据包其中之一出现较大延迟,就会影响另一方的发送。经过分析,英雄位置与技能数据包在大多情况下,不存在很强的依赖关系,其实我们可以创建多个可靠UDP,即英雄位置与技能数据包分别在2个可靠UDP下传输。然而创建多个socket可能增加系统资源消耗,同时需要写额外的代码隐藏这一细节。 Raknet在单个连接上增加了传输通道的概念(复用socket),来解决上面问题并且提高了数据传输效率。不过传输通道仅在完全按序系列模式(*_ORDERED)部分按序系列模式(*_SEQUENCED)下有作用,分别支持最大32个通道,即最大64个通道,并且它们之间互不影响。传输通道只面向对发送端,接收端看不到通道概念。(图片来源3)

  • AddressOrGUID
    发送目标设备地址

  • broadcast
    是否广播发送,广播对象不包含systemIdentifier参数指定的目标设备

  • ForceReceiptNumber(数据包序号)
    ForceReceiptNumber = 0,数据包使用Raknet内部自增长序列号。 ForceReceiptNumber > 0,数据包使用这个传入的参数值作为序列号。

  • 返回值
    返回数据包使用的序列号

合理使用传输模式与传输通道

根据数据包的需求特性,利用Raknet的传输模式和传输通道,细化发送规则,压榨性能。还是以RPG游戏玩法为例,以下列出数据包特性,并尝试推理它们的传输模式和传输通道

数据包 需求 PacketReliability OrderingChannel
英雄位置 只关心最新的角色位置 RELIABLE_SEQUENCED 1
英雄技能 技能连招效果需要严格顺序关系 RELIABLE_ORDERED 1
英雄生命值 缺失的数据包影响对战结果,生命值ui没有明显的过渡只显示最新生命数值 RELIABLE_SEQUENCED 2
文字聊天 严格的对话顺序、缺失的内容可能产生模糊的话题 RELIABLE_ORDERED 2
快捷聊天 错误队友信息顺序并不影响战斗数据, RELIABLE

【英雄位置】与【英雄技能】均在通道1并不冲突,因为它们分别在*_ORDERED和*_SEQUENCED模式上进行的,通道传输仅只在这两个传输模式系列运作,所以RELIABLE模式下的【快捷聊天】没有无传输通道。

收发数据包模型

不好的设计可能会使业务层与网络层耦合过多,导致此后的游戏扩展受限。现在来讨论客户端业务层与网络层的交互关系,重点考虑何时发包和收到包后如何通知逻辑层。下文大部分内容整理自Raknet官方文档Send packets,原文提及了3种模型,每种都有各自的优缺点,实践过程中应灵活运用。

  • 行为函数内发送
    ShootBullet控制层的函数负责角色的射击行为,需要的参数包括:射击ID、射击位置、射击方向。每次外部调用,ShootBullet内部都会发送一次数据包

    优点: 统一封装了发包逻辑,不必担心增加的外部调用,会忘记发送数据包 缺点: 然而射击行为与数据包在设计上产生了强耦合关系。本端为了处理对端相同行为的网络包,需要增加一个中间层函数DoShootBullet,参数决定这是一主动还是被动的数据同步,来决定是否发送网络包。另外,如果要增加发送参数(如子弹剩余量),这些参数会污染ShootBullet接口原来的设计原意,当然这些数据可以从全局数据层获取获取。无论如何这类强耦合关系设计时需要十分注意。

  • 行为函数后发送
    另一个收发模型就是把发送网络包,放在ShootBullet之后执行。 优点: 解决了主、被动同步和参数扩展问题。 缺点: 需要跟踪可能触发ShootBullet的逻辑,新增的外部调用容易忘记主动发送数据包。
  • 循环检查策略发送
    另外一个例子:当角色的生命值低于0时,发送数据包。我们做法是每次游戏帧都检查玩家的生命值,仅当第一次发现生命值低于0时,通知服务端。 优点: 同样解决了主、被数据同步。封装了面向数据层的网络发送层,断开了与逻辑层的关系。我们还可以在这个发包层自定义发包策略,如角色位置同步频率等 缺点: 逻辑开发程序员需要了解发包层的工作原理,以便逻辑层的操作能产生正确的数据包去通知服务端

性能测试

服务端环境 * 阿里云 华南 * CentOS Linux release 7.4.1708 (Core) 64 bit * CPU Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz 核数1个 * 物理内存1G * 带宽1Mbps

服务端逻辑 单进程1us帧率运行,转回客户端数据包

客户端逻辑 每个进程按30ms的频率随机操作:

  1. 与目标服务器重建connection
  2. 向目标服务器发送保活ping包
  3. 随机传输模式和传输通道等组合参数,向目标服务器发送乱序数据包


以下记录点,均是服务端连接数逐渐扩大至指定值后,继续运行2-20分钟后获取的数据。

记录点 并发数 运行时间(min) RTT(ms) CPU使用率 Load5 物理内存(Mb) 备注
1 1000 20 9.47 18% 0.60 128 性能测试
2 2000 20 11.50 48% 0.95 204 性能测试
3 2300 5 11.15 56% 1.31 250 负载测试
4 2800 5 12.77 66% 1.54 290 负载测试
5 3300 2 >=115262 >=90% >=3.05 >=340 压力测试

关于Load5:系统负载指运行队列的平均长度,也就是等待CPU的平均进程数。Load越高说明系统响应越慢,如果load是0,代表进程不需要等待,立刻就能获得cpu运行。Load5是5分钟的记录一次负载均值



原文:
https://lizijie.github.io/2018/08/13/Raknet%E7%A0%94%E7%A9%B6.html
作者github:
https://github.com/lizijie

  1. https://japan.unity3d.com/unite/unite2015/files/DAY2_1700_room1_Sean.pdf 

  2. https://blog.csdn.net/ycf8788/article/details/51770826  2

  3. https://blogs.unity3d.com/2014/06/11/all-about-the-unity-networking-transport- 

PREVIOUSUnity3d IL2CPP 内存对齐错误
NEXT源码安装v8报错