原文:http://gafferongames.com/networking-for-game-programmers/virtual-connection-over-udp/
Introduction
大家好,我是Glenn Fiedler,欢迎阅读我的网上电子书《游戏程序的网络设计》第三章。
在上一章中,我向你展示了如何使用UDP收发数据包。
因为UDP是无连接传输模式,一个UDP套接字可以用来与任意数量的不同的电脑交换数据包。然而在多人游戏中,一般来说,我们只希望一小部分连接的电脑之间交换数据包。
作为一个通用连接系统的第一步,我们将从最简单的可能情况开始:基于UDP在两台计算机上建立虚拟链接。
但首先,我们需要更深入的了解互联网实际工作情况。
The Internet not a series oftubes
在2006年,Senator Ted Stevens在网络立法中,做了互联网里程碑的著名演讲。
互联网不是你只是往里面扔东西。它不是一个大卡车。它是一系列的管子。
当我最开始使用互联网时,我就像Ted一样。1995年,坐在悉尼大学的计算机实验室,我使用网景浏览器在网上冲浪,我完全不知道发生了什么。
你懂得,我认为每次你连上一个网站,这里很可能会有一些实际的连接在进行,就像电话线路。我猜测,每次我连上一个新的网站要花多少钱?30美分?1美元?大学里会有人拍下我的肩膀,问我支付长途收费吗?
当然,现在看来这都太扯淡了。
你通过物理电话线连接到另一台计算机时这里并不存在接线总机,更不用说像Sen说的那样有一系列的气管。Stevens无法会让你相信的。
No direct connections
相反,你的数据基于互联网协议(IP)通过包,从计算机发送到计算机。
在一个数据包到达目的前,它可能会经过一系列的主机。你无法提前知道确切计算机组,因为这会根据互联网路由数据包而动态切换。甚至可能你发送数据包A和B到同一个地址,但它们可能会经过不同的路由。这就是用UDP传输数据无法保证次序的根源。
在类unix系统中,我们可以给”traceroute”命令一个目的地主机名或来探测数据包的路由。
在windows用“tracer”命令代替”traceroute”。
像这样尝试得到一些网站的路由:
traceroute slashdot.org
traceroute amazon.com
traceroute google.com
traceroute bbc.co.uk
traceroute news.com.au
查看一下,很快你就会明白这里不存在直接连接。
How packets get delivered
在第一章,我为数据包的发送提供了一个简单比喻,就像是在一个拥挤的房间,一张纸条经过一个又一个人的传递。
虽然这个比喻得到基本的观点穿过,但它太简单。互联网不是一个扁平的电脑网络,它是一个网络到另一个网络。因此,我们不只是在一个小屋子里传递信件,我们很可能会需要把它们发送到任何地方。
最好的比喻是邮政服务,它能更清楚地展示这一切。
当你想给任何人写信时,你只是把你的信放在邮箱里,你相信它会被正确送达。对你而言,它是怎么达到的,花了多长时间并不关心。事实上肯定有人把你的信送到目的地,那么这个过程是怎样的呢?
当然首先,邮递员不会亲自把你的信送到目的地。似乎邮政系统也不是一系列的管道。反而,邮递员会把你的信送到当地邮局。
如果这封信是本地的,那么邮局就把这封信发出去,由另一位邮递员直接送出去。但是,如果这个地址不是本地的,那就变得有趣了。当地邮局不能直接把信送出去,所以它把信传到邮政系统的上一级,也许是服务附近城市的地区邮局,或者一个在机场的邮件中心,如果地址实在太远了。理想地,实际传输也许要用到大卡车。
让我们假设这封信人洛杉机到悉尼。当地的邮局收到信后,因为它是国际信件,就把它发到洛杉机的邮件中心。这封信按地址进行再次处理,经过路由后空运到悉尼。
飞机降落在悉尼机场,这封信被一个完全不同的邮政系统接管。现在整个过程反过来开始操作。这封信分检的层次是从一般到特殊。从在悉尼机场的邮件中心发送到一个地区中心,地区中心发送到当地的邮局,最后这封信被一个有滑稽口音的邮递员送到。唉呀!
就像是邮局决定怎么传递信件到它的目的地。互联网根据数据包的IP地址发送数据包。数据包传输的低层细节和实际路由过程相当复杂,但最基本的观念是每一个路由器就是另一个计算机,并有一张路由表描述相应地址的数据包应该送到哪里去,如果路由表中没有的地址,就发送到默认网关那里去。路由表和他们所代表的物理连接,定义了网络到网络,这就是互联网。
配置路由表的工作是网络管理员的事,而不是我们程序的事。但如果你想了解关于这个的更多消息,arstechnica的一些文章提供了一些更深入的介绍关于互联网如何在对等网络中交换数据包和传输关系。你也可以在linux的常见问题中找到更多的关于路由表的详细信息。在维基百科里找到对边界网关的定义,自动发现如何路由网络之间的数据包,使网络成为一个真正的分布式系统,能够在破碎的连接间动态路由。
Virtual connections
现在回到连接上来。
如果你使用TCP套接字,你就会知道它看起来是一个连接,但因为TCP是基于IP协议实施的,并且IP协议是在一台台计算机上跳跃的包,因此TCP的连接一定是虚拟连接。
既然TCP能够通过IP协议创建虚拟连接,那么同样的我们也可以使用UDP来完成。
让我们定义在两台计算机以固定速率每秒10个包,交换UDP数据的虚连接。只要包在流动,我们就认为两个计算机之间存在虚连接。
我们的连接有两边:
一台计算机在监听,以便另一台计算机连过来。我们叫它服务器。
另一台计算机连上服务器,通过指定IP地址和端口号。我们叫它客户端。
就我们而言,在任何时候我们仅允许一个客户端连上服务器。在后面的文章中,我们会推广我们的连接系统以支持多个并发连接。同样,我们假设服务器是固定的IP地址,客户端可以直接连过来。在后续的文章中我们会介绍matchmaking和 NATpunch-through。
Protocol id
因为UDP是无连接传输模式,所以我们的UDP套接字可以收到来自任何计算机的包。
我们想缩小这个范围,这样我们的服务器就只能收到客户端的包了,客户端也只能收到服务器发的数据包。我们不能通过地址来过滤数据包,因为服务端提前并不知道客户端的地址。所以我们在每个UDP包前弄一个32bit长度的协议头,如下所示:
[uint protocol id]
(packet data...)
这些协议ID只是一些不相等的数字代表我们的游戏协议。任何一个数据包到达我们的UDP套接字,首先我们会对它的4bytes进行检查。如果不能匹配就丢掉这个包。如果匹配,我们就移走头4字节,并传输剩下的内容。
你选择一些数字只要是合理的,不相等,就可以。或许hash你的游戏名和你协议的版号是个不错的选择。但真地你可以用任何数字。关键点在于,我们的连接基于协议,数据包中不同协议的ID被忽略掉了。
Detecting connection
现在我们需要一个方式来检测连接。
当然我们可以做一些复杂的握手协议涉及多个UDP数据包来回发送。或许一个客户端可以发出“链接请求”的数据包到服务端,服务端回复一个“接受链接”给客户端,或者一个“我很忙”的数据包,当一个客户端试图链接到服务器,而服务器已经被客户端链接了。
或者……我们可以设置我们的服务器来取第一个数据包,如果接收正确的协议id就考虑建立一个连接。
客户端从发送数据包给服务端就假定开始连接,当服务器从接收到客户端第一个包开始,它注意到客户端的IP地址和端口号,并开始回复数据包。
客户端已经知道了服务器的地址和端口号,因为它指定连接。所以当客户端收到数据包时,它过滤掉不是来自服务端的数据包。类似地,一旦服务端收到来自客户端的第一个包,通过“recvfrom”函数就得到了客户端的地址和端口号,所以它能够忽略掉不是客户端发来的数据包。
我们能够摆脱这个快捷键,因为我们只有两个有关的电脑连接。在以后的文章中,我们会扩大我们的连接系统,支持超过两台计算机在客户机/服务器或对等拓扑,在这一点上我们会升级我们的连接协议以便更健壮。
但是现在,为什么让事情比需要地更加复杂呢?
Detecting disconnection
我们怎么来检测链接断开呢?
如果连接被定义为接收信息包,我们可以定义断开为不接收信息包。
为了检测,当我们没接收到数据包时,我们保持跟踪这个秒数自从最后一次在链接中从另一端收到数据包。我们在两端都这样做。
每次我们从另一端收到数据包,我们将累加器重置为0.0,每次检测时间时我们让累加器增加过去的时间。
如果累加器超过比如10秒,连接就“超时”,我们就认为断开了。
这样做也能很巧妙处理已经有一个客户端连接到服务器的情况时,第二个客户端又连上来了。因为服务器已经忽略掉非链接客户端的数据包,所以第二个客户端收不到任何数据包,来回应它发出的数据包,所以第二个客户端就会超时退出。
Conclusion
这就是设置虚拟链接的所有操作:哪些方式来建立链接,过滤掉链接外的数据包,超时来检测链接断开。
我们的链接和一些TCP的真实链接一样。源源不断到UDP数据包对多人动作游戏提供了一个合适的起点。
我们也要关心下互联网是怎么进行包的路由。比如,我们现在知道UDP数据包有时不能按次序到达是因为它们在IP协议中经过不同的路由。看下互联网地图,是不是对你的数据包到达很不可思议?这是另一个可视化显示上层自治系统层级关于IPV6。如果你愿意更深入地了解这些,另一个开始点是维基上的这片文章。
现在你已经有基于UDP的虚拟链接了,你可以很容易不用TCP就设计一个客户端/服务端关系的多人游戏。
你可以看下事例代码是如何实现这章介绍的操作。
它是一个很简单的客户端/服务端程序,每秒大约交换30个包。你可以在任何你喜欢的机器上运行服务端,给它提供一个公网IP,因为我们并不支持NAT穿越。
像这样运行客户端:
./Client 205.10.40.50
而且它会试图去链接你在命令行给出的服务端地址。如果你不指定地址,默认情况下会链接127.0.0.1。
当一个客户机连接时,你可以尝试连接另一个,你会注意到无法连接。这是故意的。因为在这个时候,只有一个客户端可以链接。
你也可以试着停止客户端和服务端当它们链接时,你会注意到大约10秒后,更一端会超时退出。当客户端超时退出时,服务端还是保持监听状态,准备另一个客户端的接入。
在它变得更复杂前,看一下源文件,因为下一章,我们打算使用序列号和回应来添加可靠性和流控制来连接。