TCP粘包解決方案 - 環形緩衝區

粘包產生原因:
先說TCP:由於TCP協議本身的機制(面向連接的可靠地協議-三次握手機制)客戶端與服務器會維持一個連接(Channel),數據在連接不斷開的情況下,可以持續不斷地將多個數據包發往服務器,但是如果發送的網絡數據包太小,那麼他本身會啓用Nagle算法(可配置是否啓用)對較小的數據包進行合併(基於此,TCP的網絡延遲要UDP的高些)然後再發送(超時或者包大小足夠)。那麼這樣的話,服務器在接收到消息(數據流)的時候就無法區分哪些數據包是客戶端自己分開發送的,這樣產生了粘包;服務器在接收到數據庫後,放到緩衝區中,如果消息沒有被及時從緩存區取走,下次在取數據的時候可能就會出現一次取出多個數據包的情況,造成粘包現象(確切來講,對於基於TCP協議的應用,不應用包來描述,而應 用 流來描述),個人認爲服務器接收端產生的粘包應該與linux內核處理socket的方式 select輪詢機制的線性掃描頻度無關。
再說UDP:本身作爲無連接的不可靠的傳輸協議(適合頻繁發送較小的數據包),他不會對數據包進行合併發送(也就沒有Nagle算法之說了),他直接是一端發送什麼數據,直接就發出去了,既然他不會對數據合併,每一個數據包都是完整的(數據+UDP頭+IP頭等等發一次數據封裝一次)也就沒有粘包一說了。

分包產生的原因就簡單的多:可能是IP分片傳輸導致的,也可能是傳輸過程中丟失部分包導致出現的半包,還有可能就是一個包可能被分成了兩次傳輸,在取數據的時候,先取到了一部分(還可能與接收的緩衝區大小有關係),總之就是一個數據包被分成了多次接收。

解決辦法:

粘包與分包的處理方法:

我根據現有的一些開源資料做了如下總結(常用的解決方案):
一個是採用分隔符的方式,即我們在封裝要傳輸的數據包的時候,採用固定的符號作爲結尾符(數據中不能含結尾符),這樣我們接收到數據後,如果出現結尾標識,即人爲的將粘包分開,如果一個包中沒有出現結尾符,認爲出現了分包,則等待下個包中出現後 組合成一個完整的數據包,這種方式適合於文本傳輸的數據,如採用/r/n之類的分隔符;

另一種是採用在數據包中添加長度的方式,即在數據包中的固定位置封裝數據包的長度信息(或可計算數據包總長度的信息),服務器接收到數據後,先是解析包長度,然後根據包長度截取數據包(此種方式常出現於自定義協議中),但是有個小問題就是如果客戶端第一個數據包數據長度封裝的有錯誤,那麼很可能就會導致後面接收到的所有數據包都解析出錯(由於TCP建立連接後流式傳輸機制),只有客戶端關閉連接後重新打開纔可以消除此問題,我在處理這個問題的時候對數據長度做了校驗,會適時的對接收到的有問題的包進行人爲的丟棄處理(客戶端有自動重發機制,故而在應用層不會導致數據的不完整性);

另一種不建議的方式是TCP採用短連接處理粘包(這個得根據需要來,所以不建議);
/***********************************************************************************************************************************************/

TCP粘包處理-RingBuf方法

TCP粘包是指發送方發送的若干包數據到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。粘包可能由發送方造成,也可能由接收方造成。TCP爲提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一包數據,造成多個數據包的粘連。如果接收進程不及時接收數據,已收到的數據就放在系統接收緩衝區,用戶進程讀取數據時就可能同時讀到多個數據包。因爲系統傳輸的數據是帶結構的數據,需要做分包處理。

爲了適應高速複雜網絡條件,我們設計實現了粘包處理模塊,由接收方通過預處理過程,對接收到的數據包進行預處理,將粘連的包分開。爲了方便粘包處理,提高處理效率,在接收環節使用了環形緩衝區來存儲接收到的數據。其結構如表1所示。

                                                            1 環形緩衝結構

字段名

類型

含義

CS

CRITICAL_SECTION

保護環形緩衝的臨界區

pRingBuf

UINT8*

緩衝區起始位置

pRead

UINT8*

當前未處理數據的起始位置

pWrite

UINT8*

當前未處理數據的結束位置

pLastWrite

UINT8*

當前緩衝區的結束位置

環形緩衝跟每個TCP套接字綁定。在每個TCPSOCKET_OBJ創建時,同時創建一個PRINGBUFFER結構並初始化。這時候,pRingBuf指向環形緩衝區的內存首地址,pReadpWrite指針也指向它。pLastWrite指針在這時候沒有實際意義。初始化之後的結構如圖1所示。


1初始化後的環形緩衝區

在每次投遞一個TCP的接收操作時,從RINGBUFFER獲取內存作接收緩衝區,一般規定一個最大值L1作爲可以寫入的最大數據量。這時把pWrite的值賦給BUFFER_OBJbuf字段,把L1賦給bufLen字段。這樣每次接收到的數據就從pWrite開始寫入緩衝區,最多寫入L1字節,如圖 2


2分配緩衝後的環形緩衝

如果某次分配過程中,pWrite到緩衝區結束的位置pEnd長度不夠最小分配長度L1,爲了提高接收效率,直接廢棄最後一段內存,標記pLastWritepWrite。然後從pRingBuf開始分配內存,如圖 3


3 使用到結尾的環形緩衝

特殊情況下,如果處理包速度太慢,或者接收太快,可能導致未處理包占用大部分緩衝區,沒有足夠的緩衝區分配給新的接收操作,如圖4。這時候直接報告錯誤即可。


4沒有足夠接收緩衝的環形緩衝

當收到一個長度爲L數據包時,需要修改緩衝區的指針。這時候已經寫入數據的位置變爲(pWrite+L),如圖 5


5收到長度爲L的數據的環形緩衝

分析上述環形緩衝的使用過程,收到數據後的情況可以簡單歸納爲兩種:pWrite>pRead,接收但未處理的數據位於pReadpWrite之間的緩衝區;pWrite<pRead,這時候,數據位於pReadpLastWritepRingbufpWrite之間。這兩種情況分別對應圖6、圖 7

首先分析圖6。此時,pRead是一個包的起始位置,如果L1足夠一個包頭長度,就獲取該包的長度信息,記爲L。假如L1>L,就說明一個數據包接收完成,根據包類型處理包,然後修改pRead指針,指向下一個包的起始位置(pRead+L)。這時候仍然類似於之前的狀態,於是解包繼續,直到L1不足一個包的長度,或者不足包頭長度。這時退出解包過程,等待後續的數據到來。


6有未處理數據的環形緩衝(1


7有未處理數據的環形緩衝(2

8稍微複雜。首先按照上述過程處理L1部分。存在一種情況,經過若干個包處理之後,L1不足一個包,或者不足一個包頭。如果這時(L1+L2)足夠一個包的長度,就需要繼續處理。另外申請一個最大包長度的內存區pTemp,把L1部分和L2的一部分複製到pTemp,然後執行解包過程。

經過上述解包之後,pRead就轉向pRingBufpWrite之間的某個位置,從而回歸情況圖 6,繼續按照圖 6部分執行解包。

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