網游中的網絡編程3:在UDP上建立虛擬連接


目錄

  1. 網游中的網絡編程系列1:UDP vs. TCP
  2. 網游中的網絡編程2:發送和接收數據包
  3. 網游中的網絡編程3:在UDP上建立虛擬連接
  4. TODO

二、在UDP上建立虛擬連接

介紹

UDP是無連接的,一個UDPsocket可以被用做,與任意數量的計算機交換數據包。然而,在多人游戲中,我們只希望在一小部分建立起連接的計算中,交換數據包。

所以,我們需要做的第一步就是:在UDP上讓兩台計算機,建立起虛擬連接。

但是,首先,我們先深入到底層,弄清楚互聯網是如何工作的。

互聯網不是一系列的電話線

在2006年, Senator Ted Stevens做了一個互聯網歷史上,著名的一次演講:

“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瀏覽器上網,當時漫無目的的瞎逛。

我當時想:每次連上一個網站,就產生一些“真實的連接”,就像電話線。我十分驚奇,當我每次訪問一個新的網站的時候需要花費多少錢?(作者當時認為,每次訪問網站都是建立在一條通信線路之上,就像電話線,需要拉線)不會有人找上門,讓我付這些線路的費用吧?

當然,這個想法現在看起來很傻。

沒有直接的連接

互聯中:沒有一條通信電纜,直接通信的兩台計算機。數據是由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地址和端口連接服務器的計算機,我們成為‘客戶端’。

我們把場景設定為(先設定簡單的場景,一點點來):不論何時,我們只允許一個客戶端連接服務器。同時,我們假定服務器的IP地址不變,客戶端是直接連接服務器。后面的文章再說支持多個客戶端連接的例子等,現在先現實我們限定條件下,簡單的虛擬連接,這樣可以更好的理解虛擬連接。

協議id

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實現

還是推薦看,英文原文中的源代碼

看完理論部分,下面我就用python根據上述原理實現:我寫的UDP上實現虛擬連接只做了兩件事:每次只能有一個socket和server進行通信;如果在一段時間無數據傳輸,則注銷掉原來的連接,允許建立新的連接。

注:下面代碼中很多細節沒有處理,僅供大家參考。

  • protocol id:我打算用時間戳hash一個字符串
  • 監測斷開連接:通過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上,代碼運行效果如下:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM