Socket粘包,封包,拆包

粘包、拆包發生原因

發生TCP粘包或拆包有很多原因,現列出常見的幾點,可能不全面,歡迎補充,

1、要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。

2、待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。

3、要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包。(服務端出現粘包)

4、接收數據端的應用層沒有及時讀取接收緩衝區中的數據,造成一次性接收多個包,出現粘包 (接收端出現粘包)

 

什麼是粘包

TCP有粘包現象,而UDP不會出現粘包。

  • TCP(Transport Control Protocol,傳輸控制協議)是面向連接的,面向流的。TCP的收發兩端都要有成對的Socket,因此,發送端爲了將更多有效的包發送出去,採用了合併優化算法(Nagle算法),將多次、間隔時間短、數據量小的數據合併爲一個大的數據塊,進行封包處理。這樣的包對於接收端來說,就沒辦法分辨,所以需要一些特殊的拆包機制。
  • UDP(User Datagram Protocol,用戶數據報協議)是無連接的,面向消息的提供高效率服務。不會使用合併優化算法。UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。

舉個例子
我們連續發送三個數據包,大小分別是1k,2k ,4k,這三個數據包,都已經到達了接收端的網絡堆棧中,如果使用UDP協議,不管我們使用多大的接收緩衝區去接收數據,我們必須有三次接收動作,才能夠把所有的數據包接收完.而使用TCP協議,我們只要把接收的緩衝區大小設置在7k以上,我們就能夠一次把所有的數據包接收下來,只需要有一次接收動作。

如何處理粘包

  1. 提前通知接收端要傳送的包的長度
    粘包問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據。

不建議使用,因爲程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這樣會放大網絡延遲帶來的性能損耗   

 

      2.加分割標識符
{數據段01}+標識符+{數據段02}+標識符

1⃣️將發送的每條消息的首尾都加上特殊標記符,前加"<"  後加">"。這裏我採取的是先將要發送的所有消息,首尾加上特殊標記後,都先放在一個字符串string中,然後一次性的發送給接收方,接受之後,再根據標記符< >,將一條條消息擇(zhái)出來。(這種方法只適合數據量較小的情況)

2⃣️發送端和接收端約定好一個標識符來區分不同的數據包,如果接收到了這麼一個分隔符,就表示一個完整的包接收完畢。

也不建議使用,因爲要發送的數據很多,數據的內容格式也有很多,可能會出現標識符不唯一的情況

 

     3.自定義包頭(建議使用)

 

在開始傳輸數據時,在包頭拼上自定義的一些信息,比如前4個字節表示包的長度,5-8個字節表示傳輸的類型(Type:做一些業務區分),後面爲實際的數據包。(這種方法就是所謂的自定義協議,這種方法是最常用的)

這樣接收方就可以根據接收到的消息長度來動態定義緩衝區的大小。

 

二、Socket的封包、拆包

1、爲什麼基於TCP的通信程序需要封包、拆包?
答:TCP是流協議,所謂流,就是沒有界限的一串數據。但是程序中卻有多種不同的數據包,那就很可能會出現如上所說的粘包問題,所以就需要在發送端封包,在接收端拆包。

 

2、那麼如何封包、拆包?
答:封包就是給一段數據加上包頭或者包尾。比如說我們上面爲解決粘包所使用的兩種方法,其實就是封包與拆包的具體實現。


封包:
封包就是給一段數據加上包頭,這樣一來數據包就分爲包頭和包體兩部分內容了.包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,這是個很重要的變量,其他的結構體成員可根據需要自己定義(如傳輸的類型).根據包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數據包.

 

 

 對於拆包目前我最常用的是以下兩種方式.
    第1種拆包方式:動態緩衝區暫存方式.之所以說緩衝區是動態的是因爲當需要緩衝的數據長度超出緩衝區的長度時會增大緩衝區長度.
    大概過程描述如下:
    A,爲每一個連接動態分配一個緩衝區,同時把此緩衝區和SOCKET關聯,常用的是通過結構體關聯.
    B,當接收到數據時首先把此段數據存放在緩衝區中.
    C,判斷緩存區中的數據長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
    D,根據包頭數據解析出裏面代表包體長度的變量.
    E,判斷緩存區中除包頭外的數據長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
    F,取出整個數據包.這裏的"取"的意思是不光從緩衝區中拷貝出數據包,而且要把此數據包從緩存區中刪除掉.刪除的辦法就是把此包後面的數據移動到緩衝區的起始地址.

    這種方法有兩個缺點.1.爲每個連接動態分配一個緩衝區增大了內存的使用.2.有三個地方需要拷貝數據,一個地方是把數據存放在緩衝區,一個地方是把完整的數據包從緩衝區取出來,一個地方是把數據包從緩衝區中刪除.這種拆包的改進方法會解決和完善部分缺點.

 

採用環形緩衝.但是這種改進方法還是不能解決第一個缺點以及第一個數據拷貝,只能解決第三個地方的數據拷貝(這個地方是拷貝數據最多的地方).

環形緩衝實現方案是定義兩個指針,分別指向有效數據的頭和尾.在存放數據和刪除數據時只是進行頭尾指針的移動

第2種拆包方式會解決這兩個問題.

第2種拆包方式:利用底層的緩衝區來進行拆包
   由於TCP也維護了一個緩衝區,所以我們完全可以利用TCP的緩衝區來緩存我們的數據,這樣一來就不需要爲每一個連接分配一個緩衝區了.另一方面我們知道recv或者wsarecv都有一個參數,用來表示我們要接收多長長度的數據.利用這兩個條件我們就可以對第一種方法進行優化了.
   對於阻塞SOCKET來說,我們可以利用一個循環來接收包頭長度的數據,然後解析出代表包體長度的那個變量,再用一個循環來接收包體長度的數據.

對於非阻塞的SOCKET,比如完成端口,我們可以提交接收包頭長度的數據的請求,當GetQueuedCompletionStatus返回時,我們判斷接收的數據長度是否等於包頭長度,若等於,則提交接收包體長度的數據的請求,若不等於則提交接收剩餘數據的請求.當接收包體時,採用類似的方法.

 

 

 

三:如何判斷包的合法性.
判斷包的合法性可以結合下面兩種方式來判斷.但是想100%的判定出非法包,只能通過信息安全中的知識來判定了,對這種方法這裏不做闡述.
1.通過包頭的結構來判斷包的合法性.
最初的時候我是根據包頭來判斷包的合法性,比如判斷Command是否超出命令範圍,nDataLen是否大於最大包的長度.但是這種方法無法過濾掉非法包,當出現非法包時我們唯一能做的就是斷開連接,或許這也是最好的處理辦法.
我們可以給一個完整的包加上開始和結束標誌,標誌可以是個整數,也可以是一串字符串.以第一種拆包方式爲例來說明.當要拆一個完整包時我們先從緩衝區有效數據頭指針地址搜索包的開始標誌,搜索到後並且當前數據夠一個包頭數據,則判斷開始標誌和包頭是否合法,若合法則根據代表數據長度的變量的值定位到包尾,判斷包尾標誌是否與我們定義的一致,若一致則這個包是合法的包.若有一項不一致則繼續尋找下個包的開始標誌,並把下個合法包的前面的數據全部捨棄.
2.通過邏輯層來判斷包的合法性.
當取出一個合法的包時,我們還要根據當前數據處理的邏輯來判斷包的合法性.比如說在登陸成功後的某段時間服務器又收到了同一個客戶端的登陸包,那我們就可以判斷這個包是非法的,簡單處理就是斷開連接.

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