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


一、TCP協議傳輸過程

TCP協議是面向流的協議,是流式的,沒有業務上的分段,只會根據當前套接字緩衝區的狀況進行拆包或者粘包:
TCP協議傳輸過程.jpgjava

發送端的字節流都會先傳入緩衝區,再經過網絡傳入到接收端的緩衝區中,最終由接收端獲取。緩存

二、TCP粘包和拆包概念

由於TCP會根據緩衝區的實際狀況進行包的劃分,在業務上認爲,有的包被拆分紅多個包進行發送,也可能多個曉小的包封裝成一個大的包發送,這就是TCP的粘包或者拆包。服務器

三、TCP粘包和拆包圖解

粘包拆包圖解.png

假設客戶端分別發送了兩個數據包D1和D2給服務端,因爲服務端一次讀取到字節數是不肯定的,故可能存在如下幾種狀況:網絡

  1. 服務端分兩次讀取到兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包。
  2. 服務端一次接收到了兩個數據包,D1和D2粘在一塊兒,發生粘包。
  3. 服務端分兩次讀取到數據包,第一次讀取到了完整的D1包和D2包的部份內容,第二次讀取到了D2包的剩餘內容,發生拆包。
  4. 服務端分兩次讀取到數據包,第一次讀取到部分D1包,第二次讀取到剩餘的D1包和所有的D2包。

當TCP緩存再小一點的話,會把D1和D2分別拆成多個包發送。socket

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

由於TCP只負責數據發送,並不處理業務上的數據,因此只能在上層應用協議棧解決,目前的解決方案概括:ide

  1. 消息定長,每一個報文的大小固定,若是數據不夠,空位補空格。
  2. 在包的尾部加回車換行符標識。
  3. 將消息分爲消息頭與消息體,消息頭中包含消息總長度。
  4. 設計更復雜的協議。

五、Netty中的解決辦法

Netty提供了多種默認的編碼器解決粘包和拆包:
Netty解決方案.pngoop

5.一、LineBasedFrameDecoder

基於回車換行符的解碼器,當遇到"n"或者 "rn"結束符時,分爲一組。支持攜帶結束符或者不帶結束符兩種編碼方式,也支持配置單行的最大長度。
LineBasedFrameDecoder與StringDecoder搭配時,至關於按行切換的文本解析器,用來支持TCP的粘包和拆包。
使用例子:編碼

private void start() throws Exception {
        //建立 EventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        NioEventLoopGroup work = new NioEventLoopGroup();
        try {
            //建立 ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            b.group(group, work)
                    //指定使用 NIO 的傳輸 Channel
                    .channel(NioServerSocketChannel.class)
                    //設置 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());
                        }
                    });
            //綁定的服務器;sync 等待服務器關閉
            ChannelFuture f = b.bind().sync();
            System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
            //關閉 channel 和 塊,直到它被關閉
            f.channel().closeFuture().sync();
        } finally {
            //關機的 EventLoopGroup,釋放全部資源。
            group.shutdownGracefully().sync();
        }
    }

注意ChannelPipeline 中ChannelHandler的順序,spa

5.二、DelimiterBasedFrameDecoder

分隔符解碼器,能夠指定消息結束的分隔符,它能夠自動完成以分隔符做爲碼流結束標識的消息的解碼。回車換行解碼器其實是一種特殊的DelimiterBasedFrameDecoder解碼器。
使用例子(後面的代碼只貼ChannelPipeline部分):.net

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.三、FixedLengthFrameDecoder

固定長度解碼器,它可以按照指定的長度對消息進行自動解碼,當制定的長度過大,消息太短時會有資源浪費,可是使用起來簡單。

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

5.四、LengthFieldBasedFrameDecoder

通用解碼器,通常協議頭中帶有長度字段,經過使用LengthFieldBasedFrameDecoder傳入特定的參數,來解決拆包粘包。
io.netty.handler.codec.LengthFieldBasedFrameDecoder的實例化:

/**
     * Creates a new instance.
     *
     * @param maxFrameLength      最大幀長度。也就是能夠接收的數據的最大長度。若是超過,這次數據會被丟棄。
     * @param lengthFieldOffset   長度域偏移。就是說數據開始的幾個字節可能不是表示數據長度,須要後移幾個字節纔是長度域。
     * @param lengthFieldLength   長度域字節數。用幾個字節來表示數據長度。
     * @param lengthAdjustment    數據長度修正。由於長度域指定的長度能夠是header+body的整個長度,也能夠只是body的長度。若是表示header+body的整個長度,那麼咱們須要修正數據長度。
     * @param initialBytesToStrip 跳過的字節數。若是你須要接收header+body的全部數據,此值就是0,若是你只想接收body數據,那麼須要跳過header所佔用的字節數。
     * @param failFast            若是爲true,則在解碼器注意到幀的長度將超過maxFrameLength時當即拋出TooLongFrameException,而不論是否已讀取整個幀。
     *                            若是爲false,則在讀取了超過maxFrameLength的整個幀以後引起TooLongFrameException。
     */
    public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
                                        int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        //略
    }
  • maxFrameLength

最大幀長度。也就是能夠接收的數據的最大長度。若是超過,這次數據會被丟棄。

  • lengthFieldOffset

長度域偏移。就是說數據開始的幾個字節可能不是表示數據長度,須要後移幾個字節纔是長度域。

  • lengthFieldLength

長度域字節數。用幾個字節來表示數據長度。

  • lengthAdjustment

數據長度修正。由於長度域指定的長度能夠是header+body的整個長度,也能夠只是body的長度。若是表示header+body的整個長度,那麼咱們須要修正數據長度。

  • initialBytesToStrip

跳過的字節數。若是你須要接收header+body的全部數據,此值就是0,若是你只想接收body數據,那麼須要跳過header所佔用的字節數。

  • failFast

若是爲true,則在解碼器注意到幀的長度將超過maxFrameLength時當即拋出TooLongFrameException,而不論是否已讀取整個幀。
若是爲false,則在讀取了超過maxFrameLength的整個幀以後引起TooLongFrameException。

下面經過Netty源碼中LengthFieldBasedFrameDecoder的註釋幾個例子看一下參數的使用:

5.4.一、2 bytes length field at offset 0, do not strip header

本例中的length字段的值是12 (0x0C),它表示「HELLO, WORLD」的長度。默認狀況下,解碼器假定長度字段表示長度字段後面的字節數。

  • lengthFieldOffset = 0: 開始的2個字節就是長度域,因此不須要長度域偏移。
  • lengthFieldLength = 2: 長度域2個字節。
  • lengthAdjustment = 0: 數據長度修正爲0,由於長度域只包含數據的長度,因此不須要修正。
  • initialBytesToStrip = 0: 發送和接收的數據徹底一致,因此不須要跳過任何字節。

LengthFieldBasedFrameDecoder-1.png

5.4.二、2 bytes length field at offset 0, strip header

由於咱們能夠經過調用readableBytes()來得到內容的長度,因此可能但願經過指定initialbystrip來刪除長度字段。在本例中,咱們指定2(與length字段的長度相同)來去掉前兩個字節。

  • lengthFieldOffset = 0: 開始的2個字節就是長度域,因此不須要長度域偏移。
  • lengthFieldLength = 2 :長度域2個字節。
  • lengthAdjustment = 0: 數據長度修正爲0,由於長度域只包含數據的長度,因此不須要修正。
  • initialBytesToStrip = 2 :咱們發現接收的數據沒有長度域的數據,因此要跳過長度域的2個字節。

LengthFieldBasedFrameDecoder-2.png

5.4.三、2 bytes length field at offset 0, do not strip header, the length field represents the length of the whole message

在大多數狀況下,length字段僅表示消息體的長度,如前面的示例所示。可是,在一些協議中,長度字段表示整個消息的長度,包括消息頭。在這種狀況下,咱們指定一個非零長度調整。由於這個示例消息中的長度值老是比主體長度大2,因此咱們指定-2做爲補償的長度調整。

  • lengthFieldOffset = 0: 開始的2個字節就是長度域,因此不須要長度域偏移。
  • lengthFieldLength = 2: 長度域2個字節。
  • lengthAdjustment = -2 :由於長度域爲總長度,因此咱們須要修正數據長度,也就是減去2。
  • initialBytesToStrip = 0 :發送和接收的數據徹底一致,因此不須要跳過任何字節。

LengthFieldBasedFrameDecoder-3.png

5.4.四、3 bytes length field at the end of 5 bytes header, do not strip header

下面的消息是第一個示例的簡單變體。一個額外的頭值被預先寫入消息中。長度調整再次爲零,由於譯碼器在計算幀長時老是考慮到預寫數據的長度。

  • lengthFieldOffset = 2 :(= the length of Header 1)跳過2字節以後纔是長度域
  • lengthFieldLength = 3:長度域3個字節。
  • lengthAdjustment = 0:數據長度修正爲0,由於長度域只包含數據的長度,因此不須要修正。
  • initialBytesToStrip = 0:發送和接收的數據徹底一致,因此不須要跳過任何字節。

LengthFieldBasedFrameDecoder-4.png

5.4.五、3 bytes length field at the beginning of 5 bytes header, do not strip header

這是一個高級示例,展現了在長度字段和消息正文之間有一個額外頭的狀況。您必須指定一個正的長度調整,以便解碼器將額外的標頭計數到幀長度計算中。

  • lengthFieldOffset = 0:開始的就是長度域,因此不須要長度域偏移。
  • lengthFieldLength = 3:長度域3個字節。
  • lengthAdjustment = 2 :(= the length of Header 1) 長度修正2個字節,加2
  • initialBytesToStrip = 0:發送和接收的數據徹底一致,因此不須要跳過任何字節。

LengthFieldBasedFrameDecoder-5.png

5.4.六、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field

這是上述全部示例的組合。在長度字段以前有預寫的header,在長度字段以後有額外的header。預先設置的header會影響lengthFieldOffset,而額外的leader會影響lengthAdjustment。咱們還指定了一個非零initialBytesToStrip來從幀中去除長度字段和預約的header。若是不想去掉預寫的header,能夠爲initialBytesToSkip指定0。

  • lengthFieldOffset = 1 :(= the length of HDR1) ,跳過1個字節以後纔是長度域
  • lengthFieldLength = 2:長度域2個字節
  • lengthAdjustment = 1: (= the length of HDR2)
  • initialBytesToStrip = 3 :(= the length of HDR1 + LEN)

LengthFieldBasedFrameDecoder-6.png

5.4.七、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

讓咱們對前面的示例進行另外一個修改。與前一個示例的唯一區別是,length字段表示整個消息的長度,而不是消息正文的長度,就像第三個示例同樣。咱們必須把HDR1的長度和長度計算進長度調整裏。請注意,咱們不須要考慮HDR2的長度,由於length字段已經包含了整個頭的長度。

  • lengthFieldOffset = 1:長度域偏移1個字節,以後纔是長度域。
  • lengthFieldLength = 2:長度域2個字節。
  • lengthAdjustment = -3: (= the length of HDR1 + LEN, negative)數據長度修正-3個字節。
  • initialBytesToStrip = 3:由於接受的數據比發送的數據少3個字節,因此跳過3個字節。

LengthFieldBasedFrameDecoder-7.png

tencent.jpg

相關文章
相關標籤/搜索