網遊中的網絡編程3:在UDP上創建虛擬鏈接

目錄

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

2、在UDP上創建虛擬鏈接

介紹

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地址和端口鏈接服務器的計算機,咱們成爲‘客戶端’。

咱們把場景設定爲(先設定簡單的場景,一點點來):不論什麼時候,咱們只容許一個客戶端鏈接服務器。同時,咱們假定服務器的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上,代碼運行效果以下:

相關文章
相關標籤/搜索