遊戲服務器中對於發包/收包的個人理解

TCP

  • 發包: tcp有自動重傳機制,所以一般的包體結構基本是
包體長度 包體數據

······很簡單明瞭,也是我們初學網絡編程是所用的結構。那麼思考一下,我們發包需要什麼信息呢?
其實我們只需兩個信息,已發送長度數據包大小。那麼結構就變成了

包體信息(已發送長度 + 數據包大小) 包體數據(包長 + 數據)

······有些人可能這裏就疑惑了,不是包體數據裏面有個包長數據嗎?爲什麼還要多一個數據包大小,呵呵,不要着急。我們接下來慢慢說

  • 包體數據內結構
    ······我在網上看到很多人都是用 包頭標識符 + 包長 + 數據 + 包尾標識符 包頭標識符 + 包長 + 數據或者包長 + 數據 + 包尾標識符 這樣的結構來處理數據包的。包括我身邊朋友實際項目中也是用的 包長 + 數據 + 包尾標識符這樣的結構。
    ······但是這樣會有個問題,那就是如果發生的斷包的情況(極少發生,可能發生的情況有:惡意改包,截斷數據包。或者對端的代碼邏輯錯誤,或者其他不知名的情況下),我們收到了一個不完整包,但之後的包都是正確的包,裏面有包長信息和 部分數據,假設包長爲16字節,我們收到包長2字節 + 部分數據 4字節,這時候後面發送的包來了,這時候底層就會把前面的不完整包和後面發來的完整包進行拼接,會導致後面的數據一直是混亂的。
    ······這時候有人出來說了。我們不是有包頭包尾標識嗎?這樣就可以判斷是不是有效包啊。那我們來看看。如果是包頭標識符 + 包長 + 數據這樣的結構。假設這時候我們收到了一個被截斷的包,裏面有5個字節。1個字節表示包頭標識,2個字節表示包長(假設16字節),剩下的2個字節表示數據。然後這時候後面的正常包數據來了。但是這個包只有8個字節,包頭標識和包長佔3個,剩下5個字節全是數據,假設這個8字節的包就是一個完整包的情況下,那我們又經過了一輪接收,這時候終於接收到了前面那個截斷包(包長 + 1)的長度(爲何要 + 1?因爲我們需要後面一個包的包頭標識符驗證是否是截斷包,如果後面的包過很久才發也是一個問題)。這時候我們檢查到 package[pack_len + 1] 的位置卻不是本該在那兒的包頭標識符,這時候我們才發現之前的數據裏包含了截斷包,只能把它丟棄。裏面還包括了一個完好的包。。。
    包長 + 數據 + 包尾標識符的結構異同。就不多講了。
    可能是沒有想過或者也是不在乎那幾個數據包吧。我可能有點強迫症。。說多了

我自己定的包體結構是這樣的

namespace network::tcp {
#pragma pack(push)
#pragma pack(1)
	/*
	包體信息
	存儲結構:包體信息 + 報文結構
	報文結構: 包頭結構 + 包體 + 包尾結構
	*/
	struct pack_info {
		slen_t	slen;	//發送長度
		slen_t	len;	//包長
	};

	/*
	包頭結構
	報文結構: 包頭結構 + 包體 + 包尾結構
	*/
	struct head_pack {
		char	head;	//包頭標識符
		slen_t	len;	//包長
	};

	/*
	包尾結構
	報文結構: 包頭結構 + 包體 + 包尾結構
	*/
	struct tail_pack {
		slen_t	len;	//包長
		char	tail;	//包尾標識符
	};
#pragma pack(pop)
}

至於爲何包尾也要加個包長,還有爲什麼需要頭尾兩端的標識符,大家思考一下?

  • 合包機制
    爲了減少服務器壓力,通常的做法是將小包合成爲大包,本來需要發送幾次才能發送完成的數據只需要一次就能完成,這樣可以減少部分服務器的壓力,但是合成的大包不能超過MTU(最大傳輸單元),那麼MTU到底有多大呢?
    其實根據寬帶連接方式的不同,MTU可能不盡相同,如下所示:
    (1). PPPoE/ADSL: 1360-1492
    (2). PPTP VPN: 1400-1460
    (3). L2TP VPN: 1400-1460
    (4). Fixed IP: 1400-1500
    (5). DHCP: 1400-1492
    保險起見,這裏我自己定的合包大小爲1024字節。那麼我們合包之後結構就變成了這樣
包體信息(已發送長度 + 數據包大小) 包體數據([包頭+ 包體 + 包尾] + [包頭+ 包體 + 包尾] + …)

怎麼樣,這下就知道我們最開始爲什麼需要包體信息中的數據包大小了吧,因爲它是代表整個數據包大小的數據,而不是單個數據包。雖然單個數據包的包頭信息中包含了包長數據。但是它只代表了自身的大小。

  • 發送優先級
    在遊戲中,遊戲數據包會有優先級劃分,比如自己的移動數據優先級 > 其他玩家移動數據的優先級,其實這個優先級發送機制很多遊戲都有(難怪玩擼啊擼的時候卡了都是自己卡,看隊友一點都不卡,可能就用了發送優先級吧)。那麼我們爲了能讓先發送的包優先級更大。我的方法是使用最大堆存儲消息隊列(deque),我們插入發送數據需要三種信息:
    1. 優先級;
    2. 發送數據;
    3. 發送長度;

······我們就是根據優先級來獲取最大堆中指定的優先級消息隊列,如果沒有則創建一個新的消息隊列並加入最大堆。再向指定的優先級消息隊列中添加我們插入的發送數據。等到發送的時候再取出使用,發送完成後再刪除消息隊列中的發送數據。如果消息隊列爲空了,那麼就把它從最大堆中移除。這樣就能保證最大堆的頂部一直都是消息優先級最高的數據。

  • 對於環形緩衝區
    我個人是對其產生了不少的疑問,所以並沒有在項目使用,我用的是自己寫的不定長內存池來申請發送數據使用的內存。申請內存的時候,可以直接申請sizeof(包體信息) + sizeof(包頭) + 數據長度 + sizeof(包尾)大小的內存,設置數據的時候直接使用指針指向對應的位置就好了。上面的#pragma pack也是爲了我們能利用好每一個字節的空間;

udp

還沒開始寫呢。彆着急,等個十年八年就出來了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章