Hi, I’m Glenn Fiedler and welcome to Networking for Game Programmers.
在这篇文章中,我们从最基础的网络编程开始:通过网络收发数据。这可能是网络程序员做的最简单,最基础的工作了,但什么是最好的方案,仍然是复杂和不明显的。
你可能听说过socket,并且可能知道,它主要有两种类型:TCP和UDP。当你写网络游戏时,我们首先需要选择使用哪个类型的socket。我们该使用TCP,UDP还是混合使用呢?小心选择,因为如果选错它会给你的多人游戏坏的影响。
你的选择,完全取决于你想做什么类型的游戏。因此,从这一点开始,在本系列文章的其余部分中,我假设您希望将一个动作游戏联网。就类似 Halo,Battlefield 1942,Quake,Unreal,CounterStrike和Team Fortress等游戏。
由于我们想做一个动作游戏的联网,我们将仔细查看每个协议的属性,深入了解互联网的实际运作方式。一旦我们掌握了所有这些信息,正确的选择就很清楚了。
TCP代表“transmission control protocol”,IP代表“Internet Protocol”。它们一起构成了你在网上做的几乎所有事情的主干,从网络浏览到IRC到电子邮件,所有这些都建立在TCP/IP之上。
如果您曾经使用过TCP套接字,那么您就知道它是一个可靠的基于连接的协议。这意味着您在两台机器之间创建了一个连接,然后您交换数据,就像您在一侧写入一个文件,在另一侧读取一个文件一样。
TCP连接可靠且有序。您发送的所有数据都保证到达另一端并按您所写的顺序发送。它也是一个流协议,因此TCP会自动将您的数据拆分成数据包,并通过网络为您发送它们。
TCP的简单性与TCP在IP或“internet protocol”级别下的实际情况形成了鲜明的对比。
这里没有连接的概念,数据包只是从一台计算机传递到下一台计算机。你可以想象这个过程有点像一张手写的便条,从一个人传给另一个人,穿过拥挤的房间,最后,到达他要找的人,但必须经过许多手。
也不能保证这张纸条能真正到达预定的人手中。寄件人只是把纸条传给对方,并希望得到最好的结果,不知道是否收到了纸条,除非对方决定回信!
当然,IP实际上比这要复杂一些,因为没有一台计算机知道计算机传递的确切顺序,从而使它快速到达目的地。有时,IP会通过同一个数据包的多个副本传递,这些数据包通过不同的路径到达目的地,导致数据包的到达顺序不对,并且是重复的。
这是因为互联网的设计是自我组织和自我修复的,能够绕过连接问题,而不是依靠计算机之间的直接连接。事实上,如果你想一想底层发生了什么,那就相当酷了。你可以在经典的tcp/ip书籍中阅读所有相关内容。
如果我们想直接发送和接收数据包,而不是像写入文件那样处理计算机之间的通信呢?
我们可以用UDP来做到这些。
UDP代表“user datagram protocol”,它是另一个建立在IP之上的协议,但与TCP不同,UDP不是增加很多特性和复杂性,而是一个在IP之上非常薄的层。
使用UDP,我们可以将数据包发送到目标IP地址(例如112.140.20.10)和端口(例如52423),然后将数据包从一台计算机传递到另一台计算机,直到到达目的地或在途中丢失。
在接收端,我们只是坐在那里监听特定的端口(如52423),当数据包从任何计算机到达时(记住没有连接!)我们得到了发送数据包的计算机的地址和端口、数据包的大小以及可以阅读数据包内容。
和IP一样,UDP也是不可靠的协议。然而,在实践中,大多数发送的数据包都会成功,但通常会有大约1-5%的数据包丢失,偶尔也会出现完全没有数据包通过的情况(记住,在您和目的地之间有很多计算机,在这些计算机中,事情可能会出错…)
也不能保证使用UDP数据包的顺序。您可以按顺序1、2、3、4、5发送5个数据包,它们可能完全不按顺序到达,如3、1、2、5、4。在实践中,数据包通常是按顺序到达的,但您不能依赖于此!
UDP还提供了一个16位校验和,理论上它是为了保护您不接收无效或被截断的数据,但您甚至不能相信这一点,因为当您在很长一段时间内快速发送UDP数据包时,只是16位是不够的。总的来讲,您甚至不能依赖这个校验和,必须添加自己的校验和。
所以简而言之,当你使用UDP时,你基本上只能靠你自己了!
我们必须决定,我们使用TCP还是UDP?
让我们来看看每个的属性:
TCP:
UDP:
决定似乎很清楚,TCP做了我们想要做的一切,而且它非常容易使用,而UDP则是一个巨大的麻烦,我们必须从头开始编写代码。
很明显我们只是使用TCP,对吗?
错!
使用TCP是开发多人游戏时可能犯的最严重的错误!为了理解原因,您需要了解TCP实际上在IP上执行的操作,以来看清楚一切。
TCP和UDP都是在IP之上构建的,但它们完全不同。UDP的行为与它下面的IP协议非常相似,而TCP抽象了所有内容,因此看起来就像是在读写文件,对你隐藏了所有复杂的数据包和不可靠的信息。
那它是怎么做到的呢?
首先,TCP是一个流协议,所以您只需将字节写入一个流,而TCP会确保它们到达另一端。由于IP是建立在数据包之上的,而TCP是建立在IP之上的,因此TCP必须将数据流分解成数据包。因此,TCP内部的代码将您发送的数据排入队列,然后当足够的数据在队列中时,它将一个数据包发送到另一台机器。
如果发送非常小的数据包,这对于多人游戏来说可能是个问题。这里可能发生的情况是,TCP可能会决定,在缓冲足够的数据以形成一个合理大小的包之前,它不会发送数据。
这是一个问题,因为您希望您的客户端播放器输入尽快到达服务器,如果它被延迟或“聚集”像TCP的小数据包,客户端的多人游戏的用户体验将非常差。游戏网络更新会延迟和低频率,而不是像我们希望的那样准时和频繁地到达。
TCP有一个选项来修复这个行为,称为TCP_NODELAY。此选项指示TCP不要等待足够的数据排队,而是立即发送您写入的任何数据。这被称为禁用Nagle’s algorithm。
不幸的是,即使您设置了这个选项,TCP对于多人游戏仍然有严重的问题,这一切都源于TCP如何处理丢失和无序的数据包,从而为您呈现可靠、有序的数据流的“幻象”。
从根本上讲,TCP将数据流分解为数据包,通过不可靠的IP发送这些数据包,然后从另一端接收到的数据包并重建数据流。
但是当一个包丢失时会发生什么呢?
当数据包无序到达或被复制时会发生什么?
由于TCP的超复杂(请参阅TCP/IP Illustrated)因此不必太详细地介绍TCP的工作原理,本质上是TCP发送一个数据包,等待一段时间,直到它检测到数据包由于没有收到ACK(或确认)而丢失,然后将丢失的数据包重新发送给另一台机器。重复的数据包被丢弃在接收端,无序的数据包被重新排序,所以一切都是可靠和有序的。
问题是,如果我们要通过TCP发送时间关键的游戏数据,每当丢掉一个包时,它必须停止并等待该数据被重新发送。是的,即使有更新的数据到达,新的数据也会被放入队列中,并且在丢失的数据包被重新传输之前,您不能访问它。重新发送数据包需要多长时间?
好吧,TCP至少需要往返延迟才能计算出需要重新发送数据,但通常需要2*RTT,和从发送方到接收方的另一个单向行程,以便重新发送数据包到达那里。因此,如果你的ping时125ms,那么您将等待大约1/5秒的时间来重新发送数据包数据,在最坏的情况下,您最多可以等待半秒或更长的时间(考虑一下如果重新发送数据包的尝试失败会发生什么情况?).如果TCP决定数据包丢失表示网络拥塞,并且切断它,会发生什么情况?是的,它确实这么做了。欢乐时光!
在实时游戏(如fps)中使用tcp,与Web浏览器、电子邮件或大多数其他应用程序不同,这些多人游戏对包传递有实时要求。
这意味着,对于游戏的许多部分,例如玩家输入和角色位置,不管一秒钟前发生了什么,游戏只关心最新的数据。
TCP的设计并没有考虑到这一点。
考虑一个非常简单的多人游戏的例子,一种类似射击的动作游戏。你想用一种非常简单的方式把它联网。从客户端向服务器发送输入的每个帧(如按键、鼠标输入控制器输入),服务器处理每个玩家的输入,更新物理模拟,然后将游戏对象的当前位置发送回客户端进行渲染。
因此,在我们的简单多人游戏中,每当一个包丢失时,所有东西都必须停止并等待该包被重新发送。在客户端游戏中,对象停止接收更新,因此它们似乎静止不动,而在服务器上,输入停止从客户端获取,因此玩家无法移动或射击。当重新发送的信息包最终到达时,您会收到这个过时的信息,您甚至不关心这些信息!另外,队列中有备份的数据包等待同时到达的重新发送,因此您必须在一个帧中处理所有这些数据包。所有东西都堆在一起了!
不幸的是,您无法修复此行为,这只是TCP的基本特性。这正是使不可靠的基于数据包的互联网看起来像可靠的有序流所需的。
事实是,我们需不要一个可靠有序的流。
我们希望我们的数据尽可能快地从客户机到服务器,而不必等待丢失的数据被重新发送。
这就是为什么在网络传输时间关键数据时永远不应使用TCP的原因!
对于诸如玩家输入和状态之类的实时游戏数据,只有最新的数据是相关的,但是对于其他类型的数据,比如从一台机器发送到另一台机器的一系列命令,可靠性和顺序可能非常重要。
接下来的诱惑是使用udp作为玩家的输入和状态,而tcp作为可靠的有序数据。如果你很聪明,你可能已经知道你可能有多个可靠的命令流,一个关于关卡加载,另一个关于AI。也许你自己会想,“好吧,如果一个包丢失了,包含一个关卡加载命令,我真的不想让AI命令暂停——它们完全无关!“。您是对的,因此您可能会尝试为每个命令流创建一个TCP套接字。
从表面上看,这似乎是个好主意。问题在于,由于TCP和UDP都建立在IP之上,因此每个协议发送的底层数据包都会相互影响。它们如何相互影响是相当复杂的,并且与TCP如何执行可靠性和流控制有关,但从根本上来说,您应该记住,TCP往往会导致UDP数据包中的数据包丢失。有关更多信息,请阅读这篇有关此主题的文章。
另外,混合使用UDP和TCP也相当复杂。如果混合使用UDP和TCP,则会失去一定的控制。也许您可以像TCP那样以更有效的方式实现可靠性,更好地满足您的需求?即使您需要可靠的有序数据,只要数据相对于可用带宽来说很小,就可以更快、更可靠地传输数据,如果您通过UDP发送数据的话。另外,如果你必须做一次NAT才能让家庭互联网连接彼此连接,分别用UDP和TCP做一次NAT穿透(甚至不确定这是否可能…)是一种痛苦。
我的建议不仅是你使用UDP,而且你只使用UDP作为你的游戏协议。不要混合使用TCP和UDP!相反,学习如何在自己的自定义基于UDP的协议中实现所需的TCP的特定功能。
当然,在游戏运行时使用HTTP与一些RESTful服务进行对话是没有问题的。我不是说你不能那样做。在游戏运行时运行的一些TCP连接不会使所有事情都失败。关键是,不要将游戏协议拆分为UDP和TCP。保持你的游戏协议在UDP上运行,这样你就可以完全控制你发送和接收的数据,以及如何实现可靠性、排序和避免拥塞。
本系列文章的其余部分将向您展示如何做到这一点,从在UDP上创建自己的虚拟连接,到创建自己的可靠性、流控制和避免拥塞。