UDP是无链接的,一个UDPsocket能够被用作,与任意数量的计算机交换数据包。然而,在多人游戏中,咱们只但愿在一小部分创建起链接的计算中,交换数据包。html
因此,咱们须要作的第一步就是:在UDP上让两台计算机,创建起虚拟链接。python
可是,首先,咱们先深刻到底层,弄清楚互联网是如何工做的。linux
在2006年, Senator Ted Stevens作了一个互联网历史上,著名的一次演讲:git
“The internet is not something that you just dump something on. It’s not a big truck. It’s a series of tubes”程序员
当我第一次使用互联网的是:1995年,我在大学的计算机实验室中,我用Netscape浏览器上网,当时漫无目的的瞎逛。github
我当时想:每次连上一个网站,就产生一些“真实的链接”,就像电话线。我十分惊奇,当我每次访问一个新的网站的时候须要花费多少钱?(做者当时认为,每次访问网站都是创建在一条通讯线路之上,就像电话线,须要拉线)不会有人找上门,让我付这些线路的费用吧?编程
固然,这个想法如今看起来很傻。浏览器
互联中:没有一条通讯电缆,直接通讯的两台计算机。数据是由IP协议,经过数据包,从一个个电脑传递过来的。(就像传纸条)服务器
一个数据包可能经过几个计算机才能到达目的地。你不能知道准确的传递过程(第一步,第二步。。。),这个过程是会变化的,是根据网络质量决定数据包的下一步走向。你可能发送过两个数据包A和B到同一个地址,它们可能走的是不一样的路线。这个也是数据包无序的一个缘由。网络
在Linux和Unix系统上(win能够用‘tracert’),可使用‘traceroute’指令来查看数据包的传递线路和途径的主机名和IP地址。traceroute请参考
试一下traceroute
指令:
traceroute: Warning: baidu.com has multiple addresses; using 220.181.57.217 traceroute to baidu.com (220.181.57.217), 64 hops max, 52 byte packets 1 192.168.1.1 (192.168.1.1) 4.727 ms 4.960 ms 4.144 ms 2 223.20.160.1 (223.20.160.1) 13.405 ms 6.047 ms 8.561 ms 3 218.241.252.185 (218.241.252.185) 4.735 ms 2.130 ms 7.771 ms 4 218.241.252.197 (218.241.252.197) 6.849 ms 5.335 ms 4.555 ms 5 202.99.1.217 (202.99.1.217) 4.025 ms 13.324 ms 3.761 ms 6 * 218.241.244.21 (218.241.244.21) 8.492 ms 218.241.244.9 (218.241.244.9) 5.389 ms 7 218.241.244.33 (218.241.244.33) 6.699 ms 4.851 ms 5.386 ms 8 * * *
注意:第八行是由于有ICMP防火墙,请求被拒绝了,因此没有探测出目的ip地址。
这个过程就能诠释:没有直接的链接。
正如第一篇文章,举的那个简单的例子:收到数据包,就像在一个房间,人们手递手传纸条。
互联网是网络的网络(网络的集合)。固然咱们不只在一个小房子中传递信件,咱们能把它传到世界各地。
最好的例子是邮局系统!
当你想要发一封信给别人,你须要把你的信放到邮箱中,同时你会相信会到达收件人的手上。信件怎么到的,你不须要关心,反正到了。总得有人把你的信送到目的地,那么究竟是怎么送到的呢?
首先,邮递员不会拿着你的信,直接送到目的地!邮递员拿着你的信,送到当地邮局,让邮局处理。
若是这封信是本地的,本地的邮局会收过来,安排让另一个邮递员直接送到目的地。可是,若是信的地址不是本地的,那么本地的邮局不会直接把信送到目的地,因此邮局会送到上一级(镇邮局送到市邮局),或者送到临近城市的邮局,若是目的地太远就会送到飞机场。信件的传输方式是用大卡车。
咱们来看一个例子:假定一封信,从洛杉矶寄到北京,本地邮局接收到信,而后发现是国际信件,就直接送到洛杉矶的邮件中心。这封信,确认收件的地址无误,就安排到下一班飞机飞往北京的航班。
飞机着陆在北京,北京的邮件系统确定是和洛杉矶的邮件系统不同。北京的邮件中心收到这封信后,就送到具体的区级的当地邮局,最终,这封信会经过一个邮递员直接送到收件人的手里。
就像邮局系统,经过地址传递信件同样。网络传递数据包是经过IP地址。传递数据包的细节和路径选择是很是复杂的,可是基本思想:每一个路由器都是一台计算机,由路由表决定数据下一步走的地址。(这部分我省略了一些路由和路由表的部分,我没有看懂,后面研究明白,回来再补全。如今不影响后面的阅读)
编辑路由表的工做是网络管理员的工做,不是咱们这些程序员关心的问题(还好😋)。可是,若是你想了解更多关于这方面的知识,能够看看下面这些文章:
如今回到链接的话题上。
若是你使用TCP socket,你知道它是面向链接的,看起来像一个‘链接’。可是TCP是创建在IP协议上的,而IP协议只是数据包在计算机之间传递(并无链接的概念),因此TCP的链接概念必定是:虚拟链接。
若是TCP能够创建再IP上创建虚拟的链接,那么咱们也能在UDP上实现虚拟链接。
让咱们定义虚拟链接:两台计算机间传输UDP数据包以固定的速度,如每秒10个包。只要数据包传输流畅,咱们就认为:两个计算机创建起了虚拟链接。
链接分为两部分:
咱们把场景设定为(先设定简单的场景,一点点来):不论什么时候,咱们只容许一个客户端链接服务器。同时,咱们假定服务器的IP地址不变,客户端是直接链接服务器。后面的文章再说支持多个客户端链接的例子等,如今先现实咱们限定条件下,简单的虚拟链接,这样能够更好的理解虚拟链接。
UDP是无链接的,UDP socket会接收任意计算机发来的数据包。
咱们将限定:服务器只从客户端接收数据包,客户端只给服务器发送数据包(一对一)。咱们不能经过地址过滤数据包,由于服务器不知道客户端的地址(python中socket能够经过recvfrom方法获得地址)。因此咱们在每一个UDP数据包加一个‘头信息’,由32位protocol id组成:
[uint protocol id] (packet data...)
protocol id只是一些惟一的数字。若是数据包的protocol id不能匹配咱们的protocol id,数据包就被忽略。若是protocol id匹配,咱们就接收packet data。
你只须要选择惟一的数字,能够用hash你的游戏名字和协议版本数字。你也能够用任何信息当作protocol id,须要保证protocol id的惟一性,由于这个protocol id是咱们链接协议的基础。
如今咱们须要一个检查链接的方法。
固然咱们能够作一些复杂的握手,此过程须要发送和接收多个UDP数据包。或许客户端‘请求链接’数据包,发送服务器,服务器响应返回给客户端‘链接接受’,或者若是客户端请求与,已经和其余客户端创建起链接的服务器,创建链接,则服务器就会返回给客户端‘忙碌中’。
或者,咱们可让服务器检查接收到的第一个数据包的protocol id是否正确,而后考虑是否创建链接。
客户端假定与服务器创建起链接,而后给服务器发送数据包。当服务器接受到客户端发来的第一个数据包,就记下该客户端的IP地址和端口号,最后,返回响应数据包。
客户端已经知道服务器的地址和端口。因此,当客户端接受数据包,客户端会过滤掉任何不是服务器地址的请求。一样,服务器接收到客户端的第一个数据包,经过recvfrom
方法,能获取到客户端的IP地址和端口。因此服务器也能够忽略不来自指定客户端的任何数据包。
咱们可使用这个简洁的方式,由于咱们只须要在两台计算机之间创建链接。在后面的文章中,咱们会升级咱们的链接系统,用于支持两个以上的计算机链接,而且使得链接更加健壮。
(就是与特定ip地址和端口的计算机进行传输数据)
咱们如何检测断开链接?
若是一个链接被定为接收数据包,那么断开链接就能够定义为不接收数据包。
为了查明咱们没有接受数据包,服务器和客户端两边都计算:从上一次接收到数据包的开始,到下一个接收到数据包的时间。(也就是所谓的‘超时时间’)
每次若是咱们接收到数据包,就重置计时器(’超时时间’清零)。若是计时器超过设定的值,侧链接‘超时’,咱们就是断开链接(再也不限制链接客户端的IP和端口)。
这也是一种优雅的方式用来处理,第二个客户端请求已创建链接的服务器的状况。创建起链接的服务器不会接收来自其余客户端的数据包,因此第二个客户端接收不到服务器响应的数据包,因此第二个客户端链接超时并处于断开链接的状态。
这些就是创建虚拟链接的过程:创建链接,过滤不是来自链接的计算机的数据包,检查断开链接,设定超时。
咱们的创建的链接跟其余TCP链接同样,稳定的UDP数据包传输是多人动做游戏的基础。
目前为止,已经在UDP上创建虚拟的链接,你就可使用它来进行多人游戏中的,client/server模式下的数据传输,来替代TCP。
仍是推荐看,英文原文中的源代码
看完理论部分,下面我就用python根据上述原理实现:我写的UDP上实现虚拟链接只作了两件事:每次只能有一个socket和server进行通讯;若是在一段时间无数据传输,则注销掉原来的链接,容许创建新的链接。
注:下面代码中不少细节没有处理,仅供你们参考。
settimeout()
方法,捕获socket.timeout异常test_server.py
#!/usr/bin/env python # -*- coding:utf-8 -*- # # Author : XueWeiHan # E-mail : 595666367@qq.com # Date : 16/5/11 下午3:54 # Desc : server import socket import time UDP_IP = '' UDP_PORT = 5000 _ID = [] # 存储创建链接的protocol_id _IP = None # 存储创建链接的IP和端口 TIME_OUT = 2 # 超时时间(s) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((UDP_IP, UDP_PORT)) sock.settimeout(TIME_OUT) def check_protocol_id(protocol_id, _ID): ''' 检测protocol_id ''' if _ID: if protocol_id in _ID: return True else: return False else: _ID.append(protocol_id) return True print '准备接收内容。' while 1: try: response = '' data, addr = sock.recvfrom(1024) # 缓冲区大小为1024bytes protocol_id, data = data.split('|') if _IP: if _IP == addr: response = '创建链接' print '从{ip}:{port},接收到内容:{data}'.format(ip=addr[0], port=addr[1], data=data) else: response = '没法创建链接' else: if check_protocol_id(protocol_id, _ID): _IP = addr response = '创建链接' print '从{ip}:{port},接收到内容:{data}'.format(ip=addr[0], port=addr[1], data=data) else: response = '没法创建链接' # 返回响应数据包给客户端 sock.sendto(response, addr) except socket.timeout: print '链接超时,注销链接,其余socket能够连入' _IP = None _ID = []
test_client.py
#!/usr/bin/env python # -*- coding:utf-8 -*- # # Author : XueWeiHan # E-mail : 595666367@qq.com # Date : 16/5/11 下午3:54 # Desc : client import socket import time import hashlib UDP_IP = '' UDP_PORT = 5000 MESSAGE = 'Hello, world!' TIME_OUT = 3 print 'UDP 目标IP:', UDP_IP print 'UDP 目标端口:', UDP_PORT print '发送的内容:', MESSAGE class Udp(object): def __init__(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.server_addr = None # 设置超时时间 self.socket.settimeout(TIME_OUT) @property def protocol_id(self): hash = hashlib.md5(str(time.time()) + 'xueweihan') return hash.hexdigest() def send_mesaage(self): # 这里只简单用|分割protocol_id和发送内容 message = self.protocol_id +'|'+ MESSAGE self.socket.sendto(message, (UDP_IP, UDP_PORT)) def get_message(self): data, addr = self.socket.recvfrom(1024) if self.server_addr: # 客户端也只接收创建链接的服务端的数据包 if self.server_addr == addr: return data else: return None else: self.server_addr = addr return data s1 = Udp() for i in range(2): try: s1.send_mesaage() print s1.get_message() except socket.timeout: print '链接超时' # 清除原来创建链接的数据 s1.server_addr = None s2 = Udp() for i in range(2): # 此时是没法创建链接的,由于上一个链接尚未销毁 try: s2.send_mesaage() print s2.get_message() except socket.timeout: print '链接超时' # 清除原来创建链接的数据 s2.server_addr = None # 暂停2秒,等待服务器注销上一次的链接 time.sleep(2) s3 = Udp() for i in range(2): # 此时是能够创建链接的,由于上面链接以超时 try: s3.send_mesaage() print s3.get_message() except socket.timeout: print '链接超时' # 清除原来创建链接的数据 s3.server_addr = None
上面代码还有不少不足的地方(TODO:超时,部分有问题),因此仅供参考。全部代码都在github上,代码运行效果以下: