【粘包和拆包】数据帧粘包和拆包处理方式

同事定义了一个二进制协议格式如下

帧头+帧长度+……

但是没有看到有数据内包含与帧头一样的数据时如何转译的说明,所以我就有疑问如何避免粘包或多帧合并发送时怎么拆包?

对方回答:

我们实际上会在缓存解包,分包与拆包,不会出现大量粘包现象的。


问题来了,我不太理解他说的:“在缓存解包,分包与拆包” 是什么意思? 是如何识别帧头帧尾,分包拆包的?

 

发帖咨询后得到了网友ba_wang_mao的热心回复,而且非常详细启发很大:

 

粘包和拆包发生的场合

 1、一般只有在TCP网络上通信时才会出现分包与折包,单片机串口通信时,只要两帧报文之间发送的时间间隔大于一定值(例如:100毫秒),就不会出现分包和折包。
       

什么是分包与拆包呢?
      假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
       (1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
       (2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
       (3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
       (4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
       粘包和拆包原因
(1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
(3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
(4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

 

如何识别帧头帧尾


      在单片机上有2种方式。

      方式1:利用2帧数据报文间隔时间来确定一帧数据包。
              

     著名的有MODBUS-RTU标准协议。 MODBUS-RTU协议规定,如果接收到某一个字节以后,如果在其后连续3.5字节时间内都没有收到一个字节,则认为之前收到的
报文,就是一条完整的报文。假定收到该报文长度为10,则接收缓冲区数组[0]=帧头,接收缓冲区数组[9]=帧尾。

      方式2:利用特殊字符串1作为帧头,特殊字符串2作为帧尾。


              而且特殊字符串1不允许出现在报文中,只能作为帧头
              特殊字符串2不允许出现在报文中,只能作为帧尾
             著名的有GPS通信报文。
             例如:GPS通信报文中的GPGGA报文,以$GPGGA作为帧头,以回车+换行符(0x0D0x0A)作为帧尾。
             其中:$GPGGA不可能出现在正常报文中,只能用于帧头。
             回车+换行符不可能出现在正常报文中,只能用于帧尾。


     方式3:帧头+帧长度

缓存解包的方式识别帧

1、缓存解包


    单片机上普通采用环形缓冲区来解决缓存解包问题。
    所谓缓存解包就是建立一个大的环形缓冲区,环形缓冲区的长度至少是【最长报文】的n倍。环形缓冲区有独立的写指针和读指针,因此就算出现粘包,也不会影响解析报文工作。


2、拆包
  假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
       (1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,【报文是完整的】,没有粘包和拆包;
       (2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
       (3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
       (4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包
        上述(2)、(3)、(4)种情况,就是拆包。上述(1)情况不需要拆包,因为D1和D2分别都是独自完整的包。


3、分包
    说白了,就是要确保每种类型的报文有唯一的帧头和帧尾,在解析报文时才好顺利识别出它是何种类型的报文。即在发送报文时,
人为的添加帧头和帧尾用于区分报文。
      一般有如下方法:
    (1)、一个是采用分隔符的方式,即我们在封装要传输的数据包的时候,采用固定的符号作为结尾符(数据中不能含结尾符),这样我们接收到数据后,如果出现结尾标识,即人为的将粘包分开,如果一个包中没有出现结尾符,认为出现了分包,则等待下个包中出现后 组合成一个完整的数据包,这种方式适合于文本传输的数据,如采用/r/n之类的分隔符;
   (2)、 另一种是采用在数据包中添加长度的方式,即在数据包中的固定位置封装数据包的长度信息(或可计算数据包总长度的信息),服务器接收到数据后,先是解析包长度,然后根据包长度截取数据包(此种方式常出现于自定义协议中),但是有个小问题就是如果客户端第一个数据包数据长度封装的有错误,那么很可能就会导致后面接收到的所有数据包都解析出错(由于TCP建立连接后流式传输机制),只有客户端关闭连接后重新打开才可以消除此问题,我在处理这个问题的时候对数据长度做了校验,会适时的对接收到的有问题的包进行人为的丢弃处理(客户端有自动重发机制,故而在应用层不会导致数据的不完整性);

4、我们实际上会在缓存解包,分包与拆包,不会出现大量粘包现象的


   缓存、分包与拆包和粘包没有必然的联系。
  
5、粘包
   (1)、当连续发送数据时,由于tcp协议的nagle算法,会将较小的内容拼接成大的内容,一次性发送到服务器端,因此造成粘包。
   (2)、当发送内容较大时,由于服务器端的recv(buffer_size)方法中的buffer_size较小,不能一次性完全接收全部内容,因此在下一次请求到达时,接收的内容依然是上一次没有完全接收完的内容,因此造成粘包现象。

6、怎么实现不转译而实现正确的分包拆包


2、”怎么实现不转译而实现正确的分包拆包:添加帧头和帧尾 或帧头+帧长度 ---实际上只有帧数据中不允许出现”帧头“/”帧尾“数据的协议才可能不转译也能分包拆包。

但是二进制数据协议是很难确定数据中是否包含和“帧头”/帧尾一样的数据的,这时候需要另外一种方法:

从数据缓冲中读取到“帧头”然后再根据后面的长度读取数据,然后根据校验码校验,如果校验失败(说明当前帧是一个残帧,帧数据中刚好有和帧头一样的数据),就丢掉这个帧头继续往后面找:
             “帧头”+ “帧长度”+ XXXXXXXXXXX   +  校验码
              校验失败就丢弃,从校验码后面一个字节开始重新查找。

 

但是这样也可能遇到这样的情况,每一帧的数据里面都有和“帧头”一样的数据<比如某个设备的ID>,然后第一帧的帧头丢失接收不全,这样的话会导致后面的所有帧都取的不正确校验不通过,都丢弃。

     要解决这个问题这属于分包的设计啦,分包设计时增加的“帧头”你一定要设计成不可能在报文中出现的字符呀!这样不就解决了吗?
      例如:
                  帧头设计为“$GPGGA"
                  帧长度为16位,占用2个字节
                  帧报文内容为 0x11 0x12 0x13 0x14
                  校验码为 0X89 0X90

我一般采用如下方式处理

   1、串口中断服务程序读取字节到环形缓冲区
   2、主程序一个字节一个字节的从环形缓冲区读到解析缓冲区
           注:解析缓冲区为字符缓冲区,这样方便使用字符串查找功能,查找“帧头”
  3、解析缓冲区中找到一帧完整的报文并检验OK后,
            再将解析缓冲区中的内容去掉帧头和帧长度 ,然后转换存放到uint8_t 的缓冲区中,供程序使用。 

 

又回到了原点,“设计成不可能在报文中出现的字符” 只适合于数据为字符串的协议。对于二进制的协议,看来只能 增加帧头的字节数和通过确认机制来规避了(坐等大神来回答解决这个问题)。

 

讨论的帖子地址:https://bbs.csdn.net/topics/395887574?page=1#post-410711563
 

posted on 2022-10-04 01:27  bdy  阅读(250)  评论(0编辑  收藏  举报

导航