Netty中的粘包拆包

一、问题描述:

1. 什么是粘包、拆包

粘包和拆包是TCP网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息 的时候,都需要考虑TCP底层的粘包/拆包机制。

TCP传输会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分 成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包 问题。

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

  1. 服务端分两次读取到了两个独立的数据包,分别是P1和P2,没有粘包和拆包;
  2. 服务端一次接收到了两个数据包,P1和P2粘合在一起,被称为TCP粘包;
  3. 如果P2的数据包比较大, 服务端分两次读取到了两个数据包,第一次读取到了完整的P1包和P2包 的部分内容,第二次读取到了P2包的剩余内容,这被称为TCP拆包
  4. 如果P1, P2的数据包都很大, 服务端分多次才能将P1和P2包接收完全,期间发生多次拆包

TCP粘包和拆包产生的原因:

数据从发送方到接收方需要经过操作系统的缓冲区,而造成粘包和拆包的主要原因就在这个缓冲区 上。粘包可以理解为缓冲区数据堆积,导致多个请求数据粘在一起,而拆包可以理解为发送的数据大于 缓冲区,进行拆分处理。

2. 粘包拆包演示

首先应该了解一次socket连接 对应 一个Channel,而一个Channel 对应一个 Handler;而不同的socket连接对应不同的handler,所以拆包和粘包只针对同一个socket连接发送的数据。

  • 拆包演示

    1. NettyServer

      public class NettyServer {
          public static void main(String[] args) throws InterruptedException {
              //1. 创建bossGroup线程组: 处理网络事件--连接事件
              EventLoopGroup bossGroup = new NioEventLoopGroup(1);
              //2. 创建workerGroup线程组: 处理网络事件--读写事件 2*处理器线程数
              EventLoopGroup workerGroup = new NioEventLoopGroup();
              //3. 创建服务端启动助手
              ServerBootstrap serverBootstrap = new ServerBootstrap();
              //4. 设置bossGroup线程组和workerGroup线程组
              serverBootstrap.group(bossGroup, workerGroup)
                      .channel(NioServerSocketChannel.class) //5. 设置服务端通道实现为NIO
                      .option(ChannelOption.SO_BACKLOG, 128)//6. 参数设置
                      .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)//6. 参数设置
                      .childHandler(new ChannelInitializer<SocketChannel>() { //7. 创建一个通道初始化对象
                          @Override
                          protected void initChannel(SocketChannel ch) throws Exception {
                              //8. 向pipeline中添加自定义业务处理handler
                              ch.pipeline().addLast(new NettyServerHandler());
                          }
                      });
              //9. 启动服务端并绑定端口,同时将异步改为同步
              ChannelFuture future = serverBootstrap.bind(9999);
              future.addListener(new ChannelFutureListener() {
                  @Override
                  public void operationComplete(ChannelFuture channelFuture) throws Exception {
                      if (channelFuture.isSuccess()){
                          System.out.println("服务端启动成功.");
                      }else {
                          System.out.println("服务端启动失败.");
                      }
                  }
              });
              //10. 关闭通道(并不是真正意义上关闭,而是监听通道关闭的状态)和关闭连接池
              future.channel().closeFuture().sync();
              bossGroup.shutdownGracefully();
              workerGroup.shutdownGracefully();
          }
      }
      
      
    2. NettyServerHandler,修改ChannelRead函数,获取当前读取的到的消息长度 和 当前读取次数

      public class NettyServerHandler implements ChannelInboundHandler {
          public int count = 0;
      
          /**
           * 通道读取事件
           *
           * @param ctx
           * @param msg
           * @throws Exception
           */
          @Override
          public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
              ByteBuf byteBuf = (ByteBuf) msg;
              System.out.println("长度是:" + byteBuf.readableBytes());
              System.out.println("读取次数 = " + (++count));
          }
      
      
          /**
           * 通道读取完毕事件
           *
           * @param ctx
           * @throws Exception
           */
          @Override
          public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          /**
           * 通道异常事件
           *
           * @param ctx
           * @param cause
           * @throws Exception
           */
          @Override
          public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
              cause.printStackTrace();
              ctx.close();
          }
      
          @Override
          public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void channelActive(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void channelInactive(ChannelHandlerContext ctx) throws Exception {
      
          }
      
      
          @Override
          public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
      
          }
      
          @Override
          public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
      
          }
      
      
      }
      
    3. NettyClient

      public class NettyClient {
          public static void main(String[] args) throws InterruptedException {
              //1. 创建线程组
              EventLoopGroup group = new NioEventLoopGroup();
              //2. 创建客户端启动助手
              Bootstrap bootstrap = new Bootstrap();
              //3. 设置线程组
              bootstrap.group(group)
                      .channel(NioSocketChannel.class)//4. 设置客户端通道实现为NIO
                      .handler(new ChannelInitializer<SocketChannel>() { //5. 创建一个通道初始化对象
                          @Override
                          protected void initChannel(SocketChannel ch) throws Exception {
                              //6. 向pipeline中添加自定义业务处理handler
                              ch.pipeline().addLast(new NettyClientHandler());
                          }
                      });
              //7. 启动客户端,等待连接服务端,同时将异步改为同步
              ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9999).sync();
              //8. 关闭通道和关闭连接池
              channelFuture.channel().closeFuture().sync();
              group.shutdownGracefully();
          }
      }
      
    4. NettyClientHandler,在通道就绪事件channelActive发送长包数据

      public class NettyClientHandler implements ChannelInboundHandler {
      
          /**
           * 通道就绪事件
           *
           * @param ctx
           * @throws Exception
           */
          @Override
          public void channelActive(ChannelHandlerContext ctx) throws Exception {
              //一次发送102400
              char[] chars = new char[102400];
              Arrays.fill(chars, 0, 102398, '1');
              chars[102399] = '\n';
              ctx.writeAndFlush(Unpooled.copiedBuffer(chars, CharsetUtil.UTF_8));
      
      
          }
      
          /**
           * 通道读就绪事件
           *
           * @param ctx
           * @param msg
           * @throws Exception
           */
          @Override
          public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
              ByteBuf byteBuf = (ByteBuf) msg;
              System.out.println("服务端发送的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
          }
      
      
          @Override
          public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
      
          }
      
      
          @Override
          public void channelInactive(ChannelHandlerContext ctx) throws Exception {
      
          }
      
      
          @Override
          public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
      
          }
      
          @Override
          public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
      
          }
      
          @Override
          public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
      
          }
      }
      
    5. 测试结果:

      请添加图片描述

      一次发送大数据量的包出现了拆分包的情况。

  • 粘包演示

    1. NettyServer 和 NettyClient不做改变

    2. NettyServerHandler 修改通道读取事件channelRead,显示每次接收的信息

          @Override
          public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
              ByteBuf byteBuf = (ByteBuf) msg;
              System.out.println("长度是:" + byteBuf.readableBytes());
              System.out.println("客户端发送过来的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
              System.out.println("读取次数:"+(++count));
          }
      
    3. NettyClientHandler修改通道就绪事件channelActive,每次发送一个短包数据

          @Override
          public void channelActive(ChannelHandlerContext ctx) throws Exception {
              for (int i = 0; i < 2; i++) {
                  ctx.writeAndFlush(Unpooled.copiedBuffer("我是Netty客户端"+i,
                          CharsetUtil.UTF_8));
              }
          }
      
    4. 测试结果:

      请添加图片描述

    多次发送短包数据出现了粘包现象。


二、解决思路:

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个 问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下:

  • 消息长度固定,累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息
  • 将换行符作为消息结束符
  • 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符
  • 通过在消息头中定义长度字段来标识消息的总长度

三、解决方案:

  1. Netty提供了4种解码器来解决,分别如下:

    • 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度 的大小

    • 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割 拆分

    • 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔 符,进行分割拆分

    • 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为 接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度

  2. 自定义分隔符,修改代码:

    • 在NettyClientHandler的通道就绪事件channelActive中发送的数据以"$"分隔

    • 在NettyServer的pipeline中添加分隔符拆包器 DelimiterBasedFrameDecoder:

      ByteBuf byteBuf = Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
      ch.pipeline().addLast(new DelimiterBasedFrameDecoder(102400, byteBuf));
      
  3. 测试结果:

    • 长包数据tcp会拆包,加了分隔符拆包器 DelimiterBasedFrameDecoder后:

      请添加图片描述

    • 短包数据tcp会粘包,加了分隔符拆包器 DelimiterBasedFrameDecoder后:

      请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值