Java NIO之tcp粘包拆包

一 ByteToMessageDecoder
1.1 實例
ByteToMessageDecoder,用於把一個byte流轉換成一個對象,實例:

public class StringDecoder extends ByteToMessageDecoder {
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, 
List<Object> out) throws Exception {        byte[] bytes = new byte[in.readableBytes()];
        in.readBytes(bytes);
        out.add(new String(bytes));
}
}

它有一個抽象方法decode,我們實現了這個方法,這個方法的第三個參數是一個List,所有加入這個List的對象都會被逐一的調用fireChannelRead方法映射事件。
使用方法:ByteToMessageDecoder其實就是一個ChannelInboundHandler,直接加入到Pipeline即可:

        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                     protected void initChannel(SocketChannel socketChannel) throws Exception {
                         socketChannel.pipeline().addLast(new StringDecoder());
                        //...
                    }
                    });

這樣,ByteBuf數據到達這個Handler之後,會被轉成String,然後繼續傳遞數據。
1.2 實現
ByteToMessage Decoder是個抽象類,它繼承了ChannelInboundHandler,做了以下邏輯:
1)重寫父類的channelRead方法,在這個方法中,把ByteBuf數據交給子類decode方法處理,decode的方法定義如下:

protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

它的第二個參數爲需要處理的ByteBuf,第三個參數爲一個List,用於記錄處理後的數據。
2)子類decode方法返回會,它會遍歷參數中的List,把裏面的對象依次取出來調用fireChannelRead方法傳遞事件。
3)如果decode方法沒有把ByteBuf讀取完,則會記錄這次的ByteBuf對象,然後下一次處理消息時,會把下一次的ByteBuf和這次的ByteBuf合併,然後再交給子類decode處理。
關於第三點,這個邏輯的目的是爲了方便處理TCP的粘包和拆包
1.3 源碼
源碼從ByteToMessageDecoder的channelRead方法開始
步驟一:把當前的ByteBuf與上次未處理的ByteBuf合併:

ByteBuf data =(ByteBuf)msg;
first = curdlation ==null;
if (first){
   cumulation = data;
}else{
    cumulation = cumulator.cumulate(ctx.alloc(),cumulation,data);
}

例如:上次未處理的ByteBuf是[1,2,3,4,5,0,0],這一次的ByteBuf是[6,7,8],處理完之後結果是[1,2,3,4,5,6,7,8]
注:可以會有一個疑問是:爲什麼ByteBuf會擴容成了8,而不是64?因爲這裏沒有使用ByteBuf的擴容邏輯,而是自己實現了一套。
步驟二:處理子類decode後添加到List中的數據:

    int outSize = out.size();
                if (outSize > 0) {
                    fireChannelRead(ctx, out, outSize);
                    out.clear();
  }

爲啥還沒調用子類的decode方法就要處理List了?因爲這個邏輯是在循環裏的,簡化代碼表示即:

while (in.isReadable()) { 
             fireChannelRead(ctx, out, outSize);
             decode(ctx, in, out);
}

所以每次循環時處理的都是上一次循環後子類添加到List中的數據。
步驟三:處理子類decode後的ByteBuf

 //這個if條件能進去,說明已經讀完了
                if (cumulation != null && !cumulation.isReadable()) {
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                } else if (++ numReads >= discardAfterReads) {
                    numReads = 0;
                    discardSomeReadBytes();
                }

這一步還有一個邏輯:如果連續16次都沒處理ByteBuf,則會把ByteBuf中的數據壓縮一次。
順便給大家推薦一個Java技術交流羣:473984645裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!
二 .tcp 拆包粘包問題
2.1 問題描述
tcp粘包,即tcp在發送數據時,可能會把兩個tcp包合併成一個發送
tcp拆包,即tcp在發送數據時,可能會把一個tcp包拆成多個來發送
例如:客戶端分兩次給服務端發送了兩個消息"ABCD" 和 “EFG”
1)服務端可能收到三個數據包,分別是"AB", “CD”, “EFG”,即第一個數據包被拆包成了兩個
2)服務端可能只會收到一個數據包:“ABCDEFG”,即兩個數據包被合併成了一個包
3)服務端甚至可能會收到"ABC", “DEFG”,會拆包再粘包
2.2 產生的原因
分爲以下三個原因:
1)socket緩衝區造成的粘包:
每個socket都有一個發送緩存區與接收緩衝區,客戶端向服務端寫數據時,實際上是寫到了服務端socket的接收緩衝區中。
服務端調用read方法時,其實只是把接收緩衝區的內容讀取到內存中了。因此,服務端調用read方法時,可能客戶端已經寫了兩個包到接收緩衝區中了,因此read到的數據其實是兩個包粘包後的數據。
2)MSS/MTU限制導致的拆包
MSS是指TCP每次發送數據允許的最大長度,一般是1500字節,如果某個數據包超過了這個長度,就要分多次發送,這就是拆包。
3)Nagle算法導致的粘包
網絡數據包都是要帶有數據頭部的,通常是40字節,假如我們發送一個字節的數據,也要加上這40個字節的頭部再發送,顯然這樣是非常不划算的。
所以tcp希望儘可能的一次發送大塊的數據包,Nagle算法就是做這個事的,它會收集多個小數據包,合併爲一個大數據包後再發送,這就是粘包。
2.3 解決辦法
通常,解決tcp粘包拆包問題,是通過定義通信協議來實現的:
定長協議
即規定每個數據包的長度,假如我們規定每個數據包的長度爲3,假如服務端收到客戶端的數據爲:“ABCD”, “EF”,那麼也可以解析出實際的數據包爲"ABC", “DEF”。
特殊分隔符協議
即規定每個數據包以什麼樣的字符結尾,如規定以"ABCD符號結尾,假如服務端收到的數據包爲:"ABCDEF", “G$”,那麼可以解析出實際數據包爲:“ABCD”, “EFG”。這種方式要確保消息體中可能會出現分隔符的情況。
長度編碼協議
即把消息分爲消息頭和消息體,在消息頭中包含消息的長度關於tcp粘包拆包的內容,這有篇文章講得非常好,強推:TCP粘包、拆包與通信協議
三 Netty中解決tcp粘包拆包問題的方法
3.1 自定義一個tcp粘包拆包處理器
基於ByteToMessageDecoder,我們可以很容易的實現處理tcp粘包拆包問題的Handler,以定長協議爲例,我們來實現一個定長協議的tcp粘包拆包處理器:

public class LengthDecoder extends ByteToMessageDecoder {
    private int length;

    public LengthDecoder(int length) {
        this.length = length;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        while (in.readableBytes() >= length) {
            byte[] buff = new byte[length];
            in.readBytes(buff);
            out.add(new String(buff));
            }
        }
    }
   

在decode方法中,我們循環判斷,如果ByteBuf中未讀的數據量大於指定的長度length,我們就讀到lenght個數據,然後轉成字符串加入到List中。
後續ByteToMessageDecoder依次把List中的數據fireChannelRead傳遞事件。
如果ByteBuf中的未讀數據不夠length,說明發生了拆包,後續還有數據,這裏直接不處理即可,ByteToMessageDecoder會幫我們記住這次的ByteBuf,下一次數據來了之後,會跟這次的數據合併後再處理。
3.2 Netty中自帶的tcp粘包拆包處理器
Netty中實現了很多種粘包拆包處理器:
1)FixedLengthFrameDecoder:與我們上面自定義的一樣,定長協議處理器
2)DelimiterBasedFrameDecoder:特殊分隔符協議的處理器
3)LineBasedFrameDecoder:特殊分隔符協議處理器的一種特殊情況,行分隔符協議處理器。
4)JsonObjectDecoder:json協議格式處理器
5)HttpRequestDecoder:http請求體協議處理器
6)HttpResponseDecoder:http響應體處理器,很明顯這個是用於客戶端的
文章較長,感謝您的閱讀。對文章如有疑問,歡迎提出。望分享的內容對大家有所幫助。蒐集整理了一些Java資料,包括Java進階學習路線以及對應學習資料,還有一些大廠面試題,需要的朋友可以自行領取:Java高級架構學習資料分享+架構師成長之路
順便給大家推薦一個Java技術交流羣:473984645裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!

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