游戏开发网络协议对比:TCP VS UDP
本文基于Glenn Fiedler的英文原文翻译,原文请戳这里 为了防止歧义,一些专有词汇将保留英文原文。 以下是翻译全文:
介绍
这篇文章我们将从网络编程最基础的方面说起,即通过网络发送和接收数据。这仅仅是个开始——网络编程中最简单和最基础的部分。但是如何达到最佳实践过程,依然是错综复杂的。如果你弄错了这个部分,那么将会给你的联机游戏带来可怕的负面结果,请务必注意。 你很可能听说过sockets,并且可能已经知道有两种主要的类型:TCP和UDP。当编写一款网络游戏时,我们首先需要选择使用哪种类型的socket。那么我们应该使用TCP sockets,UDP sockets,还是将两种混用呢? 你的选择完全取决于你想做的网络游戏的类型。本文(和后面的系列文章)的内容都将假设你要做的一款动作网游。比如大家熟知的:《光环》、《战地1942》、《雷神之锤》、《虚幻竞技场》、《反恐精英》、《军团要塞》等等。 按照我们想要做一款动作网游的假设,我们将非常详细的考察每种类型socket的特性,并且略微进一步探讨英特网是如何工作的。一旦我们掌握了这些讯息,那么作出正确的选择就易如反掌了。
TCP/IP
TCP代表“传输控制协议”,IP代表“网络互联协议”,他们合在一起组成了互联网的支柱,你在网络上做的一切事情,包括浏览网页,在线聊天、发送邮件等等,都是基于TCP/IP的。 如果你曾经使用过TCP socket,那么你会知道它是一种面向连接的可靠的协议。简单的说,你在两台主机之间创建了一个连接,然后在他们之间发送数据就好像你在一台主机写入一个文件,再从另一个主机从文件中读取出来一样。 这种连接是可靠的、并且是有序的,这就意味着你发送的所有数据必定会按照你写入的顺序送达另一端主机。TCP连接是一种数据流,它负责将你的数据分割成packets并且替你在网络上发送这些数据。 是的,你只需记住TCP协议就像写入文件一样,就是这么简单!
IP
位于TCP协议之下的低层协议“IP协议”则完全是另外一回事。 IP协议没有连接的概念,取而代之的是在一台又一台主机间传递的packets。你可以把这个过程具象化为一张手写的字条在人群中传递,经过无数双手,最终送达收件人手中。 对于字条是否能最终送达收件人手中,并没有任何保证。发件人只是把字条传出去,然后祈祷。也无法知道字条最后能否送达目标手中,除非收件人决定回信。 当然,事实上事情要比这复杂一些,由于并没有一台主机知道这个packet在主机间传递的确切顺序,因此packet能够快速到达它的目的地。有时“IP协议”会传递同一个packet的多分拷贝,这些packets将会通过不同的路径,并且很可能在不同的时间抵达目的地。 这是由于互联网被设计为自我组织和自我修复的,并且有能力克服连接障碍。如果你考虑到在这底层实际上是如何运作的,那么事实上还是很有趣的。你可以通过阅读经典书籍《TCP/IP 详解》来了解其中的所有细节。
UDP
除了像写文件一样的对待主机之间的通信的方式之外,我们还有什么方式直接收发packets呢? 我们可以使用UDP协议。UDP表示用户数据报文协议,它是另外一种基于IP协议构建的协议。就像TCP协议一样,只不过与TCP增加大量的特性和复杂度不同的是,UDP协议仅仅是IP协议之上非常精简的一种协议。 通过UDP协议,我们可以发送一个packet到目的IP地址(如:112.140.20.10)和端口(如52423)。packet会在主机间传递直到它到达目的主机或者在中途丢失。 在接收端,我们只需要监听特定的端口(如52423),当一个packet从其他任何主机到达时(请记住,并没有连接。),我们可以获得发送报文的主机的端口和地址,以及packet的大小,然后就可以开始读取数据了。 UDP协议是一种非可靠的协议。在实际使用中,我们发送的大部分packets可以正常到达目的地,但是通常会有1%-5%的packets会丢失,并且偶尔可能会出现某些时段所有packet都无法成功传输的情况(请记住在你和目标之间有成千上万的主机,随时都可能出现错误)。 报文的次序也没有任何保证。你可能按照1,2,3,4,5发送了5个packets,但他们可能完全乱序到达,比如3,1,2,5,4。在实际中,他们大部分情况下会按照次序到达,但是需要强调的是,你不能依赖这种可能性。 最后,尽管UDP协议在IP协议之上没有提供很多的功能,但它依然保证,如果你发送一个packet,这个packet要么完整的到达目的地,要么就根本不会被送达。因此如果你向其他主机发送了一个256字节长度的packet,它一定能接收到完整的256字符的数据。这几乎是UDP协议唯一能提供的保证,其他所有的一切都需要你自己实现。
TCP vs. UDP
在此我们需要作出一个决定,我们该使用TCP sockets还是UDP sockets呢? 让我们来回顾下它们各自的特性: TCP:
- 基于连接
- 保证可靠传输,按序到达
- 自动分包
- 确保不会过快发送数据,防止互联网拥塞(流量控制)
- 简单易用,你只需要像操作文件一样读写数据
UDP:
- 没有连接的概念,你必须自己实现
- 不保证可靠性和报文的次序,报文可能乱序到达,重复接收或者丢失
- 你必须手动分包和发送
- 你必须自行确保不会发送大量数据导致互联网拥塞
- 如果报文丢失,你需要自行检测这种情况,并且重传
通过对比之后,选择似乎非常明朗了,TCP提供了我们想要的所有特性并且简单易用,而UDP需要我们花费巨大的代价自己实现所有的功能,所以显然我们只要用TCP就好了,对吗? 回答错误。 采用TCP将可能是你在开发一款类似FPS的动作网游时所能犯下的最大错误!想要了解原因,你需要了解TCP是怎样实现让所有事情看起来如此简单的!
TCP工作原理
TCP和UDP都是基于IP之上构建的,但是他们有本质上的区别。UDP的行为和IP协议非常相似。而TCP对所有特性做了抽象,因此它看起来像操作文件一样,替你隐藏了packets的所有复杂性和非可靠性。 那么,它是如何做到的呢? 首先,TCP是一种流协议,因此你只需要将字节写入数据流,TCP确保他们将会送达另一端。由于IP是基于packets的,而TCP是基于IP构建的,因此TCP必须将你的数据流拆分成packets。因此,TCP内部代码会将你发送的数据进行排队,当队列中有足够的数据以后,它会将数据打包成一个packet发送到其他主机。 如果你的联机游戏经常发送一些很小的packets,那么这将会是一个问题。这里可能出现的情况是,TCP可能不会发送你的数据,直到数据缓存到一个合理的packet的大小(比如超过100字节之类的)。如果你希望你的客户端将玩家的操作尽可能快的发送到服务器,那么这就是一个问题了。如果玩家操作被延迟或者像TCP对小的packets那样“缓存”起来,那么客户端玩家的用户体验将会非常糟糕。游戏网络的更新将会变得迟缓而低频,而不是像我们希望的那样及时和迅速。 TCP有个选项是你可以通过设置来修复这种行为,这个选项是TCP_NODELAY。这个选项会使TCP不再等待排队数据缓存到足够的数量,而是马上将你写入的数据直接发送出去。通常这意味着关闭Nagle‘s 算法。 不幸的是,即使你设置了这个选项,对于联机游戏来说,TCP仍然其他有严重的问题。 这都要归咎于TCP如何处理丢包和包的乱序问题,给你产生一种可靠和有序数据流的“假象”。
TCP如何实现可靠性
TCP将数据流拆分成packets,通过非可靠的IP协议发送这些packets,然后在另一端接收这些packets然后重建数据流。 但是如果一个packet丢失了,那么会发生什么?如果packet乱序到达或者重复接收了,将会怎样? 为了防止过分深入TCP的工作细节,因为它太复杂了(请参考《TCP/IP详解》)。本质上TCP发送一个packet,等待一小段时间直到它因为没有接收到ack而检测到packet丢失了,然后它重传丢失的packet。重复的packet会在接收端丢弃,而乱序的packets会重新排序,这样所有的packet就变得可靠而有序了。 问题在于,如果我们试图通过TCP进行同步,当一个packet丢失了,TCP必须停下来等待直到数据被重传。是的,即使更新的数据到达了,这些新的数据会被放入一个队列中,并且你无法访问它们,除非丢失的packet被重传。重传packet需要消耗多长的时间呢?它至少需要一个RTL才能让TCP知道数据需要被重传,但是通常它将耗费2倍的RTT,加上从发送者到接收者的单程开销。如果你的ping值为125毫秒,你将等待至少1/5秒来让packet重发,如果是最坏的情况下,你可能需要等待0.5秒或者更多(考虑下如果重发的packet送达失败将会发生什么?)。如果TCP认为丢包意味着网络拥塞而进行回退算法将会发生什么?是的,TCP这么做了。真欢乐!
为什么你绝不应该在对网络时延要求严格的数据上使用TCP
对于如FPS类的实时游戏来说,使用TCP的问题在于,这类游戏不像WEB浏览器、电子邮件或者大部分其他程序,这些联机游戏对packet传输延迟有实时要求。对于你的游戏的许多部分来说,比如玩家输入和角色位置,超过1秒的反馈已经没有意义了,你只关心当前的数据。TCP在设计之初就完全没有考虑到这点。 以射击类的联机游戏为例,你想要通过非常简单的方式来进行联网。每帧向服务器发送客户端的输入(例如:按键,鼠标输入,控制器输入),并且每帧服务器处理来自每个用户的输入,更新场景,然后将当前游戏物体的位置发回给客户端进行渲染。 如果在我们这个简单的联机游戏中,当一个packet丢失时,你不得不停下所有事情来等待packet重传。在客户端,游戏物体停止更新,因此看起来就好像卡住了一样,而在服务端,停止接收客户端的输入,因此玩家无法移动或者射击。当重发的packet最终抵达之后,你接收到了这个陈旧的,过时的信息,然而你已经根本不关心它了。另外,在到达的同时,队列中还有备份的packets等待重传,你还不得不在一帧中处理所有的这些packets。这导致所有事情都集中在一起了。 不幸的是,不管你是否愿意,你都无法做任何事情来修复TCP的这种行为。这是TCP的天性使然。这就是使得非可靠的、基于packet的互联网看起来像可靠有序的数据流的代价。 然而我们不想要一个可靠有序的数据流,我们希望我们的数据尽快的从客户端传输到服务端,而不需要等待重传丢失的数据。 这就是为什么我们绝不应该在网络时延要求很高的数据上使用TCP。
等等!为什么我不能同时使用UDP和TCP?
对于实时游戏数据,比如玩家输入和状态,只有最近的数据是有用的,但是对于其他类型的数据,比如说从一台机器发送到另外一台机器的一系列的命令,可靠性和有序性将变得非常重要。 因此对于玩家输入和状态使用UDP,对于可靠有序的数据使用TCP变成了一种诱惑。如果你很犀利的话,你很可能甚至已经想出了使用多条可靠有序流的方案,比如一条负责关卡加载,其他负责AI。也许你自己想“嗯,我不希望AI指令被一条包含关卡载入指令的数据包丢失而拖慢,他们是完全无关的。”你想的并没有错,因此你可能想要为每条指令流创建一个TCP socket。 表面上来看,这看起来好像是一个好主意。问题在于,TCP和UDP都是基于IP构建的,每种协议发送的底层报文会互相影响。具体他们之间如何互相影响十分复杂,并且和TCP如何处理可靠性和流量控制有关,但是最基本的你应该记住TCP可能引发UDP丢包。更多信息请参考这篇论文。 而且,混合使用UDP和TCP相当复杂。如果你混用UDP和TCP,你将失去一大部分控制。也许你可以自己实现一套比TCP更有效更符合自己需求的可靠传输?即使你需要可靠有序的数据,也是可能的。使数据比通过TCP传输对可用带宽的需求更小,更快,更可靠的传输到对端。如果你必须通过NAT来开启家庭网络连接来支持语音聊天,你不得不分别实现UDP和TCP的NAT(甚至不知道这是否可行),这将非常痛苦。
结论
我们推荐你用且仅用UDP来实现你的游戏协议。不要混用TCP和UDP,而是学习如何在你自定义的基于UDP的协议中实现TCP提供的你需要使用的特定功能。 当然,在游戏运行中使用HTTP来和一些RESTful服务通信是没有问题的,这不是我们所述的问题。当你的游戏运行时,运行一组TCP连接并不会让系统瘫痪。问题在于,不要将你的游戏协议拆分成UDP和TCP。保持你的游戏协议运行在UDP之上,这样你就可以完全控制你发送和接收的数据的可靠性,有序性和实现拥塞控制。 这个系列的其他文章将向你展示怎么做,包括基于UDP创建你自己的虚拟连接,创建你自己的基于UDP的可靠传输、流控和拥塞控制。