处理从套接字的缓冲区中接收到连续的字节流,包含一个或多个数据包时,从缓冲区提取出完整的数据包,并将剩余数据做前移处理以便后续解析
1. 数据包设计及基本结构
数据包使用一个自定义的类 CPacket
来实现,其主要字段如下:
- 包头 (
sHead
) 2 字节字段,固定值0xFEFF
。用于标识数据包的起始位置,方便在接收到的连续数据中定位包头。 - 数据长度 (
nLength
) 4 字节字段,记录数据区长度 + 命令 (2 字节) + 校验和 (2 字节) 的总和。这使得在解包时可以迅速判断当前缓冲区中是否包含一个完整的数据包。 - 命令 (
sCmd
) 2 字节字段,用来标示数据包的命令类型或标志。 - 数据区 (
strData
) 可变长度字符串,实际存放需要传输的数据。 - 校验和 (
sSum
) 2 字节字段,采用简单的累加算法对数据区内的每个字节求和,用于校验数据包是否在传输过程中损坏。
这种数据结构对于模拟真实网络传输中的数据包格式非常实用,有助于提高数据解析的健壮性。
2. 封包与解包的实现
2.1 封包构造函数
封包构造函数的作用是将原始数据“打包”成符合上述格式的数据包,并计算校验和。代码的关键部分如下:
cpp
CPacket(WORD nCmd, const BYTE* pData, size_t nSize) {
sHead = 0xFEFF;
// nLength = 数据区长度 + 2字节命令 + 2字节校验和
nLength = nSize + 2 + 2;
sCmd = nCmd;
if (nSize > 0) {
strData.resize(nSize);
memcpy(&strData[0], pData, nSize);
} else {
strData.clear();
}
sSum = 0;
// 计算校验和:累加数据区的每个字节
for (size_t j = 0; j < strData.size(); j++) {
sSum += static_cast<BYTE>(strData[j]);
}
}
- 初始化包头:固定设置为
0xFEFF
。 - 计算数据长度:将传入的数据长度加上命令和校验和各占 2 字节。
- 数据复制:如果数据存在,则将原始数据复制到
strData
中。 - 校验和计算:对整个数据区的每个字符进行累加,用作校验和验证。
这样构造的数据包可以直接用于序列化后发送。
2.2 解包构造函数
当接收到一段连续的字节流时,解包构造函数会尝试从中解析出一个完整的数据包。其主要逻辑分为以下几个步骤:
cpp
CPacket(const BYTE* pData, size_t &nSize) {
size_t i = 0;
bool foundHeader = false;
// 1. 查找包头 0xFEFF
for (; i + 1 < nSize; i++) {
WORD header = *(reinterpret_cast<const WORD*>(pData + i));
if (header == 0xFEFF) {
sHead = header;
i += 2; // 跳过包头
foundHeader = true;
break;
}
}
if (!foundHeader) {
nSize = 0;
return;
}
// 2. 检查是否能读取长度、命令和校验和
if (i + 4 + 2 + 2 > nSize) {
nSize = 0;
return;
}
// 读取长度字段
nLength = *(reinterpret_cast<const DWORD*>(pData + i));
i += 4;
// 3. 判断当前缓冲区数据是否足够构成一个完整包
if (nLength + i > nSize) {
nSize = 0;
return;
}
// 4. 读取命令字段
sCmd = *(reinterpret_cast<const WORD*>(pData + i));
i += 2;
// 5. 如果存在数据区,则提取数据区数据
if (nLength > 4) {
size_t dataLen = nLength - 4; // 数据区长度 = 总长度 - 2字节命令 - 2字节校验和
strData.resize(dataLen);
memcpy(&strData[0], pData + i, dataLen);
i += dataLen;
}
// 6. 读取校验和并进行校验
sSum = *(reinterpret_cast<const WORD*>(pData + i));
i += 2;
// 计算数据区的校验和进行比对,确保数据正确传输
WORD sum = 0;
for (size_t j = 0; j < strData.size(); j++) {
sum += static_cast<BYTE>(strData[j]);
}
if (sum == sSum) {
nSize = i; // 更新 nSize 为本包消耗的字节数
return;
}
// 若校验失败,视作解包失败
nSize = 0;
}
解析说明:
- 查找包头 遍历缓冲区,查找固定包头
0xFEFF
,定位包的起点。如果找不到,则认为数据包不完整。 - 检查最小数据长度 必须至少包含长度、命令和校验和部分(共 8 字节),否则直接返回失败。
- 完整性验证 解析长度字段后,判断剩余数据是否足够解析出一个完整数据包。如果不够,则返回失败。
- 数据提取 根据数据长度提取出完整的命令和数据区,并读取校验和字段。
- 校验和验证 累加数据区中所有字节的值,并与读取到的
sSum
进行比对。如果匹配,则解析成功;否则,标记为解包失败。
2.3 数据包序列化
为了模拟实际的网络发送过程,CPacket
类提供了一个 Data()
方法,用于将数据包序列化成连续内存区域的格式:
cpp
const char* Data(std::string &strOut) const {
size_t totalSize = 2 + 4 + 2 + strData.size() + 2;
strOut.resize(totalSize);
BYTE* pData = reinterpret_cast<BYTE*>(&strOut[0]);
*(reinterpret_cast<WORD*>(pData)) = sHead;
pData += 2;
*(reinterpret_cast<DWORD*>(pData)) = nLength;
pData += 4;
*(reinterpret_cast<WORD*>(pData)) = sCmd;
pData += 2;
if (!strData.empty()) {
memcpy(pData, strData.data(), strData.size());
pData += strData.size();
}
*(reinterpret_cast<WORD*>(pData)) = sSum;
return strOut.c_str();
}
- 按照数据包格式顺序(包头、长度、命令、数据区、校验和)将各个部分依次拷贝进字符串
strOut
。 - 返回最终的连续内存数据,用于发送或后续处理。
3. 模拟从套接字缓冲区读取并处理数据包
在实际场景中,网络数据可能一次性接收到多个数据包或部分数据包,本例在 main()
函数中做了以下模拟:
-
构造与序列化数据包 模拟了两个数据包,将原始 payload 封装成
CPacket
实例,然后调用Data()
方法序列化成连续内存格式:cpp
CPacket pack1(0x0010, reinterpret_cast<const BYTE*>(payload1.data()), payload1.size()); CPacket pack2(0x0020, reinterpret_cast<const BYTE*>(payload2.data()), payload2.size()); pack1.Data(serialized1); pack2.Data(serialized2);
将两个序列化后的数据包拼接,构造了一个包含多个数据包的字节流。
-
拷贝至接收缓冲区 分配一个足够大的缓冲区,并将整个字节流复制进去,用以模拟套接字缓冲区中接收到的数据:
cpp
BYTE* buffer = new BYTE[BUFFER_SIZE]; memset(buffer, 0, BUFFER_SIZE); memcpy(buffer, recvData.data(), totalRecvSize);
-
循环解析数据包 使用一个循环来逐个从缓冲区中提取完整的数据包,成功解析后调用
PrintPacketInfo
输出信息,然后利用memmove
将剩余数据前移,以便处理下一个数据包:cpp
while (1) { size_t currentBufferSize = totalRecvSize; if (currentBufferSize > 0) { currentDataLen += currentBufferSize; size_t packetLen = currentDataLen; // 初始使用所有数据进行解析 CPacket receivedPacket(buffer, packetLen); if (packetLen > 0) { // 成功解析出完整数据包 PrintPacketInfo(receivedPacket, packetLen); // 移动缓冲区中剩下的数据至前端 currentDataLen -= packetLen; memmove(buffer, buffer + packetLen, currentDataLen); } else { cout << "数据包不完整,等待更多数据……" << endl; break; } } }
完整代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdint>
using namespace std;
// Windows 类型定义
using BYTE = unsigned char;
using WORD = uint16_t;
using DWORD = uint32_t;
const size_t BUFFER_SIZE = 1024; // 定义缓冲区大小
// 数据包类 CPacket 定义
class CPacket {
public:
WORD sHead; // 包头
DWORD nLength; // 数据长度(数据区 + 2字节命令 + 2字节校验和)
WORD sCmd; // 命令
std::string strData; // 数据区
WORD sSum; // 校验和
// 封包构造函数:将原始数据封装成数据包
CPacket(WORD nCmd, const BYTE* pData, size_t nSize) {
sHead = 0xFEFF;
// nLength 为数据区长度 + 2字节命令 + 2字节校验
nLength = nSize + 2 + 2;
sCmd = nCmd;
if (nSize > 0) {
strData.resize(nSize);
memcpy(&strData[0], pData, nSize);
} else {
strData.clear();
}
sSum = 0;
// 简单累加数据区每个字节作为校验和
for (size_t j = 0; j < strData.size(); j++) {
sSum += static_cast<BYTE>(strData[j]);
}
}
// 解包构造函数:从接收到的缓冲区中解析出一个完整数据包
// 参数 nSize 表示缓冲区中现有总字节数,
// 若成功解析出包,该变量将被更新为本包使用的字节数,否则置 0 表示解包失败
CPacket(const BYTE* pData, size_t &nSize) {
size_t i = 0;
bool foundHeader = false;
// 遍历查找包头 0xFEFF(假定数据对齐)
for (; i + 1 < nSize; i++) {
WORD header = *(reinterpret_cast<const WORD*>(pData + i));
if (header == 0xFEFF) {
sHead = header;
i += 2; // 跳过包头
foundHeader = true;
break;
}
}
if (!foundHeader) {
nSize = 0;
return;
}
// 至少需要有4字节的长度、2字节命令和2字节校验和
if (i + 4 + 2 + 2 > nSize) {
nSize = 0;
return;
}
// 读取长度字段
nLength = *(reinterpret_cast<const DWORD*>(pData + i));
i += 4;
// 检查包是否完整接收
if (nLength + i > nSize) {
nSize = 0;
return;
}
// 读取命令
sCmd = *(reinterpret_cast<const WORD*>(pData + i));
i += 2;
// 存在数据区
if (nLength > 4) {
size_t dataLen = nLength - 4; // 数据区长度 = 总长度 - 2字节命令 -2字节校验
strData.resize(dataLen);
memcpy(&strData[0], pData + i, dataLen);
i += dataLen;
}
// 读取校验和
sSum = *(reinterpret_cast<const WORD*>(pData + i));
i += 2;
// 比对校验和
WORD sum = 0;
for (size_t j = 0; j < strData.size(); j++) {
sum += static_cast<BYTE>(strData[j]);
}
if (sum == sSum) {
nSize = i; // 更新 nSize 为本包使用的字节数
return;
}
// 校验失败则视为解包失败
nSize = 0;
}
// 将数据包序列化为连续内存数据,此函数用于模拟发送时的字节流格式
// 格式:[2字节包头] + [4字节长度] + [2字节命令] + [数据区] + [2字节校验和]
const char* Data(std::string &strOut) const {
size_t totalSize = 2 + 4 + 2 + strData.size() + 2;
strOut.resize(totalSize);
BYTE* pData = reinterpret_cast<BYTE*>(&strOut[0]);
*(reinterpret_cast<WORD*>(pData)) = sHead;
pData += 2;
*(reinterpret_cast<DWORD*>(pData)) = nLength;
pData += 4;
*(reinterpret_cast<WORD*>(pData)) = sCmd;
pData += 2;
if (!strData.empty()) {
memcpy(pData, strData.data(), strData.size());
pData += strData.size();
}
*(reinterpret_cast<WORD*>(pData)) = sSum;
return strOut.c_str();
}
};
// 以数据包格式输出关键字段
void PrintPacketInfo(const CPacket& pack, size_t packetSize) {
cout << "----- 数据包信息 -----" << endl;
cout << "包头: 0x" << hex << pack.sHead << dec << endl;
cout << "数据长度 (包含命令和校验和): " << pack.nLength << " 字节" << endl;
cout << "命令: 0x" << hex << pack.sCmd << dec << endl;
cout << "校验和: 0x" << hex << pack.sSum << dec << endl;
cout << "数据区: " << pack.strData << endl;
cout << "【本包总消耗】: " << packetSize << " 字节" << endl;
cout << "----------------------" << endl;
}
int main() {
// 模拟构造两个数据包的原始载荷
std::string payload1 = "Hello, Packet!";
std::string payload2 = "Packet Test Demo";
// 构造两个数据包
CPacket pack1(0x0010, reinterpret_cast<const BYTE*>(payload1.data()), payload1.size());
CPacket pack2(0x0020, reinterpret_cast<const BYTE*>(payload2.data()), payload2.size());
//CPacket pack3(0x0020, reinterpret_cast<const BYTE*>(payload2.data()), payload2.size());
// 序列化为连续内存(模拟实际发送后的数据流)
std::string serialized1, serialized2;
pack1.Data(serialized1);
pack2.Data(serialized2);
// 将两个数据包拼接在一起,模拟一次性接收到多个包
std::string recvData = serialized1 + serialized2;
size_t totalRecvSize = recvData.size();
cout << "接收到总字节数: " << totalRecvSize << endl;
// 将接收到的数据放入预分配的接收缓冲区
BYTE* buffer = new BYTE[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
memcpy(buffer, recvData.data(), totalRecvSize);
size_t currentDataLen = 0;
// 模拟循环抽取缓冲区中的完整数据包
while (1) {
size_t currentBufferSize = totalRecvSize;
if(currentBufferSize > 0){
currentDataLen += currentBufferSize;
size_t packetLen = currentDataLen ; // 用于解析数据包的长度(初始取当前所有数据)
CPacket receivedPacket(buffer, packetLen);
if(packetLen > 0){// 成功解析出完整数据包
PrintPacketInfo(receivedPacket, packetLen);
// 移除已解析的数据包(前移剩余数据)
currentDataLen -= packetLen;
memmove(buffer, buffer + packetLen, currentDataLen);
}
else {
cout << "数据包不完整,等待更多数据……" << endl;
break;
}
}
}
delete[] buffer;
return 0;
}