socket編程 TCP 粘包和半包 的問題及解決辦法


一般在socket處理大數據量傳輸的時候會產生粘包和半包問題,有的時候tcp為了提高效率會緩沖N個包后再一起發出去,這個與緩存和網絡有關系。

粘包 為x.5個包

半包 為0.5個包

由於網絡原因 一次可能會來 0.5/1 /2/ 2.5/ 。。。。個包

當接收到時 要先看看那這個包中有多少個完整的包。把完整的包都處理了 也就是說把x都處理了。剩下的0.5留在接收區中,等待下次接收。

這回接收到的就是0.5+1.5/0.5+1.3/0.5+0.5..... 把完整的包都處理了,有殘缺的扔掉 0.8的。

一般情況 接收到正確的后都要給發送端一個應答。不給應答的算超時,發送端將重發。

有頭沒尾的不能扔

沒頭有尾的可以扔

有頭有尾但缺東西可以扔

有頭有尾不缺東西不能扔

 

之所以出現粘包和半包現象,是因為TCP當中,只有流的概念,沒有包的概念.

可以使用UDP協議.這樣可以就可以區分每個包了.但是要確保包的丟失處理.為了提到效率,可以考慮寫一個滑動窗口進行收發包.

若采用TCP協議進行傳輸,就要將每個包區分開來.可以有三種方式.因為TCP是面向流的.流只有打開和關閉,你要用一個流傳輸多個包,那就要向辦法區分出每個包.

一:: 可以每次發送同樣大小的包,過大的包不予發送,過小的包,后面部分用固定的字符'\0'進行填充.

二:: 將流按字符處理,抽出一個字符做轉義字符(通常Java用'\'來做轉義字符,比如"\n"表示換行).假如就設'\'為轉義字符,發送方如果流當中出現'\',就在后面在追加一個'\',如果包結束,則用'\'做包的結束符.這樣,在接收方,若讀取一個單獨的'\'或者流結束,就標示前面的內容構成一個包,如果連續讀取兩個'\',就將兩個'\'用一個'\'進行替換.這樣,就可以保證原來包中的信息不變,同時也能區分出每個包了.

三:: 在發送方發送一個包的時候,先將這個包的長度發送給對方(一般是4個字節表示包長),然后再將包的內容發送過去.接收方先接收4個字節,看看包的長度,然后按照長度來接收包,這樣就不會出錯了. 以上三種方法,是網絡傳輸中經常用到的方法.后兩種很常見.最后一種,在TCP長連接傳輸中應用最多. 綜合以上的說法,就是要在TCP協議以上再封裝一層協議,用來做分包的信息交換. 

 

一般處理是: 一個BUFFER,用於保存當前連接的讀緩存

有數據時,Buffer = Buffer + DataIn,不停的接收

收完成后,開始解析Buffer,

根據包的協議,不停的解析Buffer,並形成一個個包進行處理,處理后,Buffer = Buffer - Data,並繼續解包。

 

 

 

TCP粘包,拆包及解決方法

粘包拆包問題是處於網絡比較底層的問題,在數據鏈路層、網絡層以及傳輸層都有可能發生。我們日常的網絡應用開發大都在傳輸層進行,由於UDP有消息保護邊界,不會發生粘包拆包問題,因此粘包拆包問題只發生在TCP協議中。

 

什么是粘包、拆包?

假設客戶端向服務端連續發送了兩個數據包,用packet1和packet2來表示,那么服務端收到的數據可以分為三種,現列舉如下:

第一種情況,接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象,此種情況不在本文的討論范圍內。

 

第二種情況,接收端只收到一個數據包,由於TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即為粘包。這種情況由於接收端不知道這兩個數據包的界限,所以對於接收端來說很難處理。

 

第三種情況,這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要么是不完整的,要么就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對於接收端同樣是不好處理的。

 

 

 

為什么會發生TCP粘包、拆包?

發生TCP粘包、拆包主要是由於下面一些原因:

 

1. 應用程序寫入的數據大於套接字緩沖區大小,這將會發生拆包。

 

2.應用程序寫入數據小於套接字緩沖區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘包。

 

3.進行MSS(最大報文長度)大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發生拆包。

 

4.接收方法不及時讀取套接字緩沖區數據,這將發生粘包。

 

 

 

粘包、拆包解決辦法

TCP本身是面向流的,作為網絡服務器,如何從這源源不斷涌來的數據流中拆分出或者合並出有意義的信息呢?通常會有以下一些常用的方法:

1、發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據后,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。

2、發送端將每個數據包封裝為固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩沖區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。

3、可以在數據包之間設置邊界,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數據包拆分開。

 

 

如何基於Netty處理粘包、拆包問題?

涉及到相關重要組件:

  1. ByteToMessageDecoder
  2. MessageToMessageDecoder

這兩個組件都實現了ChannelInboundHandler接口,這說明這兩個組件都是用來解碼網絡上過來的數據的。而他們的順序一般是ByteToMessageDecoder位於head channel handler的后面,MessageToMessageDecoder位於ByteToMessageDecoder的后面。Netty中,涉及到粘包、拆包的邏輯主要在ByteToMessageDecoder及其實現中。

ByteToMessageDecoder

顧名思義、ByteToMessageDecoder是用來將從網絡緩沖區讀取的字節轉換成有意義的消息對象的,對於源碼層面指的說明的一段是下面這部分:

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        while (in.isReadable()) {
            int outSize = out.size();
 
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();
                
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }
 
            int oldInputLength = in.readableBytes();
            decode(ctx, inout);
 
            if (ctx.isRemoved()) {
                break;
            }
 
            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }
 
            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                        StringUtil.simpleClassName(getClass()) +
                        ".decode() did not read anything but decoded a message.");
            }
 
            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

為了節省篇幅,我把注釋刪除掉了,當上面一個channel handler傳入的ByteBuf有數據的時候,這里我們可以把in參數看成網絡流,這里有不斷的數據流入,而我們要做的就是從這個byte流中分離出message,然后把message添加給out。分開將一下代碼邏輯:

  1. 當out中有Message的時候,直接將out中的內容交給后面的channel handler去處理。
  2. 當用戶邏輯把當前channel handler移除的時候,立即停止對網絡數據的處理。
  3. 記錄當前in中可讀字節數。
  4. decode是抽象方法,交給子類具體實現。
  5. 同樣判斷當前channel handler移除的時候,立即停止對網絡數據的處理。
  6. 如果子類實現沒有分理出任何message的時候,且子類實現也沒有動bytebuf中的數據的時候,這里直接跳出,等待后續有數據來了再進行處理。
  7. 如果子類實現沒有分理出任何message的時候,且子類實現動了bytebuf中的數據,則繼續循環,直到解析出message或者不在對bytebuf中數據進行處理為止。
  8. 如果子類實現解析出了message但是又沒有動bytebuf中的數據,那么是有問題的,拋出異常。
  9. 如果標志位只解碼一次,則退出。

可以知道,如果要實現具有處理粘包、拆包功能的子類,及decode實現,必須要遵守上面的規則,我們以實現處理第一部分的第二種粘包情況和第三種情況拆包情況的服務器邏輯來舉例:

對於粘包情況的decode需要實現的邏輯對應於將客戶端發送的兩條消息都解析出來分為兩個message加入out,這樣的話callDecode只需要調用一次decode即可。

對於拆包情況的decode需要實現的邏輯主要對應於處理第一個數據包的時候第一次調用decode的時候out的size不變,從continue跳出並且由於不滿足繼續可讀而退出循環,處理第二個數據包的時候,對於decode的調用將會產生兩個message放入out,其中兩次進入callDecode上下文中的數據流將會合並為一個bytebuf和當前channel handler實例關聯,兩次處理完畢即清空這個bytebuf。

當然,盡管介紹了ByteToMessageDecoder,用戶自己去實現處理粘包、拆包的邏輯還是有一定難度的,Netty已經提供了一些基於不同處理粘包、拆包規則的實現:如DelimiterBasedFrameDecoder、FixedLengthFrameDecoder、LengthFieldBasedFrameDecoder和LineBasedFrameDecoder等等。其中:

DelimiterBasedFrameDecoder是基於消息邊界方式進行粘包拆包處理的。

FixedLengthFrameDecoder是基於固定長度消息進行粘包拆包處理的。

LengthFieldBasedFrameDecoder是基於消息頭指定消息長度進行粘包拆包處理的。

LineBasedFrameDecoder是基於行來進行消息粘包拆包處理的。

用戶可以自行選擇規則然后使用Netty提供的對應的Decoder來進行具有粘包、拆包處理功能的網絡應用開發。

 

最后

在通常的高性能網絡應用中,客戶端通常以長連接的方式和服務端相連,因為每次建立網絡連接是一個很耗時的操作。比如在RPC調用中,如果一個客戶端遠程調用的過程中,連續發起了多次調用,而如果這些調用對應於同一個連接的時候,那么就會出現服務器需要對於這些多次調用消息的粘包拆包問題的處理。如果是你,你會選擇哪種策略呢?

 


免責聲明!

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



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