Netty 如何驾驭 TCP 流式传输?粘包拆包问题全解与编解码器优秀实践

开发 网络
本文是笔者对于 Netty 如何解决半包与粘包问题的源码解析与实践的全部内容,希望对你有帮助。​

当Netty涉及网络IO数据传输时,可能会涉及到下面这些面试题:

  • 什么是TCP粘包和拆包?为什么UDP不会出现这个问题?
  • 发生粘包和拆包的原因是什么?
  • Netty是如何解决TCP粘包和拆包的?

一、详解TCP粘包拆包问题

1. 问题复现

在正式讲解问题之前,我们先来看一段示例,查看TCP粘包和拆包问题是如何发生的,下面这两段代码分别是服务端配置和业务处理器,它会在与客户端建立连接之后,不断输出客户端发送的数据:

public class NettyServer {
    public static void main(String[] args) {
        // 启动一个netty服务端需要指定 线程模型 IO模型 业务处理逻辑

        // 引导类负责引导服务端启动工作
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        // 以下两个对象可以看做是两个线程组

        // 负责监听端口,接受新的连接
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 负责处理每一个连接读写的线程组
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(1);

        // 配置线程组并指定NIO模型
        serverBootstrap.group(bossGroup, workerGroup)
                //设置IO模型,这里为NioServerSocketChannel,建议Linux服务器使用 EpollServerSocketChannel
                .channel(NioServerSocketChannel.class)
                // 定义后续每个连接的数据读写,对于业务处理逻辑
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline()
                                .addLast(new FirstServerHandler());
                    }
                });



        bind(serverBootstrap, 8888);
    }

    /**
     * 以端口号递增的形式尝试绑定端口号
     */
    private static void bind(ServerBootstrap serverBootstrap, int port) {
        serverBootstrap.bind(port);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

服务端业务处理器核心代码,逻辑也非常简单,收到消息后直接打印输出:

public class FirstServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 收到客户端数据后会回调该方法
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println(DateUtil.now() + ": 服务端读到数据 -> " + byteBuf.toString(StandardCharsets.UTF_8));

    }


}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

我们再来看看客户端的业务处理器和配置类,业务处理器的代码非常简单,在建立连接后连续发送1000条数据,数据内容为:hello Netty Server!:

public class FirstClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 客户端连接服务端成功后会回调该方法
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 1000; i++) {
            // 获取数据
            ByteBuf byteBuf = getByteBuf(ctx);
            // 把数据写到服务端
            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;
    }


}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

而配置类也是固定模板:

public class NettyClient {



    public static void main(String[] args) throws InterruptedException {
        // 整体即完成netty客户端需要指定线程模型、IO模型、业务处理逻辑

        // 负责客户端的启动
        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);


    }

    /**
     * 建立连接的方法,使用监听器来进行重试
     */
    private static Channel connect(Bootstrap bootstrap, String host, int port) {
        return bootstrap.connect(host, port).channel();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

将服务端和客户端启动后,我们可以看到下面这段输出,可以看到大量的hello Netty Server!数据粘在一起构成一个个粘包。

2023-08-29 09:09:24: 服务端读到数据 -> 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: 服务端读到数据 -> r!hello Netty Server!hello Netty Server!hello Netty Ser
  • 1.
  • 2.

2. 原因剖析

在TCP编程中,在服务端与客户端通信时消息都会有固定的消息格式,这种格式我们通常称之为protocol即协议,例如我们常见的应用层协议:HTTP、FTP等。

而上述例子出现粘包的原因本质就是我们服务端与客户端进行通信时,没有确认协议的规范,因为TCP是面向连接、面向流的协议,它会因为各种原因导致完整的数据包被拆封无数个小的数据包进行发送,进而导致接收方收到数据后无法正确的处理数据,出现粘包和拆包:

而出现TCP数据包被拆分的原因大致有3个:

  • socket缓冲区与滑动窗口
  • nagle算法
  • mss

先来说说socket缓冲区和滑动窗口的共同作用,我们都知道TCP是全双工、面向流的协议。这意味发送时必须要保证收发正常,所以TCP就提出了一个滑动窗口机制,即以滑动窗口的大小为单位,让双方基于这个窗口的大小进行数据收发,发送方只有在滑动窗口以内的数据才能被发送,接收方也只有在窗口以内的数据被接收和处理,只有接收方的滑动窗口收到发送方的数据,且处理完成并发送确认信号ACK之后,发送方的窗口才能继续向后移动:

由于TCP是面向流的协议,在此期间双方收发的数据也都会会存放到socket缓冲区中。这意味这连个缓冲区是无法知晓这些数据是否属于同一个数据包的。 同理socket缓冲区也分为发送缓冲区(SO_SNDBUF )和接收缓冲区(SO_RCVBUF),所有socket需要发送的数据也都是存放到socket的缓冲区中然后通过内核函数传到内核协议栈进行数据发送,socket接收缓冲区也是通过操作系统的内核函数将数据拷贝至socket缓冲区。

所以。socket缓冲区和滑动窗口机制共同作用下就会出现以下两种异常情况:

(1) 发送方发送的数据达到了滑动窗口的限制,停止发送,接收方的socket缓冲区拿到这些数据后,直接向应用层传输,因为包不是完整的,从接收方的角度来看,出现了拆包。

(2) 发送方发送多个数据包到接收方缓冲区,因为接收方socket缓冲区无法及时处理,导致真正开始处理时无法知晓数据包的边界,只能一次性将数据包向上传递,导致粘包。

再来说说Nagle算法,考虑到每次发送数据包时都需要为数据加上TCP Header20字节和IP header 20字节,以及还得等待发送方的ACK确认包,这就很可能出现下面这种非常浪费网络资源的情况:

为了1个字节的有用信息去组装10字节的头部信息!

对此,操作系统为了尽可能的利用网络带宽,就提出了Nagle算法,该算法要求所有已发送出去的小数据包(长度小于MSS)必须等到接收方的都回复ack信号之后,然后再将这些小数据段一并打包成一个打包发送,从而尽可能利用带宽及尽可能避免因为大量小的网络包的传输造成网络拥塞。

很明显如果将多个小的数据包合并发送,接收方也很可能因为无法确认数据包的边界而出现粘包或拆包问题:

最后就是mss,也就是Maximum Segement Size的缩写,代表传输一次性可以发送的数据最大长度,如果数据超过MSS的最大值,那么网络数据包就会被拆成多个小包发送,这种情况下也很可能因为零零散散的数据包发送而会出现粘包和拆包问题。

对此我们不妨通过WireShark进行抓包分析,基于服务端端口键入如下指令进行过滤:

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

启动客户端和服务端之后,发现双方交换得出的MSS远大于每次发送的数据大小,所以首先排除分包问题:

查看每次服务端发送的数据,无论大小还是内容都没有缺失,内核缓冲区空间也是充足的,所以原因很明显,因为TCP协议是面向流传输,接收方从内核缓冲区读取时,拿到了过多或者过少的数据导致粘包或拆包。

二、半包粘包的解决对策

1. 几种解决对策简介

其实上述的问题的原因都是因为TCP是面向流的协议,导致了数据包无法被正常切割成一个个正常数据包的流。就以上面的数据包为例,发送的数据为hello Netty Server!,其实我们做到下面这几种分割方式:

  • 如果发送的数据都是以"!"结尾,那我们的分割时就判断收到的流是否包含"!",只有包含时再将数据装成数据包发送。
  • 上述发送的数据长度为19,我们也可以规定发送的数据长度为19字节,一旦收到的数据达到19个字节之后,就组装成一个数据包。
  • 自定义一个协议,要求发送方根据协议要求组装数据包发送,例如要求数据包包含长度length和data两个字段,其中length记录数据包长度,以上述数据为例,这个字段的值为19,而data包含的就是数据内容。

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

先来看看基于分隔符的,可以看到每一个数据末尾都有一个感叹号,所以我们可以通过判断特殊符号完成数据拆包。

代码如下,我们基于DelimiterBasedFrameDecoder完成基于特殊分隔符进行拆包,每个参数对应含义为:

  • 数据包最大长度。
  • 解码时是否去掉分隔符。
  • 分隔符。
ByteBuf delimiter = Unpooled.copiedBuffer("!".getBytes());
 
                        nioSocketChannel.pipeline()
                                .addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,false,delimiter))
                                .addLast(new FirstServerHandler());
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

启动之后可以看到问题也得以解决:

2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
2023-08-29 09:19:44: 服务端读到数据 -> hello Netty Server!
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

3. 基于数据长度的解码器FixedLengthFrameDecoder

同理,我们也可以基于数据长度,对数据包进行分割:

由上文可知,我们发送的数据长度都是19,所以第一种方案是在服务端的pipeline配置一个基于长度拆包的解码器,确保在每19个字节截取一次以确保数据包可以正确读取和解析。 所以我们在pipeline添加一个FixedLengthFrameDecoder,长度设置为19。

nioSocketChannel.pipeline()
                                .addLast(new FixedLengthFrameDecoder(19))
                                .addLast(new FirstServerHandler());
  • 1.
  • 2.
  • 3.

4. 基于协议长度字段的解码器LengthFieldBasedFrameDecoder

最后一种,也是笔者比较推荐的一种长度,即自定义协议,我们在传输过程中,可能数据的长度或者分隔符都无法保证,所以我们可以和客户端协商一下,在传输的数据头部添加一个数据包长度,例如用4字节表示数据包长度。

所以客户端建立连接后写数据的代码就改为:

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

        ByteBuf buffer = ctx.alloc().buffer();
        //4个字节说明数据的长度
        buffer.writeInt(bytes.length);
        //写入数据内容
        buffer.writeBytes(bytes);

        return buffer;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

最终的数据包结构如下图所示:

图片图片

而服务端的处理器则改为使用LengthFieldBasedFrameDecoder,构造方法如下:

public LengthFieldBasedFrameDecoder(
            int maxFrameLength,
            int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip) {
       //.......
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

按照对应参数含义为:

  • maxFrameLength:数据包最大长度,这里我们设置为Integer.MAX_VALUE,等同于不限制。
  • lengthFieldOffset:该数值代表获取描述数据包长度的字段的位置偏移量,以我们的数据包为例,就是0,即从最初始的位置读取长度。
  • lengthFieldLength:描述数据包长度的字段的字节数,以我们的数据包为例就是4字节。
  • lengthAdjustment:要添加到长度字段值的补偿值,这个字段比较有意思,我们还是举个例子说明,以下面这个数据包为例,假如我们需要得到data的数据,而长度记录的值为12字节(head+length+data),为了达到我们的预期即只取10字节的数据,我们就可以基于将这个字段的值设置为-2,将12减去10以得到实际的data数据长度。

对应的我们本次数据包长度记录的值没有错,这里直接直接设置为0,无需调整。

  • initialBytesToStrip:读取时需要跳过数据包几个字节,以我们的数据包为例就说4,代表我们要跳过4字节的length字段,只要data的数据,对应的我们也给出下面这个构造方法:

于是我们就有了下面这样一个构造的解码器,再次进行压测后数据都是可以正常解析处理的:

nioSocketChannel.pipeline()
                                .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4))
                                .addLast(new FirstServerHandler());
  • 1.
  • 2.
  • 3.

5. 更多关于Netty内置解码器

设计者也在注释上为我们提供更多的使用案例,先来看看第一个示例,该数据包长度字段2字节,偏移量为0。假如我们希望读整个数据包,那么参数设置方式为:

  • lengthFieldOffset即偏移量设置为0,即长度字段无需偏移就在数据包高位。
  • lengthFieldLength为2,即读取2字节的数据,即可获得数据包长度。
  • lengthAdjustment 为0,代表长度字段描述的数据就是后续数据的长度,无需调整。
  • initialBytesToStrip 为0,即读取时从数据包最开始位置读取并加上长度字段里描述的长度的数据,无需跳过。
* <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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

再来看看示例2,数据包和上文相同,只不过希望读取的数据不包含length字段,所以参数设置为:

  • lengthFieldOffset即偏移量设置为0,即长度字段无需偏移就在数据包高位。
  • lengthFieldLength为2,即读取2字节的数据,即可获得数据包长度。
  • lengthAdjustment 为0,代表长度字段描述的数据就是后续数据的长度,无需调整。
  • initialBytesToStrip 为2,即读取时从数据包起始位置开始,跳过2字节数据,即跳过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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

再来看看情况3,2字节长度描述长度,只不过该长度包含了描述长度的字段长度,即length的值为length字段长度2+后续HELLO, WORLD字符串长度为14。如果我们希望获取一个完整的数据包,那么参数就需要设置为:

  • lengthFieldOffset即偏移量设置为0,即长度字段无需偏移就在数据包高位。
  • lengthFieldLength为2,即读取2字节的数据,即可获得数据包长度。
  • lengthAdjustment 为-2,代表长度字段描述的是整个包的长度,需要减去length字段的长度。
  • initialBytesToStrip 为0,即读取时从数据包最开始位置读取并加上长度字段里描述的长度的数据,无需跳过。
* 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

示例4需要跳过header字段读取到长度字段,最后需要得到一个包含所有部分的数据包,所以参数如下:

  • lengthFieldOffset即偏移量设置为2,即跳过Header 。
  • lengthFieldLength为3,即读取3字节的数据,即可获得数据包长度。
  • lengthAdjustment 为0,代表长度字段描述的是就是后续数据的长度,无需调整。
  • initialBytesToStrip 为0,即读取时从数据包最开始位置读取并加上长度字段里描述的长度的数据,无需跳过。
* 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

示例5情况比较特殊,length描述后文数据的长度,却不包含后文header的长度,若我们希望获取到所有部分的数据包,则参数需要设置为:

  • lengthFieldOffset即偏移量设置为0,即无需偏移,长度就在数据包高位。
  • lengthFieldLength为3,即读取3字节的数据,即可获得数据包长度。
  • lengthAdjustment 为2,即代表length字段仅仅记录的Actual Content的长度,length字段后面还有一个header的长度需要计算,故设置为2,意味实际长度要+2。
  • initialBytesToStrip 为0,即读取时从数据包最开始位置读取并加上长度字段里描述的长度的数据,无需跳过。
* <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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

示例6,长度在hdr1后面,并且最终读取的数据是hdr2和Actual Content。参数设置为:

  • lengthFieldOffset即偏移量设置为1,即跳过HDR1。
  • lengthFieldLength为2,即读取2字节的数据,即可获得数据包长度。
  • lengthAdjustment 为1,即代表length字段仅仅记录的Actual Content的长度,length字段后面还有一个HDR2 的长度需要计算,故设置为1,意味实际长度要+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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

示例7即可Length记录的是整个包的长度,为了拿到HDR2和Actual Content的数据,对应参数设置如下:

  • lengthFieldOffset即偏移量设置为1,即跳过HDR1。
  • lengthFieldLength为2,即读取2字节的数据,即可获得数据包长度。
  • lengthAdjustment 为-3,即代表减去HDR1和 LEN的字段长度。
  • 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

三、小结

以上便是笔者对于Netty如何解决半包与粘包问题的源码解析与实践的全部内容,希望对你有帮助。

责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2021-10-08 09:38:57

NettyChannelHand架构

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粘包通信协议

2020-01-06 15:23:41

NettyTCP粘包

2024-12-19 11:00:00

TCP网络通信粘包

2021-03-09 22:30:47

TCP拆包协议

2022-04-28 08:38:09

TCP协议解码器

2024-08-16 21:47:18

2024-07-05 08:27:07

2020-12-23 07:53:01

TCP通信Netty

2021-04-07 13:52:57

GoogleLyra编译器

2023-07-26 16:31:09

Windows 10Windows 11微软

2022-10-10 10:38:22

FedoraopenSUSE视频编解码

2020-03-10 08:27:24

TCP粘包网络协议

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媒体编解码器
点赞
收藏

51CTO技术栈公众号