TCP粘包和拆包

粘包和拆包

TCP的粘包和拆包问题往往出现在基于TCP协议的通讯中

什么是粘包?

在学习粘包之前,先纠正一下读音,很多视频教程中将“粘”读作“nián”。经过调研,个人更倾向于读“zhān bāo”。

如果在百度百科上搜索“粘包”,对应的读音便是“zhān bāo”,语义解释为:网络技术术语。指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

TCP是面向字节流的协议,就是没有界限的一串数据,本没有“包”的概念,“粘包”和“拆包”一说是为了有助于形象地理解这两种现象。

粘包拆包发生场景

因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。

如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。

如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。

关于粘包和拆包可以参考下图的几种情况:

上图中演示了以下几种情况:

* 正常的理想情况,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包;
* 粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
* 拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
* 拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。

演示粘包出现的情况

TCP服务器代码,只展示核心代码,向客户端连接循环发送100个数据

     while (count < 100)
     {
        string msg = "我是一个整包";
        byte[] bytes = Encoding.UTF8.GetBytes(msg);
        stream.Write(bytes,0,bytes.Length);
        count++;
    }

在客户端只接受到了3个数据(我的测试,可能有所不同),并且数据中有许多的`?`​,这些问号的出现是因为一个汉字占用3个字节,传输时被进行了错误的分割,如`1 + 2、2+1`​,导致数据解析错误。

如果将上例的`msg`​变量修改为更大的内容,可以看到拆包的效果

处理粘包问题

前置任务

一. 一般情况下,信息的发送都应该是有限制的,比如微信发送文件大小就有要求,我们这里举例最多允许发送`int个字节(2^32)字节`​长度的数据
  
二. 一个汉字占用3个字节 我们将会在发送的数据包前面添加一个包头,用于标识该数据的长度,如下

byte[] data = Encoding.UTF8.GetBytes("窗前明月光,疑是地上床,举头望明月,低头思故乡。窗前明月光,疑是地上床,举头望明月,低头思故乡。窗前明月光,疑是地上床,举头望明月,低头思故乡。窗前明月光,疑是地上床,举头望明月,低头思故乡。");  // 待发送的数据
int length = data.Length;  // 288

这个代码要发送的数据为288,我们发送数据包时将会这个288放在数据包的头部,因为int占用4个字节,因此该数据包应该是像下面这个样子,就像是256进制,第四位最大值位255,到256就要向第三位进1

      二进制
      [00000000, 00000000, 00000001, 00100001, 数据位...]
      十进制
      [0, 0, 1, 33, 数据位....]


三. 大端和小端

  1. 小端就是低位对应低地址,高位对应高地址。
  2. 大端就是低位对应高地址,高位对应低地址
  3. C#模式是小端模式,也就是小值在前,大值在后,比如`[1,2,3,4]`​其实是`[4,3,2,1]`​
  4. 也就是说,上例中我们的`288`​发送过去之后其实是`[00100001, 00000001, 00000000, 00000000]`​,分别是所谓的个位、十位、百位、千位,其实不是很合适,能理解其意即可
4. 转换问题,第三条中的`[0,0,1,33]`​其实是`[33,1,0,0]`​,满256进一的256进制,因此转换为10进制为`33 * 256 ^ 0 + 1 * 256 ^ 1 + 0 * 256 ^ 2 + 0 * 256 ^ 3`​
  

分隔法

加分割符处理半包:让上个包的后半部分加上前半部分。多次发送过来的消息中使用分隔符分割后,第一个数据可能包含上一个包的数据,最后一个数据可能包含下一个包的数据,第一个包拼接上一个包的最后一个数据后一定是完整的数据

粘包就是将多个包进行了粘连,并且不能进行正确的分割,我们只需要将其正确分割即可,如下

    // 保存上一个半包内容(没有的话,就是""空字符串)
    StringBuilder lastPacket = new StringBuilder();
    
    void OnMessage(){
      // 先获取发送过来的数据
      byte[] buffer = new byte[1024];
      int length = stream.Read(buffer, 0, buffer.Length);
      string msg = Encoding.UTF8.GetString(buffer, 0, length);
      // 使用 "|" 进行拆分,约定每个包以 | 结尾
      string[] msgs = msg.Split('|');
      // 将第一个数据拼接到上一个半包,拼接后一定是完整的数据
      lastPacket.Append(msgs[0]);
      Console.WriteLine(lastPacket.ToString());
    
      // 从第二个(索引是1)开始遍历
      for (int i = 1; i < msgs.Length - 1; i++)
      {
          // 一定是个整包(除了第一个和最后一个)
          count++;
          Console.WriteLine(msgs[i]);
      }
    
      // 清空lastPacket后记录最后一个包的内容,也就是当作下一个包的开始部分
      lastPacket.Clear();
      lastPacket.Append(msgs[msgs.Length - 1]);
    } 

加入包头,进行拆包处理

发送数据时将数据的前4位当作包头,用于记录该数据包的数据长度,然后我们再从数据包中截取对应长度的数据

发送时:

    // 1、将要发送的数据转换为字节数组
    byte[] bodyByte = Encoding.UTF8.GetBytes(body);
    // 2、创建内存流用于写入数据包
    MemoryStream ms = new MemoryStream();
    BinaryWriter bw = new BinaryWriter(ms);
    // 3、写入包头(数据的长度,int类型,占4个字节,也就是4位)
    bw.Write(bodyByte.Length);
    // 4、写入包体
    bw.Write(bodyByte);
    // 5、发送给客户端
    clientStream.Write(ms.ToArray(), 0, (int)ms.Length);

接收时:

    // 省略部分代码,这块代码应该包裹在一个方法中
    Task.Run(() =>
    {
        while (true)
        {
            byte[] body = new byte[1024];
            // 数组总长度
            int length = ns.Read(body, 0, body.Length);
    
            if (lastPack.Count > 0)
            {
                // 5、如果之前有半包,则先把半包读完
                lastPack.AddRange(body.Take(lagePackRemaining));
                count++;
                Console.WriteLine(count + ":" + Encoding.UTF8.GetString(lastPack.ToArray(), 0, lastPack.Count));
                lastPack.Clear();
                UnPack(body, length, lagePackRemaining);
            }
            else
            {
                // 1、第一个肯定是没有半包的
                UnPack(body, length, 0);
            }
        }
    });
    
    /// <summary>
    /// 拆包的方法
    /// </summary>
    /// <param name="body">字节数组</param>
    /// <param name="length">字节数组中的有效位</param>
    /// <param name="startIndex">从哪一位开始读取</param>
    private void UnPack(byte[] body, int length, int startIndex)
    {
        // 2、计算单个包的体积
        int singleLength = 0;
        for (int i = startIndex; i < startIndex + 4; i++)
        {
            singleLength += body[i] * (int)Math.Pow(256, i - startIndex);
        }
        // 3. 解析数据
        // 如果       开始位 + 单个数据长度 + 4(头位) == 数据有效位    说明这一定是个整包
        if (startIndex + singleLength + 4 == length)
        {
            // 从开始位 + 4 读取数据长度
            string msg = Encoding.UTF8.GetString(body, startIndex + 4, singleLength);
            Console.WriteLine(++count + ":" + msg);
        }
        // 如果       开始位 + 单个数据长度 + 4(头位) < 数据有效位    说明这不只是一个包
        else if (startIndex + singleLength + 4 < length)
        {
            // 取出整包
            string msg = Encoding.UTF8.GetString(body, startIndex + 4, singleLength);
            Console.WriteLine(++count + ":" + msg);
            // 读取下一个包,从当前包的结束位开始读取
            UnPack(body, length, startIndex + 4 + singleLength);
        }
        // 否则说明     开始位 + 单个数据长度 + 4(头位) > 数据有效位    说明这是一个半包
        else
        {
            // 半包的话暂时存起来
            lastPack.AddRange(body.Skip(startIndex + 4).Take(singleLength));
            // 记录这个半包还差几个
            lagePackRemaining = singleLength - lastPack.Count;
        }
    }

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值