Netty粘包拆包问题解决

什么是粘包拆包?

假设客户端分别发送两个数据包 D1D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是 D1D2,没有粘包和拆包。
  1. 服务端一次接收到了两个数据包, D1D2粘合在一起,称为TCP粘包。
  2. 服务端第一次读到完整的D1包和D2包的部分,第二次读到D2剩余的部分。称为TCP拆包。
  3. 服务端第一次读取到D1包的部分内容,第二次读取到D1包的剩余部分和D2的全部。

产生的原因

  1. 应用程序 write写入的字节大小大于套接口发送缓冲区大小。
  2. 进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
  3. 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
    在这里插入图片描述

解决方法

1. 短连接

发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低。
测试步骤:
1. 导入依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>

2. 服务端测试代码

public class TestServer {
    static final Logger log = LoggerFactory.getLogger(TestServer.class);
    public static void main(String[] args) {
        new ServerBootstrap()
            .group(new NioEventLoopGroup())
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    log.debug("conneted...");
                    // 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));

                    //设置固定分隔符
//                        ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

                    //固定长度
//                        ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            System.out.println(msg);
                            ctx.fireChannelRead(msg);
                            System.out.println(msg);
                        }
                    });

                }
            })
            .bind(new InetSocketAddress("localhost",8200));
    }
}

LengthFieldBasedFrameDecoder:参数含义:
maxFrameLength:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。

lengthFieldOffset:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。

lengthFieldLength:长度域字节数。用几个字节来表示数据长度。

lengthAdjustment:数据长度修正。因为长度域指定的长度可以使header+body的整个长度,也可以只是body的长度。如果表示header+body的整个长度,那么我们需要修正数据长度。

initialBytesToStrip:跳过的字节数。如果你需要接收header+body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。

3. 客户端测试代码:

public class ShortConnect {
    /**
     * 短连接
     */
    static final Logger log = LoggerFactory.getLogger(ShortConnect.class);
    public static void main(String[] args){
        for (int i = 0; i < 10; i++) {
            send();
        }
    }
    public static void send(){
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(worker)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        log.debug("conneted...");
                        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                super.channelActive(ctx);
                                log.debug("sending...");
                                ByteBuf buffer = ctx.alloc().buffer();
                                buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                                ctx.writeAndFlush(buffer);
                                // 发完即关
                                ctx.close();
                            }
                        });
                    }
                });
            ChannelFuture channelFuture = bootstrap.connect("localhost",8200).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e){
            log.error("client error");
        } finally {
            worker.shutdownGracefully();
        }
    }
}

2. 固定长度

每一条消息采用固定长度。
缺点是,数据包的大小不好把握

  • 长度定的太大,浪费
  • 长度定的太小,对某些数据包又显得不够

修改代码

        @Override
         public void channelActive(ChannelHandlerContext ctx) throws Exception {
             log.debug("sending...");
             // 发送内容随机的数据包
             Random r = new Random();
             char c = 'a';
             ByteBuf buffer = ctx.alloc().buffer();
             for (int i = 0; i < 10; i++) {
                 byte[] bytes = new byte[8];
                 for (int j = 0; j < r.nextInt(8); j++) {
                     bytes[j] = (byte) c;
                 }
                 c++;
                 //每次发送固定长度的数据包
                 buffer.writeBytes(bytes);
             }
             ctx.writeAndFlush(buffer);
         }

3. 固定分隔符

处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误
修改代码

      @Override
      public void channelActive(ChannelHandlerContext ctx) throws Exception {
          log.debug("sending...");
          // 发送内容随机的数据包
          Random r = new Random();
          char c = 'a';
          ByteBuf buffer = ctx.alloc().buffer();
          for (int i = 0; i < 10; i++) {
              byte[] bytes = new byte[8];
              for (int j = 0; j < r.nextInt(8); j++) {
                  bytes[j] = (byte) c;
              }
              c++;
              buffer.writeBytes(bytes);
          }
          ctx.writeAndFlush(buffer);
      }

4. 预设长度

在发送消息前,先约定用定长字节表示接下来数据的长度。
服务端 pipeline中 添加

ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));

LengthFieldBasedFrameDecoder:参数含义:
maxFrameLength:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。

lengthFieldOffset:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。

lengthFieldLength:长度域字节数。用几个字节来表示数据长度。

lengthAdjustment:数据长度修正。因为长度域指定的长度可以是header+body的整个长度,也可以只是body的长度。如果表示header+body的整个长度,那么我们需要修正数据长度。

initialBytesToStrip:跳过的字节数。如果你需要接收header+body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。

客户端代码:

         @Override
         public void channelActive(ChannelHandlerContext ctx) throws Exception {
             log.debug("sending...");
             // 发送内容随机的数据包
             Random r = new Random();
             char c = 'a';
             ByteBuf buffer = ctx.alloc().buffer();
             for (int i = 0; i < 10; i++) {
                 int length = r.nextInt(16);
                 //先写入长度
                 buffer.writeByte(length);
                 //再写入数据
                 for (int j = 0; j < length; j++) {
                     buffer.writeByte((byte) c);
                 }
                 c++;
             }
             ctx.writeAndFlush(buffer);
         }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值