Pixockets:我们如何为游戏服务器编写自己的网络库



你好!连接了Pixonic的首席服务器开发人员Stanislav Yablonsky。

当我第一次来到Pixonic时,我们的游戏服务器是基于Photon Realtime SDK的应用程序:一个多功能但非常沉重的框架。看来,该解决方案是简化服务器的工作。直到某点为止。

Photon Realtime通过使用它在播放器和服务器之间交换数据而将我们与自身联系在一起,并且也将其绑定到Windows,因为它只能在它上面运行。从运行时(运行时)的角度来看,这对我们都施加了限制:不可能更改.NET虚拟机和操作系统的许多重要设置。我们习惯于使用Linux服务器,而不是Windows。此外,它们花了我们更少的钱。

而且,使用Photon会在服务器和客户端上以及在进行性能分析时影响性能,从而在垃圾收集器上产生了相当大的负担,并形成了大量的装箱/拆箱操作。

简而言之,Photon Realtime的解决方案对我们而言远非最佳,而且很长一段时间内有必要对其做一些事情-但总是有更紧迫的任务,而手却无法解决服务器的问题。

由于不仅对解决问题而且对更好地理解网络都很有趣,所以我决定主动采取主动,尝试自己编写一个库。但是,您了解在家里-在家里,在工作-在工作,因此,开发图书馆的时间只是在运输中。但是,这并没有阻止该想法实现。

这是怎么回事-请继续阅读。

图书馆思想


由于我们正在开发在线游戏,因此不间断工作对我们非常重要,因此低开销已成为图书馆的主要要求。对于我们来说,这首先是垃圾收集器的低负载。为此,我尝试避免分配,并且在难以实现或根本无法解决的情况下,我们创建了池(用于字节缓冲区,连接状态,标头等)。

为了简化和方便地进行支持和组装,我们开始仅使用C#和系统套接字。另外,重要的是要适应每帧的时间预算,因为来自服务器的数据应该按时到达。因此,我试图减少执行时间,即使是以某些非最佳性为代价的:也就是说,在某些地方,有必要用更简单,更可预测的算法代替快速且部分复杂的算法和数据结构。例如,我们没有使用无锁队列,因为它们在垃圾收集器上创建了负载。

通常,对于多人射击游戏,我们的数据通过UDP发送。最重要的是,还添加了数据包的分段和组装,用于发送比帧大小更大的数据,以及由于转发和建立连接而实现的可靠传递。

库中的UDP帧默认为1200字节。由于大多数现代网络中的MTU都高于此值,因此这种大小的数据包应在具有较低碎片风险的现代网络中传输。同时,通常,此金额足以适应游戏中下一个滴答(状态更新)之后需要发送给玩家的更改。

建筑


在我们的库中,我们使用两层套接字:

  • 第一层负责处理系统调用,并为下一级提供更方便的API。
  • 第二层直接处理会话,数据包的分段/组合,它们的转发等。



依次,用于连接的类也分为两个级别:

  • 下层(SockBase)负责通过UDP发送和接收数据。它是套接字系统对象上的薄包装。
  • 顶层(SmartSock)通过UDP提供其他功能。剪切和粘贴包装,转发未到达的数据,拒绝重复-这都是他的责任范围。

较低的级别分为两个类:BareSock和ThreadSock。

  • BareSock在调用发起的同一线程中工作,并以非阻塞模式发送和接收数据。
  • ThreadSock将数据包放入队列中,从而创建用于发送和接收数据的单独线程。访问它时,只有一种操作:从队列中添加或删除数据。

BareSock通常用于与客户端ThreadSock-与服务器一起使用。

工作特色


我还编写了两种类型的低级套接字:

  • 第一个是同步单线程。在其中,我们获得了内存和处理器的最小开销,但同时在访问套接字时直接发生了系统调用。通常,这可以最大程度地减少开销(无需使用队列和其他缓冲区),但是调用本身可能比从队列中取出一项花费更长的时间。
  • 第二个是异步的,具有用于读取和写入的单独线程。在这种情况下,由于在访问套接字时,读取或写入线程已暂停,因此队列,同步和发送/接收时间(几毫秒内)会产生额外的开销。

我们还尝试使用SocketAsyncEventArgs-也许是我所知道的.NET中最先进的网络API。但是事实证明,它可能不适用于UDP:通过它的TCP堆栈可以正常工作,但是UDP会给出有关错误地截取帧甚至在.NET内部崩溃的错误-好像虚拟机本机部分的内存已损坏。我没有找到这种方案的运作实例。

我们库的另一个重要功能是减少数据丢失。我们的印象是,为了摆脱重复,许多库都丢弃了旧的数据包,这是我们后来从自己的经验中看到的。当然,这样的实现要简单得多,因为在这种情况下,一个带有到达最后一帧的编号的计数器就足够了,但是它不太适合我们。因此,Pixockets使用最后一个帧的编号中的循环缓冲区来过滤出重复项:新到达的数字(而不是旧的数字)将被覆盖,并在最后接收到的帧中搜索重复项。



因此,如果在当前帧之前发送了一个数据包,但在之后发送了一个数据包,则它仍将到达目的地。例如,在位置插补的情况下,这可以极大地帮助您。在这种情况下,我们将有一个更完整的故事。

数据包结构


库中的数据按以下方式传输:



包的开头是标头:

  • 它从数据包的大小开始,而该大小又被限制为64 KB。
  • 大小后面是带有标志的字节。其余标题的解释取决于其可用性。
  • 接下来是会话或连接的标识符。

有了适当的标志,我们得到:

  • 如果依次设置了带有数据包编号的标志,则在会话标识符之后发送数据包编号。
  • 跟在他之后-同样在设置了标志的情况下-确认包的数量及其数量。

标头的末尾是有关片段的信息:

  • 片段序列的标识符,这是区分不同消息的片段所必需的;
  • 片段的序列号;
  • 消息中的碎片总数。

有关片段的信息还需要设置相应的标志。

库已编写。下一步是什么?


为了获得更准确的同步连接信息,我们随后组织了显式连接。这有助于我们清楚地了解当一方认为连接已建立并且没有中断,而另一方认为连接已中断时的情况。

在Pixockets的第一个版本中,情况并非如此:客户端不需要调用Connect(主机,端口)方法-它只是开始将数据发送到已知的地址和端口。然后,服务器调用了Listen(端口)方法,并开始从特定地址接收数据。会话数据在收到/发送数据包后初始化。

现在,要建立连接,就必须进行“握手”(交换特殊格式的数据包),并且客户端必须致电Connect。

此外,我的一位同事为该库进行了分叉,不仅更加关注网络安全性,还增加了一些功能,例如可以直接在套接字内部重新连接的功能:例如,在Wi-Fi和4G之间切换时,现在可以自动恢复连接。但是我们稍后再讨论。

测试中


当然,我们为该库编写了单元测试:它们检查了建立连接,发送和接收数据,数据包的分段和组装,发送和接收数据的各种异常的所有主要方法-例如重复,丢失,发送和接收顺序不匹配。为了进行初始性能检查,我编写了一些用于集成测试的特殊测试应用程序:ping客户端,ping服务器和用于在网络上同步屏幕上色环的位置,颜色和数量的应用程序。

在测试应用程序证明我们的库的功能之后,我们开始将其与其他库进行比较:与我们的旧Photon Realtime和UDP库LiteNetLib 0.7。

我们测试了游戏服务器的简化版本,该服务器仅收集玩家的输入并发回“胶合”结果。我们在6人的房间中容纳了500名玩家,刷新率是每秒30次。



在Pixockets的情况下,垃圾收集器的负载和处理器的消耗以及数据包丢失的百分比降低了-显然是由于这样的事实,与其他版本的UDP不同,我们不忽略后期数据包。

在我们确认了我们的解决方案在综合测试中的优势后,下一步就是在实际项目中运行该库。

当时,在我们选择的项目中,客户端和游戏服务器通过Photon Server同步。我向客户端和服务器添加了Pixockets支持,从而可以控制配对服务器中的协议选择-客户端向其发送进入游戏的请求。

在一段时间内,客户同时使用两种协议,而那时我们收集了有关其运行情况的统计信息。在统计信息收集结束时,事实证明结果与综合测试没有什么不同:垃圾收集器和处理器上的负载减少了,数据包丢失了。同时,Ping降低了一点。因此,该游戏的下一版本已经在Pixockets上完全发布,而无需使用Photon Realtime SDK。



未来的计划


现在,我们要在库中实现以下功能:

  • 简化的连接:现在它无法最佳工作,并且在客户端上调用Connect之后,您需要调用Read直到连接状态更改;
  • 显式关机:此刻,另一侧的关机仅通过定时器进行;
  • 内置ping来维持连接;
  • 自动确定最佳帧大小(现在仅使用一个常数)。

您可以在存储库地址查看并参与Pixockets的进一步开发

All Articles