优化

Video Game Optimization——网络

储存性能由硬件属性限制,而网络应用则由网络链接限制。计算机的其他部分都远快于网络链接部分。

好的网络代码拥抱网络链接的限制。你需要知道背后到底发生了什么来得到更好的结果。尽管这样,还是有机会在限制之下构建高效的网络世界。

基础问题

网络问题有三个因素:吞吐量、延迟以及可靠性。吞吐量是数据的传输率。延迟是数据从一个兰链接到另一个所需要的时间。Round Trip Time(RTT)是一次连接收到返回需要的时间。

可靠性是之前没有提到的一个因素。如果你绘制一个三角形,那硬件马上就会工作,如果你需要从硬件上读取,你也不需要担心任何复杂的错误情况。

网络传输完全不一样。不仅仅是网络包可能乱序,而且他们也可能损坏、切断甚至是丢失。在最底层你不知道到底发生了啥。

成功的网络系统需要处理这些问题。

传输的类型

每一个操作系统在发布的时候都实现了伯克利Socket。在Mac和Linux上,都是从原始实现派生出来的。在WIndows上有WinSock,作为伯克利API的超集。有一些别的不一样的技术,但是总的来说网络技术伯克利已经定义了标准。

第一个差异是可靠性。你可以自己处理也可以交给系统。这主要在TCP和UDP上区分。在UDP里面你可以发送和接受报文,但是没有固定的链接的概念。数据报文可能丢失、失败或者乱序。你需要自己处理这种情况。实际网络的特性暴露在你面前,你可以更少卷入操作系统中。

TCP,Transmission Control Protocol,你发数据类似于文件系统的时候。数据保证是按顺序交付的。另外一方面,网络栈保证数据按照另一端的发送顺序收到并且保证无丢失。这通过一些开销保证有效实现。

另一个网络之间的差异是客户端服务端构架和对等的。不同的游戏采用不同的方法来做。游戏类型和玩家人数来决定使用哪种方式。还有很多其他资料在性能方面讨论这方面,所以可以看一下章节最后的推荐资料。

现在讨论一下更多可靠性的细节已经在游戏中时如何起作用的。

有两个主要的需要思考传输的条件:可靠性和顺序性。

数据交付可靠或者不可靠。可靠的表示除非链接断开,否则一定会在另一侧接收到。而不可靠则是有一定概率数据丢失,虽然成功率还是比较高的。

是否按顺序则是保证你是否发送信息123然后收到也是按照123.

按照这种区分可以看到4种类型的报文:

  • 可靠有序
  • 可靠无序
  • 不可靠有序
  • 不可靠无序

在接下来的部分我们可以看到不同类型的游戏使用不同的报文类型。

有一点需要理解的是TCP的实现基于UDP的报文,这意味着你可以自己实现TCP类似的协议,而且你可以有更多的控制权。

在这章的末尾,我们会讨论中间件。一些开源一些闭源,可以帮助你在网络游戏中工作。

游戏状态和事件

我们已经讨论了从网络获取数据buffer的不同方式。一旦你有能力控制数据流,你可以继续讨论不同的交付需求以及不同的数据。两个主要的游戏数据,一个是事件以及最近的游戏状态数据。

事件一般就是RPC,一般需求时可靠的。

而近期的状态基本就是状态同步之类(非帧同步)的,有大量的object的属性需要同步,一般来说非可靠也是可以接受的。

比如门有没有开,你可以通过1bit的数据来表示。但是如果是乱序数据的话最好是要拒绝老的数据,或者已经处理过的数据,在TCP上容易处理,UDP的话就需要小心一些。

不光是门。Gameplay相关的大部分数据都需要通过网络状态同步。

你要模拟每个端的行为的时候这个方法可能不是很好。大部分近期状态都是近似结果。举个例子玩家控制需要在每一帧非常吻合,并且符合玩家输入。否则作弊就可能发生,并且有artifacts,例如玩家穿墙。

解决方法是从客户端发消息的逻辑转移到服务器计算逻辑。这样服务器可以回放客户端的移动,服务器是权威的话就可以发送全精度的玩家更新信息。P2P的情况下可以吧客户端替换成玩家端,而把服务器替换成其他玩家端。

带宽和bit打包和网络包

当我们发送消息前,我们需要知道如何编码网络包。之前我们已经知道了有大量的游戏状态需要同步,如果我们分开把这些状态同步出去,那是大量的网络包,导致大量的overhead。

**首先带宽保证还原度。**你更有效利用带宽或者你可以用更多的带宽,那么有更多的状态可以同步出去,那大家的世界状态也就更一致。比如每个人都可以获得全精度的世界,在每个包里面,但这基本上是不可能的,所以系统必须能够在每个包里面发送足够多的状态。

你想要尽可能减少更新,基本的工具就是如何打包数据。你应该能有更多的方式去打包数据。Ken一对浮点等用不同的精度去打包,比如生命值不需要太多的位去打包。整数经常在可知范围内所以也可以更高效打包。

在你使用bitstream打包之后,你可以使用一些编码科技。下图显示了如何将这些方法整合起来。

View of the game protocol-encoding stack. At the lowest level is the network transport that transmits bytes. Above this, the bitstream, which allows bit-level encoding. Above this, various context-specific options like LZW, Huffman encoding, or decimation. And on top, the various kinds of updates that your game sends.

哈夫曼编码对string有用,你或许想要一个共享的字符串池,然后用index索引而不是发送整个字符串。你可以使用LZW压缩对于大体量数据。有更多的高级技术例如:算数编码利用统计学的优势来组织。

你的更新越少,你就可以在包内发送更多的数据,更有效的按优先级排序,用户的体验也就越好。

**其次是有效得让数据优先级排序。**对于每一个网络包,你希望填入最重要的数据,然后发送更少的重要数据如果你有空间的话。对于最近的状态你可以用启发式地进行发送,而非通过时间来判断。通过举例或者游戏重要性来判断。一个飞向玩家的发射物远比一个缓慢移动的载具更重要。

**第三个是只发送你必须发送的。**为了保存带宽并且防止作弊。检查可能需要花一些工作,如果你有任何的区域,潜在可见性集,或者一个最小的视锥,可以工作得很好。基本上差不多准确到最差的情况就可以了。你可以从玩家眼睛中发射线来判断是不是可见。跳过不可见物体可以帮你节省大量的带框。跳过更新不可见物体有巨大的好处。客户端不知道除了和玩家有接触的事物以外的世界的变化,可以节约带宽以及处理起性能。

裁剪和优先级更新不一样。在低优先级更新你或许希望客户端知道物体的更新。比如树可能是很低优先级但是依旧油管。另一方面,墙的另一侧的敌人的移动对于玩家而言是非常重要的,但是你可能会希望省略它的信息来防止作弊。

你的网络包里还有一些需要注意的。保证网络包不要太大。最大的网络包差不多1400bytes是安全的(现在已经过时了吧)再大的网络包就可能出现丢包的情况,损坏或者切断。一个快速说明:以太网的MTU(Maximum transmission unit)是1500bytes。因为大部分英特网基于以太网,所以这是一个比较合适的尺寸。如果你减去包头的体积,压缩和传输的overhead,差不多就在1400bytes左右。选择这个大小给你一些缓冲的区间。

保证你有规律的发送网络包。路由器给使用带宽的人进行分发。如果你的带宽使用变化很大,路由器倾向于分配给其他使用带宽的用户。所以最好发送固定的尺寸以固定的频率。举个例子,你或许每秒发送10个800byte的包。你可以基于玩家连接、服务器负载来决定这个值。

如何优化网络

优化网络代码也遵循相同的周期:profile、改变、再次测量,就和其他的优化工作一样。瓶颈有点不一样。计算一般不是瓶颈,因为网络连接更慢。为了优化玩家体验,意味着你需要降低延迟,并且保持还原度。

首先让操作系统做你想做的事情。大部分应用程序端到端传输大数据,比如FTP,不关心系统内部发生了什么。操作系统本身就支持,只不过有延迟。

如果你的网络IO线程在主线程,你会希望去使用非阻塞IO,这样你就可以去做其他有价值的工作,例如绘制之类的。大部分操作系统通过设置fcntl或者ioctl之类的都可以做到。write和read一次性返回传输队列中的数据,read和rev可以等待数据完成或者直接返回表明没有数据可用。如果连接是阻塞的,就需要等待,默认保证协议可以正常运行。因为连接表现得像文件系统,如果是非阻塞的,那么应用程序可以检测是否有新的数据然后正确的进行处理。

网络性能的关键是——处理它。拥抱网络本身就有的问题。操作系统企图通过buffer和延迟来让网络看上去像线性流。

说到buffer需要注意Nagle算法。你在发送很多小包的时候会带来网络的overhead,大概是40bytes的数据会附带,实际上是无用数据。通过将小包合并来降低overhead。你可以通过TCP_NODELAY(POSIX的一部分)来关闭这个功能,如果你希望更多对TCP包地控制就可以打开。

有时候让TCP去做你想做的事情有点蠢。虽然有时候你确实会用到TCP,但是大部分时候最好自己在UDP之上进行实现,因为像Nagel算法有时候并不是光在操作系统中需要处理的东西,在每个端都会发生。网络调度可以根据当前的网络情况来动态调整。病毒检查会延迟交付,直到知道所有的数据是安全的为止。TCP更容易通过每个连接的基础去理解,但是会干扰对客户端或服务器的理解。

UDP是控制更好的选择。因为它是无连接的,大部分调度硬件和软件不干涉它。而且直接暴露了网络的特性:乱序、延迟、丢包等。

另一个问题是过于频繁的网络通信超过了目前带宽。所以压缩在现在的发展这么好。

压缩让你有了trade-off的机会,你可以把计算力和带宽使用做一个tradeoff。就像jpg在压缩后需要等待解压才能使用。

最好的优化带宽的使用方法是添加一个Counter来计算各个种类的数据使用占比。

另一个性能的overhead来自于连接。一般P2P的压力会更大。大部分的网络服务都可能遭到拒绝服务攻击。你需要同时考虑连接处理和连接活跃的时候的性能消耗。

在连接处理过程中有玩家无效的风险。如果设计得不好,那很容易就发起大量的连接用完所有的服务器资源。正常的玩家都因为网络情况或者防火墙一直重试,无法连接,浪费资源。

好的解决方案是通过验证码来避免简单的客户端网络连接。

连接时的计算复杂度过高也会严重影响连接数量。

小心DNS查找,通过名字来查找(gethostbyname)是非常慢的。

一般的方法是把DNS查找放在另外一个线程,这样就可以非阻塞。

拥抱失败

在本地网上任何游戏都能跑得很快,所以在测试的时候需要添加一些丢包、弱连接。好的方法是拥抱失败:

  • 假装高延迟:测试高延迟的情况,100ms是比较好的假设。一些游戏中的玩家深圳可以到达500ms的延迟。保证你的游戏不会因为短期的网络问题而down掉,因为再好的网度可能有不好的时候。
  • 预期中有丢包的情况:如果你用耳朵UDP。那20%的丢包也是正常的你要保证偶尔的20~30秒没有任何包的情况
  • 每一个人都想攻击你:大部分时候DDOS攻击有计划的。但是也可能是周期性的大量请求。Bug也可能发生DoS,例如客户端疯狂重连。用最坏的情况做假设,解决代码中可能出问题的地方。

欺骗用户

最好的网络体验,就是欺骗用户。主要就是从游戏的状态上进行处理。

人类的眼睛对于跳变的东西特别敏感,但是如果是顺滑改变的则不会有这么高的注意力。

对于游戏状态也是用插值修改,位置不要直接设置还是顺滑移动。你可以通过预判的方式来在客户端先进行预表现,直到你得到服务器的确认。

这种方法称作dead reckoning。因为许多物体倾向于以固定模式移动,你可以按照这种方式尝试给不同的物体采用物理。线性差值一般都大差不差,如果不行的话就尝试自己猜未来的走向。你使用更多的数据就猜得越准。但是要小心的是你等待更新的时间越长,越可能出现错误的猜测。纠正错误可能会导致不自然的移动,误导玩家。

一般的场景

使用网络的场景一般有以下一些:

资源下载

大量游戏需要更新或者补丁。MMO可能要stream世界资源,或者游戏需要和web服务交互。所有这些都需要大量保证交付的有顺序的数据。

大部分的解决方案就是直接用TCP。在大部分的情况下如果你只需要直接使用HTTP。类似于libcurl的库可以很方便的传输。http很容易部署或者直接使用内容分发网络。

内容分发网络像亚马逊的CloudFront是第三方的服务器,可以优化大体积消息的交付。

流式传输音频或视频

音视频流更有挑战性。玩家语音需要实时性,所以需要做一些质量压缩。这是最简单的有损场景,因为数据只要尽快传过去就好了,即使丢失了也不用重传。

这种方法需要有损压缩,不过好的事情是有大量有损压缩的方案。

聊天

聊天是常见的请求,很容易实现。消息需要使用不丢失并且有序的方式传输。带宽不大,并且也很简单。

但是当你在面对成千上万个玩家的时候这个就会成为瓶颈。

你可以使用LZW压缩,Huffman编码或者查找表来降低带宽。也可以直接用zlib的compress在发送之前。

因为聊天不是延迟敏感的,所以也可以不放在最高优先级。

Gameplay

Gameplay更困难。延迟很重要,例如RTS或者FPS你只关心最近的状态,不像聊天需要了解每句话。

一种特殊情况是被每个玩家操作的物体。很明显这不是RTS或者FPS的需求。只发送最近的状态你无法感觉正确。这种情况下需要高进度返回服务器的状态,每个时间戳的移动,这样才能还原每个玩家的操作。

Profiling网络

在了解延迟以及其他的网络特性之后,有没有什么别的优化网络的可能性呢。怎么样才能提高玩家体验?

首先确定网络层代码是否足够高效。你应该小心定位是否有任何瓶颈,并且优化它。使用benchmark以及小心测量,来确定是否有浪费计算或者内存。在大部分的游戏中,网络代码使用了最少量的CPU时间,相比于渲染、物理和gameplay。

第二,暴露你的网络层行为,特别是包数量以及带宽如何使用的。添加计数代码可以看出每种类型的Object的更新频率和情况,以此来定位问题。

有时候一个物体可以导致大量的网络包,有可能是游戏中的物体数量导致的,也可能是因为实现得有问题。为了优化网络性能,你可以减少相关Object的数量,或者使用更少的update或者update的尺寸。降低精度或者更高效的编码。寻找机会不发送任何东西。

第三,小心客户端和服务器不通不的情况。这种情况应该尽可能减少,特别是涉及到玩家角色的。

最后侵略性地优化可察觉的延迟。玩家的输入和反馈应该只需要一帧,如果你想让你的游戏更顺滑的话。

如何在你的游戏中构建优良的网络环境

要正确得最大化潜在的网络性能,需要做大量的基础工作,很难正确设置,而且游戏之间都差不多。所以使用库会比较好。

有大量的网络游戏库。我们很推荐OpenTNL。开源并且对用户十分友好。

当然这不是唯一的选项:RakNet、ENet以及Quazal也是选择。Quake1到3都是开源的。UE2和3,unity3D以及Torque有好的网络。

额外的阅读:

‘‘Networking for Game Developers’’ (http://gafferongames.com/networking-for-game-programmers/) 系列文章。GDC08年的讲座“Robust, Efficient Networking”(http://coderhump.com/austin08/)。每一个游戏精粹书都有网络的文章。Yahn Bernier

的文章也不错。(http://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Protocol_Design_and_Optimization)

如果你决定使用你自己的网络层,最好一开始就设计到你的游戏中,并且在不利环境下大量测试。很多自己实现网络层的游戏到最后都有很多难以解决的困难。

结论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注