原文:http://gafferongames.com/networking-for-game-programmers/udp-vs-tcp/
介绍
大家好,我是Glenn Fiedler,欢迎阅读我的网上电子书《游戏程序的网络设计》第一章。
在这一章中,我们将从网络编程的最基本方面开始,通过网络收发数据。这是一切的开始—网络程序员工作中最简单和最基本的操作,但最佳的网络通讯策略还是很复杂并且不明显。小心一点,因为如果你在这一步出错了,会对你的多人游戏中造成很可怕的影响。
你或许很可能听过套接字,并且可能意识到这里有两种主要的方法:TCP和UDP。当编写一个网络游戏时,我们首先需要选择使用哪种类型的套接字。我们是使用TCP,UDP,还是同时混合使用。
你所做的决定完全取决于你编写的网络游戏是哪种类型。基于这点考虑和这系列剩余的文章,我假定你是编写一个动作游戏,诸如此类:Halo, Battlefield 1942, Quake, Unreal, CounterStrke, Team Fortress。
在我们打算编写动作游戏的情况下,我们需要进一步观察各种类型套接字的性能,更深入了解互联网的实际工作情况。一旦我们了解清楚了所有的这些信息,我们会很容易做出正确的选择。
TCP/IP
TCP表示“传输控制协议”,IP 表示 “互联网协议”。它们几乎是你网上所做一切操作的基础,从网页浏览器到IRC到电子邮件,所有的一切都是在TCP/IP的基础上建立起来的。
如果你曾经使用过TCP套接字,你就会知道它是基于可靠的连接协议。简而言之,你在两台机器之间建立起连接,然后你在两个计算机传输数据就像是在一台上写文件,然后在另一台上读文件。
这个连接是可靠而且有序的,意味着你所有传输的数据到达另一台计算机时,数据的顺序和你写入的顺序是一样的。它也是流式数据,意味着TCP将你的数据分拆成数据包并且通过网络传输这些数据。
再次,请记住这就像是写一个文件。很简单!
IP
TCP的简单与实际运行的下层IP协议形成鲜明的对比。
这里没有连接的观念代替包从一个计算机到另一个计算机。你可以相像这个过程就像是在一个拥挤的房间,从一个人手里传到另一个手里一张纸条,在经过多个人手传递后才到达最终目的地。
并不能保证这张纸条一定会送到目的人手中。传递者一直传递纸条并做最好的打算,从不知道这个纸条是否会被下个人收到,除非下个人有回执过来。
当然,实际情况更加复杂,因为没有一台计算机知道确切的序列号这个包经过哪些计算机,以便更快地达到目的地。有时IP传递同一个包的多个副本,这些包通过不同的路径达到目的地,所以它们很可能在不同的时间达到。
这是因为互联网被设计为自己组织和自己重复,以绕过连接问题。在低级上的实际工作普遍放实际更酷,你可以阅读一些经典的关于TCP/IP的插图书。
UDP
如果不像写文件一样在电脑间进行通讯,我们该如何直接的发送和接收数据包呢?
我们还能使用UDP来操作。UDP表示“用户数据包协议”,它是基于IP的另一个协议,和TCP类似,但这次并没有添加大量的功能和复杂性,只是做了很简单的一层封装。
通过UDP我们可以直接向目的IP(比如:112.140.20.10)和端口(比如:52423)发送数据包。这个数据包会在一个个电脑间传递,直到达到目的地,或者在传递途中被丢掉。
作为接收者,我们只是在这里监听指定的端口(比如:52423),当从其它电脑传递过来一个数据包(记住这里没有连接了!)我们会注意到发送这个数据包的电脑的端口和IP地址,这个包的大小,并且可以读这个包的数据。
UDP是不可靠传输协议。事实上,大多数的数据包都能正常到达,但通常有1%--5%的丢包率,并且偶尔可能一个包也收不到(记住在你和目的计算机之间存在大量的计算机,它们可能会出错…)
包的顺序通常也没保证。可能你发送五个包以1,2,3,4,5的顺序,但这些数据包完整达到目的地时,顺序可能变成3,1,2,5,4。事实上,它们在所有的时间上都是按顺序达到,但这是不能保证的。
尽管UDP并没有在IP协议上层做过多的操作,但它能够保证一点。如果你发了一个包,它要么会全部达到目的地,要么就全部丢失。所以如果你发送256byte的一个包到另一个计算机,那么那台计算机不可能只收到这个包的初始100bytes数据,它肯定能得到全部的256bytes的数据。这是你使用UDP得到的唯一保证,而其它的一切都是由你决定的。
TCP vs.UDP
现在我们要做出决定,我们是使用TCP套接字还是使用UDP套接字。
我们来看下各自的特点:
TCP:
基于连接的
可靠性和顺序保证
自动将你的发送数据打包
在互联网连接中确保数据不会发送的过快(流控制)
方便使用,你读和写数据,就和操作文件一样
UDP:
没有连接的概念,你不得不自己写代码
不能保证数据包传输的可靠性和顺序性,它们可能顺序达到,也可能出现重复包,甚至一个也不达到
你必须自己将发送数据打包然后进行发送
你必须自己确保网络连接在掌控中,发送数据不能过快
如果一个包丢掉了,你需要设计出几中方法来发现,如果必要的话,要设法重传
这个决定似乎很明确了,TCP做了我们想做的所有事,并且超级简单,而UDP则十分不便,我们不得不自己从头编写所有的代码。所以很明显,我们应该使用TCP对吧?
错了。
当你设计一个网络游戏时,使用TCP可能是你做得最差的选择。为了理解为什么,你需要去观察下基于IP协议的TCP让一切变得如此简单的实际工作方式。
TCP的实际工作方式
TCP和UDP都是基于IP协议的,但它们根本不同。UDP的表现很明显工作在IP协议之上,但TCP抽象了一切操作,使用它就像是读写一个文件,对你隐藏了传送数据包的复杂和不可靠。
但它究竟是如何做到这点呢?
首先,TCP是一个流式协议,所以你只需写数据到一个流,TCP会确保它们到达另一方。因为IP协议是建立在数据包上的,而TCP协议又是建立在IP协议上的,因此TCP必需将你的流式数据拆分成数据包。所以,一些内部的TCP代码将你发送的数据放入消息队列,当队列中有足够的数据,它会发包给另一个机器。
在多人游戏中,这就会有一个问题,如果你发送一个很小的数据包。这里会发生什么呢?TCP可能会决定它不会立即发送数据,直到缓冲区有足够的数据组成一个合理大小的包(通常大约是超过100 bytes)。这就有一个问题,因为你希望你的客户端的操作尽可能快地达到服务器,如果它有延迟或者像TCP那样被当作小的数据包“收集”起来,那么客户端的用户体验就会很差。网络游戏的更新就会迟到和少见,而不是像我们想像中的那样准时和频繁。
TCP有个选项,你可以用来修复它的这种行为,叫作“TCP_NODELAY”。这个选项通知TCP不用等到队列中有足够的数据,立即发送你写入的任何数据。
不幸的是,即使这样,对于多人游戏TCP还是有一些很严重的问题。
这一切都源于TCP处理数据包丢失和次序方式,给你一种错觉,可靠、有序的数据流。
TCP如何实现可靠性
从根本上讲,TCP将数据流拆成包,通过不可靠的IP协议发送数据包,然后在另一端接收这些数据包,并还原成数据流。
但如果一个数据包丢失会发生什么呢?如果一个数据包达到的顺序出错或者重复出现时,又会发生什么呢?
不用去知道TCP工作的过多细节,因为那实在是太难懂了(请参考TCP/IP插图)。本质上TCP发送一个数据包,发现数据包丢失后,就会重传这个数据包到另一台机器。重复的数据包在接收方被丢掉,次序出错的数据包会重组,所以一切都变得可靠和有序。
问题是我们如果使用TCP来进行同步,当一个数据包丢失后,就不得不停下来,等待这个数据包重传。是的,即使最近的数据达了,新的数据已经到达队列了,你还是不能读取它,直到你收到了丢掉的数据包。需要多长时间,它才会重新发包呢?这将花费TCP计算出哪些数据需要重发至少往返延迟,并且另一个单程发送者重发数据包给接收者。所以如果ping 125ms,最佳情况下你将大概等待 1/5th 秒来完成数据包的重发,在最坏的情况下你甚至要等上一分钟或更长(考虑到重发数据失败?)
为什么你不应该使用TCP来设计一个多人游戏的网络
在游戏中使用TCP的问题和浏览器,电子邮件或者大多数程序都不一样,多人游戏在数据包的传输上有一个实时性要求。在你的游戏的很多部分,比如说玩家输入和字符位置,它并不关心一秒前发生了什么,它只是在意最近的数据。
考虑一个多人游戏的非常简单的例子,一些类型的动作游戏比如射击。你希望你的网络很简单。在每一帧你从客户端发送操作数据到服务端(比如按键,鼠标输入,控制输入),并且每一帧服务器处理来自每一位玩家的输入,更新模拟结果,然后把当前的位置返回给客户端进行呈现。
所以在你的简单多人游戏中,当一个数据丢失后,一切都将停止下来等待另一个数据包重发。在客户端游戏角色停止接收更新,所以他们似乎保持静止,在服务端也停止接收客户端的输入信息,所以玩家无法移动或射击。当重发的数据包终于到达,你接收到这个你已经不再关心的旧的数据,过时的信息。另外这里有很多数据包在队列中等待这个重发的数据包,它们将在同时到达,所以你不得不在一帧中同时处理这些数据包。所有的事情都堵在一起。
不幸地是,这里你没法做任何事来改正TCP的这种行为。不管你是否愿意,这都只是它的本质。这也是它所做的使一个不可靠,基于包的网络成为一个可靠的,有序的网络。
我们不需要一个可靠的,有序的命令流。
我们希望自己的数据尽可能快的从客户端到达服务端而不必等待丢失的数据重发。、
这就是为什么多人游戏从不采用TCP来设计网络。
等等?为什么我们不能同时使用UDP和TCP
对于实时游戏数据像玩家的输入和状态,只有最近的数据是有关的,但其它类型的数据,比如从一台机器到另一台机器发送的一系列指令,可靠性和次序性很重要。
所以最佳选择似乎是用UDP处理玩家的输入和状态,TCP处理可靠命令数据。甚至你已经在计算你需要多少个流处理可靠的,次序的命令,似乎一个是关卡载入,一个是AI。或许你自认为,“如果数据包丢失将导致加载一个命令,这是我不想AI命令导致的。它们真没关系。”你是对的,所以你试图为每一个命令流创建TCP套接字。
表面看来,这似乎是个好主意。但问题是TCP和UDP都是基于IP协议的,每个协议潜在的数据包会影响到彼此。他们究竟是如何相互影响是相当复杂的,涉及TCP如何执行可靠性和流控制,但从根本上讲,你要记住易于引起UDP的包丢失。更多信息,请阅读这个章节。
结论
我的建议是你不仅能用UDP,也只能使用UDP。不要混用TCP和UDP,相反了解基于UDP协议如何实现的TCP特定部分。
其余的这个系列文章向您展示如何做到这一点,从创建自己的虚拟连接基于协议在UDP上的,以创建您自己的可靠性和流控制。
接下来,一些更具体的:怎样发送和接收使用UDP数据包