自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Netty 如何駕馭 TCP 流式傳輸?粘包拆包問題全解與編解碼器優(yōu)秀實(shí)踐

開發(fā) 網(wǎng)絡(luò)
本文是筆者對(duì)于 Netty 如何解決半包與粘包問題的源碼解析與實(shí)踐的全部?jī)?nèi)容,希望對(duì)你有幫助。?

當(dāng)Netty涉及網(wǎng)絡(luò)IO數(shù)據(jù)傳輸時(shí),可能會(huì)涉及到下面這些面試題:

  • 什么是TCP粘包和拆包?為什么UDP不會(huì)出現(xiàn)這個(gè)問題?
  • 發(fā)生粘包和拆包的原因是什么?
  • Netty是如何解決TCP粘包和拆包的?

一、詳解TCP粘包拆包問題

1. 問題復(fù)現(xiàn)

在正式講解問題之前,我們先來看一段示例,查看TCP粘包和拆包問題是如何發(fā)生的,下面這兩段代碼分別是服務(wù)端配置和業(yè)務(wù)處理器,它會(huì)在與客戶端建立連接之后,不斷輸出客戶端發(fā)送的數(shù)據(jù):

public class NettyServer {
    public static void main(String[] args) {
        // 啟動(dòng)一個(gè)netty服務(wù)端需要指定 線程模型 IO模型 業(yè)務(wù)處理邏輯

        // 引導(dǎo)類負(fù)責(zé)引導(dǎo)服務(wù)端啟動(dòng)工作
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        // 以下兩個(gè)對(duì)象可以看做是兩個(gè)線程組

        // 負(fù)責(zé)監(jiān)聽端口,接受新的連接
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 負(fù)責(zé)處理每一個(gè)連接讀寫的線程組
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(1);

        // 配置線程組并指定NIO模型
        serverBootstrap.group(bossGroup, workerGroup)
                //設(shè)置IO模型,這里為NioServerSocketChannel,建議Linux服務(wù)器使用 EpollServerSocketChannel
                .channel(NioServerSocketChannel.class)
                // 定義后續(xù)每個(gè)連接的數(shù)據(jù)讀寫,對(duì)于業(yè)務(wù)處理邏輯
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline()
                                .addLast(new FirstServerHandler());
                    }
                });



        bind(serverBootstrap, 8888);
    }

    /**
     * 以端口號(hào)遞增的形式嘗試綁定端口號(hào)
     */
    private static void bind(ServerBootstrap serverBootstrap, int port) {
        serverBootstrap.bind(port);
    }
}

服務(wù)端業(yè)務(wù)處理器核心代碼,邏輯也非常簡(jiǎn)單,收到消息后直接打印輸出:

public class FirstServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 收到客戶端數(shù)據(jù)后會(huì)回調(diào)該方法
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println(DateUtil.now() + ": 服務(wù)端讀到數(shù)據(jù) -> " + byteBuf.toString(StandardCharsets.UTF_8));

    }


}

我們?cè)賮砜纯纯蛻舳说臉I(yè)務(wù)處理器和配置類,業(yè)務(wù)處理器的代碼非常簡(jiǎn)單,在建立連接后連續(xù)發(fā)送1000條數(shù)據(jù),數(shù)據(jù)內(nèi)容為:hello Netty Server!:

public class FirstClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 客戶端連接服務(wù)端成功后會(huì)回調(diào)該方法
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 1000; i++) {
            // 獲取數(shù)據(jù)
            ByteBuf byteBuf = getByteBuf(ctx);
            // 把數(shù)據(jù)寫到服務(wù)端
            ctx.channel().writeAndFlush(byteBuf);
        }

    }

    private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
        byte[] bytes = "hello Netty Server!".getBytes(StandardCharsets.UTF_8);

        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeBytes(bytes);

        return buffer;
    }


}

而配置類也是固定模板:

public class NettyClient {



    public static void main(String[] args) throws InterruptedException {
        // 整體即完成netty客戶端需要指定線程模型、IO模型、業(yè)務(wù)處理邏輯

        // 負(fù)責(zé)客戶端的啟動(dòng)
        Bootstrap bootstrap = new Bootstrap();
        // 客戶端的線程模型
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        // 指定線程組
        bootstrap.group(workerGroup)
                //指定NIO模型
                .channel(NioSocketChannel.class)
                // IO處理邏輯
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel channel) throws Exception {
                        channel.pipeline().addLast(new FirstClientHandler());
                    }
                });

        // 建立連接
        connect(bootstrap, "127.0.0.1", 8888);


    }

    /**
     * 建立連接的方法,使用監(jiān)聽器來進(jìn)行重試
     */
    private static Channel connect(Bootstrap bootstrap, String host, int port) {
        return bootstrap.connect(host, port).channel();
    }
}

將服務(wù)端和客戶端啟動(dòng)后,我們可以看到下面這段輸出,可以看到大量的hello Netty Server!數(shù)據(jù)粘在一起構(gòu)成一個(gè)個(gè)粘包。

2023-08-29 09:09:24: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Server!hello Netty Serve
2023-08-29 09:09:24: 服務(wù)端讀到數(shù)據(jù) -> r!hello Netty Server!hello Netty Server!hello Netty Ser

2. 原因剖析

在TCP編程中,在服務(wù)端與客戶端通信時(shí)消息都會(huì)有固定的消息格式,這種格式我們通常稱之為protocol即協(xié)議,例如我們常見的應(yīng)用層協(xié)議:HTTP、FTP等。

而上述例子出現(xiàn)粘包的原因本質(zhì)就是我們服務(wù)端與客戶端進(jìn)行通信時(shí),沒有確認(rèn)協(xié)議的規(guī)范,因?yàn)門CP是面向連接、面向流的協(xié)議,它會(huì)因?yàn)楦鞣N原因?qū)е峦暾臄?shù)據(jù)包被拆封無(wú)數(shù)個(gè)小的數(shù)據(jù)包進(jìn)行發(fā)送,進(jìn)而導(dǎo)致接收方收到數(shù)據(jù)后無(wú)法正確的處理數(shù)據(jù),出現(xiàn)粘包和拆包:

而出現(xiàn)TCP數(shù)據(jù)包被拆分的原因大致有3個(gè):

  • socket緩沖區(qū)與滑動(dòng)窗口
  • nagle算法
  • mss

先來說說socket緩沖區(qū)和滑動(dòng)窗口的共同作用,我們都知道TCP是全雙工、面向流的協(xié)議。這意味發(fā)送時(shí)必須要保證收發(fā)正常,所以TCP就提出了一個(gè)滑動(dòng)窗口機(jī)制,即以滑動(dòng)窗口的大小為單位,讓雙方基于這個(gè)窗口的大小進(jìn)行數(shù)據(jù)收發(fā),發(fā)送方只有在滑動(dòng)窗口以內(nèi)的數(shù)據(jù)才能被發(fā)送,接收方也只有在窗口以內(nèi)的數(shù)據(jù)被接收和處理,只有接收方的滑動(dòng)窗口收到發(fā)送方的數(shù)據(jù),且處理完成并發(fā)送確認(rèn)信號(hào)ACK之后,發(fā)送方的窗口才能繼續(xù)向后移動(dòng):

由于TCP是面向流的協(xié)議,在此期間雙方收發(fā)的數(shù)據(jù)也都會(huì)會(huì)存放到socket緩沖區(qū)中。這意味這連個(gè)緩沖區(qū)是無(wú)法知曉這些數(shù)據(jù)是否屬于同一個(gè)數(shù)據(jù)包的。 同理socket緩沖區(qū)也分為發(fā)送緩沖區(qū)(SO_SNDBUF )和接收緩沖區(qū)(SO_RCVBUF),所有socket需要發(fā)送的數(shù)據(jù)也都是存放到socket的緩沖區(qū)中然后通過內(nèi)核函數(shù)傳到內(nèi)核協(xié)議棧進(jìn)行數(shù)據(jù)發(fā)送,socket接收緩沖區(qū)也是通過操作系統(tǒng)的內(nèi)核函數(shù)將數(shù)據(jù)拷貝至socket緩沖區(qū)。

所以。socket緩沖區(qū)和滑動(dòng)窗口機(jī)制共同作用下就會(huì)出現(xiàn)以下兩種異常情況:

(1) 發(fā)送方發(fā)送的數(shù)據(jù)達(dá)到了滑動(dòng)窗口的限制,停止發(fā)送,接收方的socket緩沖區(qū)拿到這些數(shù)據(jù)后,直接向應(yīng)用層傳輸,因?yàn)榘皇峭暾?,從接收方的角度來看,出現(xiàn)了拆包。

(2) 發(fā)送方發(fā)送多個(gè)數(shù)據(jù)包到接收方緩沖區(qū),因?yàn)榻邮辗絪ocket緩沖區(qū)無(wú)法及時(shí)處理,導(dǎo)致真正開始處理時(shí)無(wú)法知曉數(shù)據(jù)包的邊界,只能一次性將數(shù)據(jù)包向上傳遞,導(dǎo)致粘包。

再來說說Nagle算法,考慮到每次發(fā)送數(shù)據(jù)包時(shí)都需要為數(shù)據(jù)加上TCP Header20字節(jié)和IP header 20字節(jié),以及還得等待發(fā)送方的ACK確認(rèn)包,這就很可能出現(xiàn)下面這種非常浪費(fèi)網(wǎng)絡(luò)資源的情況:

為了1個(gè)字節(jié)的有用信息去組裝10字節(jié)的頭部信息!

對(duì)此,操作系統(tǒng)為了盡可能的利用網(wǎng)絡(luò)帶寬,就提出了Nagle算法,該算法要求所有已發(fā)送出去的小數(shù)據(jù)包(長(zhǎng)度小于MSS)必須等到接收方的都回復(fù)ack信號(hào)之后,然后再將這些小數(shù)據(jù)段一并打包成一個(gè)打包發(fā)送,從而盡可能利用帶寬及盡可能避免因?yàn)榇罅啃〉木W(wǎng)絡(luò)包的傳輸造成網(wǎng)絡(luò)擁塞。

很明顯如果將多個(gè)小的數(shù)據(jù)包合并發(fā)送,接收方也很可能因?yàn)闊o(wú)法確認(rèn)數(shù)據(jù)包的邊界而出現(xiàn)粘包或拆包問題:

最后就是mss,也就是Maximum Segement Size的縮寫,代表傳輸一次性可以發(fā)送的數(shù)據(jù)最大長(zhǎng)度,如果數(shù)據(jù)超過MSS的最大值,那么網(wǎng)絡(luò)數(shù)據(jù)包就會(huì)被拆成多個(gè)小包發(fā)送,這種情況下也很可能因?yàn)榱懔闵⑸⒌臄?shù)據(jù)包發(fā)送而會(huì)出現(xiàn)粘包和拆包問題。

對(duì)此我們不妨通過WireShark進(jìn)行抓包分析,基于服務(wù)端端口鍵入如下指令進(jìn)行過濾:

ip.src==127.0.0.1 and ip.dst==127.0.0.1 and tcp.port==8888

啟動(dòng)客戶端和服務(wù)端之后,發(fā)現(xiàn)雙方交換得出的MSS遠(yuǎn)大于每次發(fā)送的數(shù)據(jù)大小,所以首先排除分包問題:

查看每次服務(wù)端發(fā)送的數(shù)據(jù),無(wú)論大小還是內(nèi)容都沒有缺失,內(nèi)核緩沖區(qū)空間也是充足的,所以原因很明顯,因?yàn)門CP協(xié)議是面向流傳輸,接收方從內(nèi)核緩沖區(qū)讀取時(shí),拿到了過多或者過少的數(shù)據(jù)導(dǎo)致粘包或拆包。

二、半包粘包的解決對(duì)策

1. 幾種解決對(duì)策簡(jiǎn)介

其實(shí)上述的問題的原因都是因?yàn)門CP是面向流的協(xié)議,導(dǎo)致了數(shù)據(jù)包無(wú)法被正常切割成一個(gè)個(gè)正常數(shù)據(jù)包的流。就以上面的數(shù)據(jù)包為例,發(fā)送的數(shù)據(jù)為hello Netty Server!,其實(shí)我們做到下面這幾種分割方式:

  • 如果發(fā)送的數(shù)據(jù)都是以"!"結(jié)尾,那我們的分割時(shí)就判斷收到的流是否包含"!",只有包含時(shí)再將數(shù)據(jù)裝成數(shù)據(jù)包發(fā)送。
  • 上述發(fā)送的數(shù)據(jù)長(zhǎng)度為19,我們也可以規(guī)定發(fā)送的數(shù)據(jù)長(zhǎng)度為19字節(jié),一旦收到的數(shù)據(jù)達(dá)到19個(gè)字節(jié)之后,就組裝成一個(gè)數(shù)據(jù)包。
  • 自定義一個(gè)協(xié)議,要求發(fā)送方根據(jù)協(xié)議要求組裝數(shù)據(jù)包發(fā)送,例如要求數(shù)據(jù)包包含長(zhǎng)度length和data兩個(gè)字段,其中l(wèi)ength記錄數(shù)據(jù)包長(zhǎng)度,以上述數(shù)據(jù)為例,這個(gè)字段的值為19,而data包含的就是數(shù)據(jù)內(nèi)容。

2. 基于分隔符的解碼器DelimiterBasedFrameDecoder

先來看看基于分隔符的,可以看到每一個(gè)數(shù)據(jù)末尾都有一個(gè)感嘆號(hào),所以我們可以通過判斷特殊符號(hào)完成數(shù)據(jù)拆包。

代碼如下,我們基于DelimiterBasedFrameDecoder完成基于特殊分隔符進(jìn)行拆包,每個(gè)參數(shù)對(duì)應(yīng)含義為:

  • 數(shù)據(jù)包最大長(zhǎng)度。
  • 解碼時(shí)是否去掉分隔符。
  • 分隔符。
ByteBuf delimiter = Unpooled.copiedBuffer("!".getBytes());
 
                        nioSocketChannel.pipeline()
                                .addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,false,delimiter))
                                .addLast(new FirstServerHandler());

啟動(dòng)之后可以看到問題也得以解決:

2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!
2023-08-29 09:19:44: 服務(wù)端讀到數(shù)據(jù) -> hello Netty Server!

3. 基于數(shù)據(jù)長(zhǎng)度的解碼器FixedLengthFrameDecoder

同理,我們也可以基于數(shù)據(jù)長(zhǎng)度,對(duì)數(shù)據(jù)包進(jìn)行分割:

由上文可知,我們發(fā)送的數(shù)據(jù)長(zhǎng)度都是19,所以第一種方案是在服務(wù)端的pipeline配置一個(gè)基于長(zhǎng)度拆包的解碼器,確保在每19個(gè)字節(jié)截取一次以確保數(shù)據(jù)包可以正確讀取和解析。 所以我們?cè)趐ipeline添加一個(gè)FixedLengthFrameDecoder,長(zhǎng)度設(shè)置為19。

nioSocketChannel.pipeline()
                                .addLast(new FixedLengthFrameDecoder(19))
                                .addLast(new FirstServerHandler());

4. 基于協(xié)議長(zhǎng)度字段的解碼器LengthFieldBasedFrameDecoder

最后一種,也是筆者比較推薦的一種長(zhǎng)度,即自定義協(xié)議,我們?cè)趥鬏斶^程中,可能數(shù)據(jù)的長(zhǎng)度或者分隔符都無(wú)法保證,所以我們可以和客戶端協(xié)商一下,在傳輸?shù)臄?shù)據(jù)頭部添加一個(gè)數(shù)據(jù)包長(zhǎng)度,例如用4字節(jié)表示數(shù)據(jù)包長(zhǎng)度。

所以客戶端建立連接后寫數(shù)據(jù)的代碼就改為:

private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
        byte[] bytes = "hello Netty Server!".getBytes(StandardCharsets.UTF_8);

        ByteBuf buffer = ctx.alloc().buffer();
        //4個(gè)字節(jié)說明數(shù)據(jù)的長(zhǎng)度
        buffer.writeInt(bytes.length);
        //寫入數(shù)據(jù)內(nèi)容
        buffer.writeBytes(bytes);

        return buffer;
    }

最終的數(shù)據(jù)包結(jié)構(gòu)如下圖所示:

圖片圖片

而服務(wù)端的處理器則改為使用LengthFieldBasedFrameDecoder,構(gòu)造方法如下:

public LengthFieldBasedFrameDecoder(
            int maxFrameLength,
            int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip) {
       //.......
    }

按照對(duì)應(yīng)參數(shù)含義為:

  • maxFrameLength:數(shù)據(jù)包最大長(zhǎng)度,這里我們?cè)O(shè)置為Integer.MAX_VALUE,等同于不限制。
  • lengthFieldOffset:該數(shù)值代表獲取描述數(shù)據(jù)包長(zhǎng)度的字段的位置偏移量,以我們的數(shù)據(jù)包為例,就是0,即從最初始的位置讀取長(zhǎng)度。
  • lengthFieldLength:描述數(shù)據(jù)包長(zhǎng)度的字段的字節(jié)數(shù),以我們的數(shù)據(jù)包為例就是4字節(jié)。
  • lengthAdjustment:要添加到長(zhǎng)度字段值的補(bǔ)償值,這個(gè)字段比較有意思,我們還是舉個(gè)例子說明,以下面這個(gè)數(shù)據(jù)包為例,假如我們需要得到data的數(shù)據(jù),而長(zhǎng)度記錄的值為12字節(jié)(head+length+data),為了達(dá)到我們的預(yù)期即只取10字節(jié)的數(shù)據(jù),我們就可以基于將這個(gè)字段的值設(shè)置為-2,將12減去10以得到實(shí)際的data數(shù)據(jù)長(zhǎng)度。

對(duì)應(yīng)的我們本次數(shù)據(jù)包長(zhǎng)度記錄的值沒有錯(cuò),這里直接直接設(shè)置為0,無(wú)需調(diào)整。

  • initialBytesToStrip:讀取時(shí)需要跳過數(shù)據(jù)包幾個(gè)字節(jié),以我們的數(shù)據(jù)包為例就說4,代表我們要跳過4字節(jié)的length字段,只要data的數(shù)據(jù),對(duì)應(yīng)的我們也給出下面這個(gè)構(gòu)造方法:

于是我們就有了下面這樣一個(gè)構(gòu)造的解碼器,再次進(jìn)行壓測(cè)后數(shù)據(jù)都是可以正常解析處理的:

nioSocketChannel.pipeline()
                                .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4))
                                .addLast(new FirstServerHandler());

5. 更多關(guān)于Netty內(nèi)置解碼器

設(shè)計(jì)者也在注釋上為我們提供更多的使用案例,先來看看第一個(gè)示例,該數(shù)據(jù)包長(zhǎng)度字段2字節(jié),偏移量為0。假如我們希望讀整個(gè)數(shù)據(jù)包,那么參數(shù)設(shè)置方式為:

  • lengthFieldOffset即偏移量設(shè)置為0,即長(zhǎng)度字段無(wú)需偏移就在數(shù)據(jù)包高位。
  • lengthFieldLength為2,即讀取2字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為0,代表長(zhǎng)度字段描述的數(shù)據(jù)就是后續(xù)數(shù)據(jù)的長(zhǎng)度,無(wú)需調(diào)整。
  • initialBytesToStrip 為0,即讀取時(shí)從數(shù)據(jù)包最開始位置讀取并加上長(zhǎng)度字段里描述的長(zhǎng)度的數(shù)據(jù),無(wú)需跳過。
* <b>lengthFieldOffset</b>   = <b>0</b>
 * <b>lengthFieldLength</b>   = <b>2</b>
 * lengthAdjustment    = 0
 * initialBytesToStrip = 0 (= do not strip header)
 *
 * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 * +--------+----------------+      +--------+----------------+
 * | Length | Actual Content |----->| Length | Actual Content |
 * | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
 * +--------+----------------+      +--------+----------------+
 * </pre>

再來看看示例2,數(shù)據(jù)包和上文相同,只不過希望讀取的數(shù)據(jù)不包含length字段,所以參數(shù)設(shè)置為:

  • lengthFieldOffset即偏移量設(shè)置為0,即長(zhǎng)度字段無(wú)需偏移就在數(shù)據(jù)包高位。
  • lengthFieldLength為2,即讀取2字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為0,代表長(zhǎng)度字段描述的數(shù)據(jù)就是后續(xù)數(shù)據(jù)的長(zhǎng)度,無(wú)需調(diào)整。
  • initialBytesToStrip 為2,即讀取時(shí)從數(shù)據(jù)包起始位置開始,跳過2字節(jié)數(shù)據(jù),即跳過length字段。
* lengthFieldOffset   = 0
 * lengthFieldLength   = 2
 * lengthAdjustment    = 0
 * <b>initialBytesToStrip</b> = <b>2</b> (= the length of the Length field)
 *
 * BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
 * +--------+----------------+      +----------------+
 * | Length | Actual Content |----->| Actual Content |
 * | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
 * +--------+----------------+      +----------------+
 * </pre>

再來看看情況3,2字節(jié)長(zhǎng)度描述長(zhǎng)度,只不過該長(zhǎng)度包含了描述長(zhǎng)度的字段長(zhǎng)度,即length的值為length字段長(zhǎng)度2+后續(xù)HELLO, WORLD字符串長(zhǎng)度為14。如果我們希望獲取一個(gè)完整的數(shù)據(jù)包,那么參數(shù)就需要設(shè)置為:

  • lengthFieldOffset即偏移量設(shè)置為0,即長(zhǎng)度字段無(wú)需偏移就在數(shù)據(jù)包高位。
  • lengthFieldLength為2,即讀取2字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為-2,代表長(zhǎng)度字段描述的是整個(gè)包的長(zhǎng)度,需要減去length字段的長(zhǎng)度。
  • initialBytesToStrip 為0,即讀取時(shí)從數(shù)據(jù)包最開始位置讀取并加上長(zhǎng)度字段里描述的長(zhǎng)度的數(shù)據(jù),無(wú)需跳過。
* lengthFieldOffset   =  0
 * lengthFieldLength   =  2
 * <b>lengthAdjustment</b>    = <b>-2</b> (= the length of the Length field)
 * initialBytesToStrip =  0
 *
 * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 * +--------+----------------+      +--------+----------------+
 * | Length | Actual Content |----->| Length | Actual Content |
 * | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
 * +--------+----------------+      +--------+----------------+
 * </pre>

示例4需要跳過header字段讀取到長(zhǎng)度字段,最后需要得到一個(gè)包含所有部分的數(shù)據(jù)包,所以參數(shù)如下:

  • lengthFieldOffset即偏移量設(shè)置為2,即跳過Header 。
  • lengthFieldLength為3,即讀取3字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為0,代表長(zhǎng)度字段描述的是就是后續(xù)數(shù)據(jù)的長(zhǎng)度,無(wú)需調(diào)整。
  • initialBytesToStrip 為0,即讀取時(shí)從數(shù)據(jù)包最開始位置讀取并加上長(zhǎng)度字段里描述的長(zhǎng)度的數(shù)據(jù),無(wú)需跳過。
* BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
 * +----------+----------+----------------+      +----------+----------+----------------+
 * | Header 1 |  Length  | Actual Content |----->| Header 1 |  Length  | Actual Content |
 * |  0xCAFE  | 0x00000C | "HELLO, WORLD" |      |  0xCAFE  | 0x00000C | "HELLO, WORLD" |
 * +----------+----------+----------------+      +----------+----------+----------------+
 * </pre>

示例5情況比較特殊,length描述后文數(shù)據(jù)的長(zhǎng)度,卻不包含后文header的長(zhǎng)度,若我們希望獲取到所有部分的數(shù)據(jù)包,則參數(shù)需要設(shè)置為:

  • lengthFieldOffset即偏移量設(shè)置為0,即無(wú)需偏移,長(zhǎng)度就在數(shù)據(jù)包高位。
  • lengthFieldLength為3,即讀取3字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為2,即代表length字段僅僅記錄的Actual Content的長(zhǎng)度,length字段后面還有一個(gè)header的長(zhǎng)度需要計(jì)算,故設(shè)置為2,意味實(shí)際長(zhǎng)度要+2。
  • initialBytesToStrip 為0,即讀取時(shí)從數(shù)據(jù)包最開始位置讀取并加上長(zhǎng)度字段里描述的長(zhǎng)度的數(shù)據(jù),無(wú)需跳過。
* <pre>
 * lengthFieldOffset   = 0
 * lengthFieldLength   = 3
 * <b>lengthAdjustment</b>    = <b>2</b> (= the length of Header 1)
 * initialBytesToStrip = 0
 *
 * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
 * +----------+----------+----------------+      +----------+----------+----------------+
 * |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
 * | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
 * +----------+----------+----------------+      +----------+----------+----------------+
 * </pre>

示例6,長(zhǎng)度在hdr1后面,并且最終讀取的數(shù)據(jù)是hdr2和Actual Content。參數(shù)設(shè)置為:

  • lengthFieldOffset即偏移量設(shè)置為1,即跳過HDR1。
  • lengthFieldLength為2,即讀取2字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為1,即代表length字段僅僅記錄的Actual Content的長(zhǎng)度,length字段后面還有一個(gè)HDR2 的長(zhǎng)度需要計(jì)算,故設(shè)置為1,意味實(shí)際長(zhǎng)度要+1。
  • initialBytesToStrip 為3,即跳過HDR1和length開始讀取。
* lengthFieldOffset   = 1 (= the length of HDR1)
 * lengthFieldLength   = 2
 * <b>lengthAdjustment</b>    = <b>1</b> (= the length of HDR2)
 * <b>initialBytesToStrip</b> = <b>3</b> (= the length of HDR1 + LEN)
 *
 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
 * +------+--------+------+----------------+      +------+----------------+
 * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
 * | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
 * +------+--------+------+----------------+      +------+----------------+
 * </pre>

示例7即可Length記錄的是整個(gè)包的長(zhǎng)度,為了拿到HDR2和Actual Content的數(shù)據(jù),對(duì)應(yīng)參數(shù)設(shè)置如下:

  • lengthFieldOffset即偏移量設(shè)置為1,即跳過HDR1。
  • lengthFieldLength為2,即讀取2字節(jié)的數(shù)據(jù),即可獲得數(shù)據(jù)包長(zhǎng)度。
  • lengthAdjustment 為-3,即代表減去HDR1和 LEN的字段長(zhǎng)度。
  • initialBytesToStrip 為3,即跳過HDR1和length開始讀取。
* lengthFieldOffset   =  1
 * lengthFieldLength   =  2
 * <b>lengthAdjustment</b>    = <b>-3</b> (= the length of HDR1 + LEN, negative)
 * <b>initialBytesToStrip</b> = <b> 3</b>
 *
 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
 * +------+--------+------+----------------+      +------+----------------+
 * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
 * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
 * +------+--------+------+----------------+      +------+----------------+
 * </pre>

三、小結(jié)

以上便是筆者對(duì)于Netty如何解決半包與粘包問題的源碼解析與實(shí)踐的全部?jī)?nèi)容,希望對(duì)你有幫助。

責(zé)任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2021-10-08 09:38:57

NettyChannelHand架構(gòu)

2021-08-03 08:38:21

Netty解碼器使用

2020-10-15 18:31:36

理解Netty編解碼

2021-07-15 10:35:16

NettyTCPJava

2019-10-17 11:06:32

TCP粘包通信協(xié)議

2024-12-19 11:00:00

TCP網(wǎng)絡(luò)通信粘包

2020-01-06 15:23:41

NettyTCP粘包

2021-03-09 22:30:47

TCP拆包協(xié)議

2022-04-28 08:38:09

TCP協(xié)議解碼器

2024-07-05 08:27:07

2024-08-16 21:47:18

2021-04-07 13:52:57

GoogleLyra編譯器

2023-07-26 16:31:09

Windows 10Windows 11微軟

2022-10-10 10:38:22

FedoraopenSUSE視頻編解碼

2020-12-23 07:53:01

TCP通信Netty

2020-03-10 08:27:24

TCP粘包網(wǎng)絡(luò)協(xié)議

2024-06-03 08:09:46

2019-10-25 00:32:12

TCP粘包Netty

2021-12-25 16:20:38

微軟WindowsWindows 10

2020-02-19 19:15:27

UbuntuLinux媒體編解碼器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)