提示: 1、数据帧的长度不定,但是帧头帧尾是固定的 2、数据帧的参数数量不定,请注意 3、每次收到的数据可能不是完整的一帧,但是不能把不完整的数据帧丢弃,应该等待到下一完整帧接收到后才丢弃 4、一次可能接受到不止一个数据帧,可能是多个,需要针对不同数据帧进行分割
搜说关键词:数据帧粘包 数据帧拆包 帧处理
以下内容文献摘抄CSDN博客的bandaoyu博主,加上自己的一些总结:
什么是分包与拆包呢?
假设客户端分别发送了两个数据包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帧数据报文间隔时间来确定一帧数据包。如果在其后连续3.5字节时间内都没有收到一个字节,则认为之前收到的
报文,就是一条完整的报文。假定收到该报文长度为10,则接收缓冲区数组[0]=帧头,接收缓冲区数组[9]=帧尾。
方式2:利用特殊字符串1作为帧头,特殊字符串2作为帧尾。而且特殊字符串1不允许出现在报文中,只能作为帧头;特殊字符串2不允许出现在报文中,只能作为帧尾。回车+换行符不可能出现在正常报文中,只能用于帧尾。
方式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分别都是独自完整的包。
下一是法一,但是有错误
#include<iostream>
#include<vector>
using namespace std;
//定义数据帧结构体
struct DataFrame
{
//假设数据帧由4个字节的数据和1个字节的校验码组成
vector<unsigned char>data;
unsigned char checksum;
};
//数据帧解析函数
void pareaDataFrame(const vector<unsigned char >& receiveData)
{
if (receiveData.size() != sizeof(DataFrame))
{
std::cout << "接收的数据不是有效的数据框" << endl;
return;
}
}
//将接受到的数据进行类型转换,以便访问各个字段
const DataFrame* frame = reinterpret_cast<const DataFrame*>(receivedData.data());
std::cout << "接收的数据框" << endl;
std::cout << "Data:"; // 这里缺少打印data字段的代码
std::cout << "checksum:" << static_cast<int>(frame->checksum) << endl;
int main()
{
//假设受到的数据帧保存在receiveData变量中
vector<unsigned char>receiveData = { 0x01,0x02,0x03,0x04,0xAB };
pareaDataFrame(receiveData);
return 0;
}
一下是法二:
上述代码是一个用于处理数据帧的函数processDataFrame和主函数main。其中,processDataFrame函数根据传入的接收到的数据receivedData,查找并处理数据帧。
在主函数main中,定义了一个接收到的数据receivedData,其中包含了多个数据帧以及部分不完整的数据帧。
processDataFrame函数首先声明一个静态变量isIncompleteFrame用于标记是否存在不完整的数据帧。然后通过查找字符串"<frame>"获取第一个数据帧的起始位置frameStart,如果未找到帧头并且上一帧数据还未完整接收到,则直接返回。
之后使用while循环处理多个数据帧的情况,首先查找帧尾的位置frameEnd,如果未找到帧尾则表示接收到的是不完整的数据帧,将isIncompleteFrame设为true,并退出循环。否则,取出完整的数据帧frame,并解析输出数据帧的内容。
继续查找下一个帧头的位置frameStart,直到所有数据帧都被处理完。
最后,如果存在不完整的数据帧且已经处理完所有数据帧,则将剩余数据保存到receivedData以便下次拼接。
主函数main中调用processDataFrame函数,并传入接收到的数据receivedData。
整体来说,该代码实现了处理数据帧的功能,可以识别并处理完整的数据帧,以及保存并等待下次拼接的不完整的数据帧。
```cpp
#include <iostream>
#include <string>
using namespace std;
void processDataFrames(const std::string& receivedData)
{
static bool isIncompleteFrame = false; // 标记是否存在不完整的数据帧
// 查找帧头的位置
size_t frameStart = receivedData.find("<frame>");
// 如果未找到帧头,表示收到的数据不完整,与上次接收到的数据拼接起来
if (frameStart == std::string::npos && !isIncompleteFrame)
{
// 上一帧数据还未完整接收到,丢弃这部分数据
return;
}
// 处理多个数据帧的情况
while (frameStart != std::string::npos)
{
// 查找帧尾的位置
size_t frameEnd = receivedData.find("</frame>", frameStart);
// 如果未找到帧尾,表示接收到的是不完整的数据帧
if (frameEnd == std::string::npos)
{
isIncompleteFrame = true;
break;
}
// 取出完整的数据帧
std::string frame = receivedData.substr(frameStart, frameEnd - frameStart + 8);
// 解析输出数据帧的内容
std::cout << "Received data frame: " << frame << std::endl;
// 继续查找下一个帧头的位置
frameStart = receivedData.find("<frame>", frameEnd);
}
// 如果存在不完整的数据帧,将其保存以便下次拼接
if (isIncompleteFrame && frameStart == std::string::npos)
{
receivedData == receivedData.substr(frameStart);
}
}
int main()
{
std::string receivedData = "<frame>data1</frame><frame>data2</frame><partial_frame/>";
processDataFrames(receivedData);
return 0;
}