4. 固定长度协议方案:设计思路与原理
聊完了粘包拆包的本质,咱们得开始动手解决了。第一个方案,也是最直观的方案——固定长度协议。
说白了,就是让每个数据包都长一个样。你发 100 字节,我收 100 字节。不多不少,刚刚好。这样接收端就不用猜哪里是头哪里是尾了,按长度切就行。
我记得刚入行那会儿,带我的老工程师跟我说过一句话:「网络编程里,最简单的方案往往最可靠。」固定长度协议就是这句话的典型代表。
设计思路与原理
核心思想其实就一句话:通信双方约定一个固定的报文长度 L,发送端每次发送 L 字节,接收端每次也读取 L 字节。
你想想看,如果每个包都是 100 字节,那接收端就简单了——每次从缓冲区里取 100 字节,就是一个完整的包。不存在粘包,因为长度固定;也不存在拆包,因为长度已知。
原理图如下:
定长协议头设计
固定长度协议有两种常见设计方式:
| 设计方式 | 说明 | 典型场景 |
|---|---|---|
| 纯固定长度 | 整个报文长度固定,没有头尾区分 | 传感器数据、GPS 定位包 |
| 定长头 + 定长体 | 头部固定长度,头部中包含固定长度的数据体 | 金融交易报文、工业控制协议 |
我个人习惯用第二种——定长头 + 定长体。虽然看起来多了一层,但扩展性好。头部可以放一些元信息,比如消息类型、序列号、校验码等。
定长协议头示例结构:
总长度 = 2 + 4 + 2 + 92 = 100 字节
| 消息类型(2B) | 序列号(4B) | 保留字段(2B) | 数据体(92B) |总长度 = 2 + 4 + 2 + 92 = 100 字节
发送端实现(补齐/截断)
发送端要处理两个问题:数据太短怎么办?数据太长怎么办?
- 补齐(Padding):当实际数据小于固定长度时,用填充字节补足。常用填充字符有
\0(空字符)、空格、或者自定义的填充标记。 - 截断(Truncation):当实际数据大于固定长度时,要么截断,要么报错。我建议直接报错,因为截断会导致数据丢失,而且很难排查。
来看一段发送端的代码示例:
#define PACKET_LENGTH 100
#define PADDING_CHAR '\0'
int send_fixed_length(int sockfd, const char *data, int data_len) {
char buffer[PACKET_LENGTH];
if (data_len > PACKET_LENGTH) {
// 数据超长,直接报错
fprintf(stderr, "数据长度 %d 超过固定长度 %d\n", data_len, PACKET_LENGTH);
return -1;
}
// 拷贝实际数据
memcpy(buffer, data, data_len);
// 补齐剩余部分
if (data_len < PACKET_LENGTH) {
memset(buffer + data_len, PADDING_CHAR, PACKET_LENGTH - data_len);
}
// 发送固定长度的数据包
return send(sockfd, buffer, PACKET_LENGTH, 0);
}
💡 小技巧: 我曾经在补齐时用过
0xFF 作为填充字节,结果调试时发现接收端把填充数据当成了有效内容。后来统一改用 \0,配合接收端跳过尾部空字符,问题就解决了。
接收端实现(按长度切割)
接收端就简单了。每次从缓冲区里读取固定长度的数据,就是一个完整的包。如果缓冲区里的数据不够一个包的长度,就等下一次读取。
核心逻辑就三步:
- 检查缓冲区中可读数据是否 ≥ 固定长度
- 如果够,取出固定长度的数据作为一个完整包
- 如果不够,等待更多数据到达
#define PACKET_LENGTH 100
int recv_fixed_length(int sockfd, char *out_buffer) {
static char recv_buf[4096];
static int buf_len = 0;
int n;
while (1) {
// 检查是否已经有一个完整包
if (buf_len >= PACKET_LENGTH) {
memcpy(out_buffer, recv_buf, PACKET_LENGTH);
// 移除已处理的数据
memmove(recv_buf, recv_buf + PACKET_LENGTH, buf_len - PACKET_LENGTH);
buf_len -= PACKET_LENGTH;
return PACKET_LENGTH;
}
// 读取更多数据
n = recv(sockfd, recv_buf + buf_len, sizeof(recv_buf) - buf_len, 0);
if (n <= 0) {
return n; // 连接关闭或出错
}
buf_len += n;
}
}
⚠️ 注意: 接收端的
memmove 一定要用,不能用 memcpy。因为源地址和目的地址可能有重叠,memcpy 在这种情况下行为是未定义的。我曾经在这个坑里栽过跟头,排查了一整天才发现是 memcpy 的问题。
优缺点分析
| 优点 | 缺点 |
|---|---|
|
|
说白了,固定长度方案就是用带宽换简单。如果你的数据包长度相对稳定,那这个方案性价比很高。但如果数据长度忽长忽短,那补齐的浪费就有点心疼了。
适用场景
根据我的经验,固定长度协议最适合以下几类场景:
- 物联网传感器数据:温度、湿度、气压等数据,每个字段长度固定,拼起来就是一个固定长度的包
- GPS 定位数据:经纬度、高度、时间等,格式统一,长度固定
- 金融交易报文:很多老牌金融协议(如 FIX 协议的某些变种)就采用固定长度
- 工业控制协议:Modbus、CAN 等底层协议,追求确定性和低延迟
- 音视频编解码:某些音频帧(如 PCM)长度固定,用固定长度协议天然匹配