Netty-TCP粘包和拆包原理

TCP粘包和拆包原理

  • TCP粘包和拆包基本介绍

    • TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务端)都要有——成对的Socket,因此发送端为了将多个发给接收端的包,更有效的发给对象,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。

    • 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。

      • 在这里插入图片描述

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

        • 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
        • 服务端一次就接收到了一个数据包,这个数据包包含了D1和D2的数据,称之为粘包
        • 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2-1包的部分内容,第二次读取到了D2-2包的剩余内容,这称之为拆包
        • 服务器分两次读取到了数据包,第一次读取到了D1包的部分内容D1-1,第二次读取到了D1包的剩余内容D1-2和完整的D2包
    • TCP粘包和拆包产生的原因

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

      • 在这里插入图片描述

      • 详细的来说,造成粘包和拆包的主要原因有三个

        • 应用程序write写入的字节大小大于套接口发送缓冲大小
        • 进行MSS大小的TCP分段
        • 以太网帧的payload大于MTU进行IP分片
    • 粘包和拆包的解决办法

      • 由于底层的TCP无法理解上层的业务数据(也就是操作系统是无法解析应用发来的消息的),所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下
        • 消息长度固定,累计读取到长度和为定长的LEN的报文后,就认为读取到一个完整的信息
        • 将回车换行符作为消息结束符
        • 将特殊分隔符作为消息结束符,回车换行符就是一种特殊的结束分隔符
        • 通过在消息头中定义长度字段来标识消息的总长度
    • Netty中的粘包和拆包解决方案

      • 针对上一小节描述的粘包和拆包的解决方案,对于拆包问题比较简单,用户可以自己定义自己的编码器进行处理,Netty并没有提供相应的组件。对于粘包的问题,由于拆包比较复杂,代码比较处理比较繁琐,Netty提供了4种解码器来解决,分别如下:
      1. 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小
      2. 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分
      3. 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分
      4. 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度
    • TCP粘包和拆包现象实例

      • TCPPackageClientHandler

        • package com.jl.java.web.tcppackage;
          
          import io.netty.buffer.ByteBuf;
          import io.netty.buffer.Unpooled;
          import io.netty.channel.ChannelHandlerContext;
          import io.netty.channel.SimpleChannelInboundHandler;
          import io.netty.util.CharsetUtil;
          
          /**
           * @author jiangl
           * @version 1.0
           * @date 2021/5/27 21:58
           */
          public class TCPPackageClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
          
              @Override
              public void channelActive(ChannelHandlerContext ctx) throws Exception {
                  //客户端发送10条数据 hello ,server
                  for(int i=0;i<10;i++){
                      ByteBuf buf = Unpooled.copiedBuffer("hello,server" + i, CharsetUtil.UTF_8);
                      ctx.writeAndFlush(buf);
                  }
              }
          
              @Override
              protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                  byte[] buf = new byte[msg.readableBytes()];
                  msg.readBytes(buf);
                  System.out.println(new String(buf));
              }
          }
          
      • TCPPackageServerHandler

        • package com.jl.java.web.tcppackage;
          
          import io.netty.buffer.ByteBuf;
          import io.netty.buffer.Unpooled;
          import io.netty.channel.ChannelHandlerContext;
          import io.netty.channel.SimpleChannelInboundHandler;
          import io.netty.util.CharsetUtil;
          
          import java.util.UUID;
          
          /**
           * @author jiangl
           * @version 1.0
           * @date 2021/5/27 22:00
           */
          public class TCPPackageServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
              private int count =0;
              @Override
              protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                  byte[] array = new byte[msg.readableBytes()];
                  msg.readBytes(array);
                  System.out.println("服务器端接收到数据:"+new String(array));
                  System.out.println("服务器端接收到消息量=" + (++this.count));
          
                  //服务器回送数据给客户端,回送一个随机id
                  ByteBuf responseBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString()+"\n", CharsetUtil.UTF_8);
                  ctx.writeAndFlush(responseBuf);
              }
          }
          
  • TCP粘包和拆包的解决方案

    • 使用自定义协议+编解码器来解决
    • 关键就是要解决 服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免了TCP粘包、拆包
    • 具体实例
      • 要求客户端发送5个Message对象,客户端每次发送一个Message对象
      • 服务端每次接受一个Message,分5次进行解码,每读取到一个Message,会回复一个Message对象给客户端
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty中的TCP粘包拆包问题是由于底层的TCP协议无法理解上层的业务数据而导致的。为了解决这个问题,Netty提供了几种解决方案。其中,常用的解决方案有四种[1]: 1. 固定长度的拆包器(FixedLengthFrameDecoder):将每个应用层数据包拆分成固定长度的大小。这种拆包器适用于应用层数据包长度固定的情况。 2. 行拆包器(LineBasedFrameDecoder):将每个应用层数据包以换行符作为分隔符进行分割拆分。这种拆包器适用于应用层数据包以换行符作为结束符的情况。 3. 分隔符拆包器(DelimiterBasedFrameDecoder):将每个应用层数据包通过自定义的分隔符进行分割拆分。这种拆包器适用于应用层数据包以特定分隔符作为结束标志的情况。 4. 基于数据包长度的拆包器(LengthFieldBasedFrameDecoder):将应用层数据包的长度作为接收端应用层数据包的拆分依据。根据应用层协议中包含的数据包长度进行拆包。这种拆包器适用于应用层协议中包含数据包长度的情况。 除了使用这些拆包器,还可以根据业界主流协议的解决方案来解决粘包拆包问题[3]: 1. 消息长度固定:累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息。 2. 使用特殊的分隔符:将换行符或其他特殊的分隔符作为消息的结束标志。 3. 在消息头中定义长度字段:通过在消息头中定义长度字段来标识消息的总长度。 综上所述,Netty提供了多种解决方案来解决TCP粘包拆包问题,可以根据具体的业务需求选择合适的解决方案[1][3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值