Netty学习(二)—拆包粘包问题

本文介绍了TCP协议中拆包和粘包的现象及其原因,包括TCP粘包的发生与TCP拆包的发生。针对这些问题,提出了三种解决策略:定长、特殊分隔符和数据长度标识。通过Netty的LineBasedFrameDecoder、DelimiterBasedFrameDecoder和FixedLengthFrameDecoder解码器,详细展示了如何在实际应用中解决TCP粘包问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Netty学习(二)—拆包粘包问题

无论是服务端还是客户端在进行数据发送收取的时候需要考虑TCP底层的粘包/拆包机制,因为如果不进行处理会造成收取的数据和预想的不一致;

个人主页:tuzhenyu’s page
原文地址:Netty学习(二)—拆包粘包问题

(0) 拆包粘包的原因

TCP粘包发生的原因

  • 粘包现象出现的根本原因是TCP协议是一个面向数据流的通信协议,数据流中没有分界线;在发送或接收时并不会考虑业务数据的具体含义,而是会根据发送缓冲区或者接收缓冲区的情况进行数据包的划分;

  • 为了提高发送效率,发送端会采用Nagle算法优化发送,将时间间隔较短的多次小数据包发送合并成一个大数据包发送,接收端不能辨别出合并情况就出现了粘包情况;

  • 接收端的应用程序未能及时的读取接收缓冲区的数据,多次发送的数据堆积在套接字缓冲区,当应用程序读取时无法分辨就出现了粘包现象;

TCP拆包发生的原因

  • 拆包现象出现的根本原因在于数据包的发送受到链路MTU的限制,也就是链路能传输的最大数据包的大小的限制;

  • 当发送的数据包大小大于MTU-20(TCP报文头部)-20(IP报文头部)时就会出现TCP分组,将大数据包拆分成多个小数据包分别发送;

(1) 拆包粘包的解决策略

  • 数据定长,发送数据报文的长度小于MSS(最大可发送长度)如果不够则用空格补齐,接收时也进行定长接收,能解决粘包问题,不能解决拆包问题;

  • 特殊分割符,在数据结尾用回车换行符等分割,接收到的数据也以特殊字符进行分割,能解决粘包问题,不能解决拆包问题;

  • 将数据长度和数据一起发送,将数据分为消息头和消息体两部分,消息头中包含数据总长度,能解决拆包和粘包问题;

(2) 粘包的示例

  • 客户连续端发送数据到服务端
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    byte[] req = null;
    ByteBuf buffer = null;
    for (int i=0;i<100;i++){
        req = ("this is No."+i+" server sent the message ").getBytes();
        buffer = Unpooled.buffer(req.length);
        buffer.writeBytes(req);
        ctx.writeAndFlush(buffer);
    }
}
  • 服务端接收数据
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf buf = (ByteBuf)msg;
    byte[] bytes = new byte[buf.readableBytes()];
    buf.readBytes(bytes);
    System.out.println("receive the bytes: "+new String(bytes,"UTF-8"));
}
  • 如果没有出现粘包现象则服务端会接收100次数据,输出100行数据;但是真实的情况服务端只接收了四次数据,出现了粘包现象,具体的是客户端发送粘包还是服务端接粘包不能判断;
receive the bytes: this is No.0 server sent the message this is No.1 server sent the message this is ...

receive the bytes: No.27 server sent the message this is No.28 server sent the message this is...

receive the bytes: s No.54 server sent the message this is No.55 server sent the message this is...

receive the bytes:  is No.81 server sent the message this is No.82 server sent the message this is...

(3) 分包的示例

  • 客户端发送大数据包到服务端
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    byte[] req = null;
    ByteBuf buffer = null;
    StringBuilder sb = new StringBuilder();
    for (int i=0;i<100;i++){
        sb.append("abcdefghijklmnopqrstuvwxyz");
    }
    req = sb.toString().getBytes();
    buffer = Unpooled.buffer(req.length);
    buffer.writeBytes(req);
    ctx.writeAndFlush(buffer);
}
  • 服务端接收数据,每接一次输出一行
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf buf = (ByteBuf)msg;
    byte[] bytes = new byte[buf.readableBytes()];
    buf.readBytes(bytes);
    System.out.println("receive the bytes: "+new String(bytes,"UTF-8"));
}
  • 按照预想的效果如果没有分包则客户端发送一次,服务端接收输出一次;实际上由于发送的数据包过大则客户端分多次进行发送,服务端也接收输出了多次;
receive the bytes: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz...

receive the bytes: klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh...

receive the bytes: uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst...

(4) LineBasedFrameDecoder解决粘包问题

  • LineBasedFrameDecoder解决粘包问题主要是通过对输入流以换行符“\n”对数据流进行分割,每次读取时只读取换行符之前的数据;LineBasedFrameDecoder不能解决拆包问题;

  • 服务端发送数据,在每次发送结尾处添加换行符

ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup,workGroup).channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new ServerHandler());
                        }
                    });
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("the client says: "+ body);

        byte[] req = null;
        ByteBuf buffer = null;
        for (int i=0;i<100;i++){
            req = ("this is No."+i+" server sent the message\n").getBytes();
            buffer = Unpooled.buffer(req.length);
            buffer.writeBytes(req);
            ctx.writeAndFlush(buffer);
        }
    }
  • 客户端接收数据,添加LineBasedFrameDecoder对数据流进行分割
Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            socketChannel.pipeline().addLast(new ClientHandler());
                        }
                    });
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("the server says: "+ body);
    }
  • 接收端添加LineBasedFrameDecoder解码器后对输入数据流进行分割,解决了粘包的情况
the server says: this is No.0 server sent the message

the server says: this is No.1 server sent the message

the server says: this is No.2 server sent the message

the server says: this is No.3 server sent the message

the server says: this is No.4 server sent the message

the server says: this is No.5 server sent the message

the server says: this is No.6 server sent the message

the server says: this is No.7 server sent the message

the server says: this is No.8 server sent the message

the server says: this is No.9 server sent the message

the server says: this is No.10 server sent the message

(4) DelimiterBasedFrameDecoder解决粘包问题

  • DelimiterBasedFrameDecoder特殊字符解码器和LineBasedFrameDecoder换行符解码器类似,都是在读取输入流时以特殊字符为分割符读取数据;DelimiterBasedFrameDecoder特殊字符解码器不能解决拆包问题,只能解决粘包问题;

  • 服务端发送数据到客户端

byte[] req = null;
ByteBuf buffer = null;
for (int i=0;i<100;i++){
    req = ("this is No."+i+" server sent the message$").getBytes();
    buffer = Unpooled.buffer(req.length);
    buffer.writeBytes(req);
    ctx.writeAndFlush(buffer);
}
  • 客户端添加DelimiterBasedFrameDecoder解码器按照特殊字符对输入数据流进行分割;
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
                                    Unpooled.copiedBuffer("$".getBytes())));
                            socketChannel.pipeline().addLast(new ClientHandler());
                        }
                    });
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("the server says: "+ body);
    }

(5) FixedLengthFrameDecoder定长解码器解决粘包问题

  • FixedLengthFrameDecoder定长解码器是提前设定消息数据的长度,接收端按照设定的长度进行数据流的分割;FixedLengthFrameDecoder只能解决粘包问题不能解决拆包问题;

  • 客户端接收数据,添加定长解码器分割数据流;

Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(("hello world").getBytes().length));

                            socketChannel.pipeline().addLast(new ClientHandler());
                        }
                    });
  • 服务端发送特定长度的数据,如果数据长度不够则使用空格填充保证数据包长度的一定;
for (int i=0;i<100;i++){
    req = ("hello world").getBytes();
    buffer = Unpooled.buffer(req.length);
    buffer.writeBytes(req);
    ctx.writeAndFlush(buffer);
}

总结

  • 本文主要总结了粘包/拆包问题出现的原因和解决办法,以及Netty利用编码解码器对拆包/粘包问题的解决办法,并以LineBasedFrameDecoder,DelimiterBasedFrameDecoder和FixedLengthBasedFrameDecoder解码器为例解决TCP粘包问题;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值