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上,代碼運行效果以下: