深入了解Netty【八】TCP拆包、粘包和解決方案


1、TCP協(xié)議傳輸過程

TCP協(xié)議是面向流的協(xié)議,是流式的,沒有業(yè)務(wù)上的分段,只會(huì)根據(jù)當(dāng)前套接字緩沖區(qū)的情況進(jìn)行拆包或者粘包:


發(fā)送端的字節(jié)流都會(huì)先傳入緩沖區(qū),再通過網(wǎng)絡(luò)傳入到接收端的緩沖區(qū)中,最終由接收端獲取。

2、TCP粘包和拆包概念

因?yàn)門CP會(huì)根據(jù)緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分,在業(yè)務(wù)上認(rèn)為,有的包被拆分成多個(gè)包進(jìn)行發(fā)送,也可能多個(gè)曉小的包封裝成一個(gè)大的包發(fā)送,這就是TCP的粘包或者拆包。

3、TCP粘包和拆包圖解

假設(shè)客戶端分別發(fā)送了兩個(gè)數(shù)據(jù)包D1和D2給服務(wù)端,由于服務(wù)端一次讀取到字節(jié)數(shù)是不確定的,故可能存在以下幾種情況:

  1. 服務(wù)端分兩次讀取到兩個(gè)獨(dú)立的數(shù)據(jù)包,分別是D1和D2,沒有粘包和拆包。
  2. 服務(wù)端一次接收到了兩個(gè)數(shù)據(jù)包,D1和D2粘在一起,發(fā)生粘包。
  3. 服務(wù)端分兩次讀取到數(shù)據(jù)包,第一次讀取到了完整的D1包和D2包的部分內(nèi)容,第二次讀取到了D2包的剩余內(nèi)容,發(fā)生拆包。
  4. 服務(wù)端分兩次讀取到數(shù)據(jù)包,第一次讀取到部分D1包,第二次讀取到剩余的D1包和全部的D2包。

當(dāng)TCP緩存再小一點(diǎn)的話,會(huì)把D1和D2分別拆成多個(gè)包發(fā)送。

4、TCP粘包和拆包解決策略

因?yàn)門CP只負(fù)責(zé)數(shù)據(jù)發(fā)送,并不處理業(yè)務(wù)上的數(shù)據(jù),所以只能在上層應(yīng)用協(xié)議棧解決,目前的解決方案歸納:

  1. 消息定長,每個(gè)報(bào)文的大小固定,如果數(shù)據(jù)不夠,空位補(bǔ)空格。
  2. 在包的尾部加回車換行符標(biāo)識(shí)。
  3. 將消息分為消息頭與消息體,消息頭中包含消息總長度。
  4. 設(shè)計(jì)更復(fù)雜的協(xié)議。

5、Netty中的解決辦法

Netty提供了多種默認(rèn)的編碼器解決粘包和拆包:


5.1、LineBasedFrameDecoder

基于回車換行符的解碼器,當(dāng)遇到"\n"或者 "\r\n"結(jié)束符時(shí),分為一組。支持?jǐn)y帶結(jié)束符或者不帶結(jié)束符兩種編碼方式,也支持配置單行的最大長度。
LineBasedFrameDecoder與StringDecoder搭配時(shí),相當(dāng)于按行切換的文本解析器,用來支持TCP的粘包和拆包。
使用例子:

private void start() throws Exception {
        //創(chuàng)建 EventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        NioEventLoopGroup work = new NioEventLoopGroup();
        try {
            //創(chuàng)建 ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            b.group(group, work)
                    //指定使用 NIO 的傳輸 Channel
                    .channel(NioServerSocketChannel.class)
                    //設(shè)置 socket 地址使用所選的端口
                    .localAddress(new InetSocketAddress(port))
                    //添加 EchoServerHandler 到 Channel 的 ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new LineBasedFrameDecoder(1024));
                            p.addLast(new StringDecoder());
                            p.addLast(new StringEncoder());
                            p.addLast(new EchoServerHandler());
                        }
                    });
            //綁定的服務(wù)器;sync 等待服務(wù)器關(guān)閉
            ChannelFuture f = b.bind().sync();
            System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
            //關(guān)閉 channel 和 塊,直到它被關(guān)閉
            f.channel().closeFuture().sync();
        } finally {
            //關(guān)機(jī)的 EventLoopGroup,釋放所有資源。
            group.shutdownGracefully().sync();
        }
    }

注意ChannelPipeline 中ChannelHandler的順序,

5.2、DelimiterBasedFrameDecoder

分隔符解碼器,可以指定消息結(jié)束的分隔符,它可以自動(dòng)完成以分隔符作為碼流結(jié)束標(biāo)識(shí)的消息的解碼?;剀嚀Q行解碼器實(shí)際上是一種特殊的DelimiterBasedFrameDecoder解碼器。
使用例子(后面的代碼只貼ChannelPipeline部分):

ChannelPipeline p = ch.pipeline();
p.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("制定的分隔符".getBytes())));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());

5.3、FixedLengthFrameDecoder

固定長度解碼器,它能夠按照指定的長度對消息進(jìn)行自動(dòng)解碼,當(dāng)制定的長度過大,消息過短時(shí)會(huì)有資源浪費(fèi),但是使用起來簡單。

 ChannelPipeline p = ch.pipeline();
p.addLast(new FixedLengthFrameDecoder(1 << 5));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());

5.4、LengthFieldBasedFrameDecoder

通用解碼器,一般協(xié)議頭中帶有長度字段,通過使用LengthFieldBasedFrameDecoder傳入特定的參數(shù),來解決拆包粘包。
io.netty.handler.codec.LengthFieldBasedFrameDecoder的實(shí)例化:

    /**
     * Creates a new instance.
     *
     * @param maxFrameLength      最大幀長度。也就是可以接收的數(shù)據(jù)的最大長度。如果超過,此次數(shù)據(jù)會(huì)被丟棄。
     * @param lengthFieldOffset   長度域偏移。就是說數(shù)據(jù)開始的幾個(gè)字節(jié)可能不是表示數(shù)據(jù)長度,需要后移幾個(gè)字節(jié)才是長度域。
     * @param lengthFieldLength   長度域字節(jié)數(shù)。用幾個(gè)字節(jié)來表示數(shù)據(jù)長度。
     * @param lengthAdjustment    數(shù)據(jù)長度修正。因?yàn)殚L度域指定的長度可以是header+body的整個(gè)長度,也可以只是body的長度。如果表示header+body的整個(gè)長度,那么我們需要修正數(shù)據(jù)長度。
     * @param initialBytesToStrip 跳過的字節(jié)數(shù)。如果你需要接收header+body的所有數(shù)據(jù),此值就是0,如果你只想接收body數(shù)據(jù),那么需要跳過header所占用的字節(jié)數(shù)。
     * @param failFast            如果為true,則在解碼器注意到幀的長度將超過maxFrameLength時(shí)立即拋出TooLongFrameException,而不管是否已讀取整個(gè)幀。
     *                            如果為false,則在讀取了超過maxFrameLength的整個(gè)幀之后引發(fā)TooLongFrameException。
     */
    public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
                                        int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        //略
    }
  • maxFrameLength
    最大幀長度。也就是可以接收的數(shù)據(jù)的最大長度。如果超過,此次數(shù)據(jù)會(huì)被丟棄。
  • lengthFieldOffset
    長度域偏移。就是說數(shù)據(jù)開始的幾個(gè)字節(jié)可能不是表示數(shù)據(jù)長度,需要后移幾個(gè)字節(jié)才是長度域。
  • lengthFieldLength
    長度域字節(jié)數(shù)。用幾個(gè)字節(jié)來表示數(shù)據(jù)長度。
  • lengthAdjustment
    數(shù)據(jù)長度修正。因?yàn)殚L度域指定的長度可以是header+body的整個(gè)長度,也可以只是body的長度。如果表示header+body的整個(gè)長度,那么我們需要修正數(shù)據(jù)長度。
  • initialBytesToStrip
    跳過的字節(jié)數(shù)。如果你需要接收header+body的所有數(shù)據(jù),此值就是0,如果你只想接收body數(shù)據(jù),那么需要跳過header所占用的字節(jié)數(shù)。
  • failFast
    如果為true,則在解碼器注意到幀的長度將超過maxFrameLength時(shí)立即拋出TooLongFrameException,而不管是否已讀取整個(gè)幀。
    如果為false,則在讀取了超過maxFrameLength的整個(gè)幀之后引發(fā)TooLongFrameException。

下面通過Netty源碼中LengthFieldBasedFrameDecoder的注釋幾個(gè)例子看一下參數(shù)的使用:

5.4.1、2 bytes length field at offset 0, do not strip header

本例中的length字段的值是12 (0x0C),它表示“HELLO, WORLD”的長度。默認(rèn)情況下,解碼器假定長度字段表示長度字段后面的字節(jié)數(shù)。

  • lengthFieldOffset = 0: 開始的2個(gè)字節(jié)就是長度域,所以不需要長度域偏移。
  • lengthFieldLength = 2: 長度域2個(gè)字節(jié)。
  • lengthAdjustment = 0: 數(shù)據(jù)長度修正為0,因?yàn)殚L度域只包含數(shù)據(jù)的長度,所以不需要修正。
  • initialBytesToStrip = 0: 發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.2、2 bytes length field at offset 0, strip header

因?yàn)槲覀兛梢酝ㄟ^調(diào)用readableBytes()來獲得內(nèi)容的長度,所以可能希望通過指定initialbystrip來刪除長度字段。在本例中,我們指定2(與length字段的長度相同)來去掉前兩個(gè)字節(jié)。

  • lengthFieldOffset = 0: 開始的2個(gè)字節(jié)就是長度域,所以不需要長度域偏移。
  • lengthFieldLength = 2 :長度域2個(gè)字節(jié)。
  • lengthAdjustment = 0: 數(shù)據(jù)長度修正為0,因?yàn)殚L度域只包含數(shù)據(jù)的長度,所以不需要修正。
  • initialBytesToStrip = 2 :我們發(fā)現(xiàn)接收的數(shù)據(jù)沒有長度域的數(shù)據(jù),所以要跳過長度域的2個(gè)字節(jié)。
5.4.3、2 bytes length field at offset 0, do not strip header, the length field represents the length of the whole message

在大多數(shù)情況下,length字段僅表示消息體的長度,如前面的示例所示。但是,在一些協(xié)議中,長度字段表示整個(gè)消息的長度,包括消息頭。在這種情況下,我們指定一個(gè)非零長度調(diào)整。因?yàn)檫@個(gè)示例消息中的長度值總是比主體長度大2,所以我們指定-2作為補(bǔ)償?shù)拈L度調(diào)整。

  • lengthFieldOffset = 0: 開始的2個(gè)字節(jié)就是長度域,所以不需要長度域偏移。
  • lengthFieldLength = 2: 長度域2個(gè)字節(jié)。
  • lengthAdjustment = -2 :因?yàn)殚L度域?yàn)榭傞L度,所以我們需要修正數(shù)據(jù)長度,也就是減去2。
  • initialBytesToStrip = 0 :發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.4、3 bytes length field at the end of 5 bytes header, do not strip header

下面的消息是第一個(gè)示例的簡單變體。一個(gè)額外的頭值被預(yù)先寫入消息中。長度調(diào)整再次為零,因?yàn)樽g碼器在計(jì)算幀長時(shí)總是考慮到預(yù)寫數(shù)據(jù)的長度。

  • lengthFieldOffset = 2 :(= the length of Header 1)跳過2字節(jié)之后才是長度域
  • lengthFieldLength = 3:長度域3個(gè)字節(jié)。
  • lengthAdjustment = 0:數(shù)據(jù)長度修正為0,因?yàn)殚L度域只包含數(shù)據(jù)的長度,所以不需要修正。
  • initialBytesToStrip = 0:發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.5、3 bytes length field at the beginning of 5 bytes header, do not strip header

這是一個(gè)高級示例,展示了在長度字段和消息正文之間有一個(gè)額外頭的情況。您必須指定一個(gè)正的長度調(diào)整,以便解碼器將額外的標(biāo)頭計(jì)數(shù)到幀長度計(jì)算中。

  • lengthFieldOffset = 0:開始的就是長度域,所以不需要長度域偏移。
  • lengthFieldLength = 3:長度域3個(gè)字節(jié)。
  • lengthAdjustment = 2 :(= the length of Header 1) 長度修正2個(gè)字節(jié),加2
  • initialBytesToStrip = 0:發(fā)送和接收的數(shù)據(jù)完全一致,所以不需要跳過任何字節(jié)。
5.4.6、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field

這是上述所有示例的組合。在長度字段之前有預(yù)寫的header,在長度字段之后有額外的header。預(yù)先設(shè)置的header會(huì)影響lengthFieldOffset,而額外的leader會(huì)影響lengthAdjustment。我們還指定了一個(gè)非零initialBytesToStrip來從幀中去除長度字段和預(yù)定的header。如果不想去掉預(yù)寫的header,可以為initialBytesToSkip指定0。

  • lengthFieldOffset = 1 :(= the length of HDR1) ,跳過1個(gè)字節(jié)之后才是長度域
  • lengthFieldLength = 2:長度域2個(gè)字節(jié)
  • lengthAdjustment = 1: (= the length of HDR2)
  • initialBytesToStrip = 3 :(= the length of HDR1 + LEN)
5.4.7、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the whole message

讓我們對前面的示例進(jìn)行另一個(gè)修改。與前一個(gè)示例的惟一區(qū)別是,length字段表示整個(gè)消息的長度,而不是消息正文的長度,就像第三個(gè)示例一樣。我們必須把HDR1的長度和長度計(jì)算進(jìn)長度調(diào)整里。請注意,我們不需要考慮HDR2的長度,因?yàn)閘ength字段已經(jīng)包含了整個(gè)頭的長度。

  • lengthFieldOffset = 1:長度域偏移1個(gè)字節(jié),之后才是長度域。
  • lengthFieldLength = 2:長度域2個(gè)字節(jié)。
  • lengthAdjustment = -3: (= the length of HDR1 + LEN, negative)數(shù)據(jù)長度修正-3個(gè)字節(jié)。
  • initialBytesToStrip = 3:因?yàn)榻邮艿臄?shù)據(jù)比發(fā)送的數(shù)據(jù)少3個(gè)字節(jié),所以跳過3個(gè)字節(jié)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 簡介 用簡單的話來定義tcpdump,就是:dump the traffic on a network,根據(jù)使用者...
    JasonShi6306421閱讀 1,354評論 0 1
  • 簡介 用簡單的話來定義tcpdump,就是:dump the traffic on a network,根據(jù)使用者...
    保川閱讀 6,088評論 1 13
  • 1,網(wǎng)絡(luò)傳輸三大問題:packet reordering, packet duplication, and pac...
    碼農(nóng)崛起閱讀 527評論 0 0
  • 拆包的原理 關(guān)于拆包原理的上一篇博文 netty源碼分析之拆包器的奧秘 中已詳細(xì)闡述,這里簡單總結(jié)下:netty的...
    簡書閃電俠閱讀 45,671評論 28 71
  • 血液透析腎友常規(guī)使用的藥物 對于血液透析病人來說,維系生命除了要規(guī)律透析,藥物也是維系生命必不可少的,那么血液透析...
    jw8868閱讀 347評論 1 1

友情鏈接更多精彩內(nèi)容