【Net】TCP粘包与半包
文章目录
- TCP粘包与半包
- 1 背景
- 2 粘包(packet stick)
- 3 半包(packet split)
- 4 为什么会出现粘包/半包?
- 5 如何解决?
- 6 示例
- 7 总结
TCP粘包与半包
在网络编程中,粘包和半包问题是常见的 TCP 协议特有问题,尤其在基于流的传输协议中(如 TCP),它们常导致接收端无法正确还原发送端原本的一条条消息。
1 背景
TCP 是“字节流”协议,不保留消息边界,它只是一个字节流协议,只保证字节的顺序和完整性,但不关心应用层每条消息的边界。这就导致了“粘包”和“半包”的出现。
2 粘包(packet stick)
定义:多条数据包被粘在一起,接收端一次接收到了多条消息数据。
举例:
客户端连续发送两条消息:
[hello][world]
由于 TCP 是流式协议,可能在接收端变成:
[helloworld]
此时接收端无法确定 “hello” 和 “world” 的边界。
3 半包(packet split)
定义:一条完整的数据被拆成了几部分接收,接收端一次只能收到其中的一部分。
举例:
客户端发送一条 10 字节的消息:
[helloworld]
可能接收端第一次 recv 只收到:
[hello]
下一次再收到:
[world]
也就是说,一条消息被拆成了“半包”。
4 为什么会出现粘包/半包?
- TCP 特性导致:
- TCP 是字节流,不维护消息边界;
- Nagle 算法 会将小包合并发送(导致粘包);
- 接收端 buffer 缓冲区大小不确定,一次 read/recv 可能读不到完整数据(导致半包);
- 操作系统的发送/接收策略 也会影响包的合并与拆分。
5 如何解决?
-
通用思路:在应用层实现消息边界的识别机制
以下几种常见方案可以避免粘包/半包问题:
-
定长协议
- 每条消息固定长度(例如每条消息都是 128 字节)。
- 优点:实现简单;
- 缺点:浪费带宽,不适用于变长消息。
-
添加分隔符
- 每条消息结尾加特定分隔符(如
"\r\n"
)。 - 接收端通过查找分隔符来拆分消息;
- 缺点:消息内容中不能出现分隔符。
- 每条消息结尾加特定分隔符(如
-
长度前缀协议(最常用)
-
每条消息前加一个固定长度的字段表示消息体长度(如 4 字节整数):
[4字节长度][消息体]
示例:
[00000005][hello] [00000005][world]
- 接收端读取前 4 字节判断消息长度,再读取对应长度的消息体,精确拆包。
-
-
6 示例
C++ 实现的长度前缀协议收发逻辑示例,适用于基于 TCP 的客户端或服务器程序,用于解决粘包/半包问题。
- 协议格式
[4字节消息长度][消息体内容]
- 消息长度为 uint32_t(网络字节序)
- 核心发送/接收逻辑
发送端逻辑(附加长度前缀)
#include <arpa/inet.h> // htonl
#include <string>
#include <unistd.h> // writebool sendMessage(int sockfd, const std::string& message) {uint32_t len = htonl(message.size()); // 转为网络字节序std::string packet;packet.append(reinterpret_cast<const char*>(&len), sizeof(len)); // 4字节长度packet.append(message); // 实际消息体size_t totalSent = 0;while (totalSent < packet.size()) {ssize_t sent = write(sockfd, packet.data() + totalSent, packet.size() - totalSent);if (sent <= 0) return false;totalSent += sent;}return true;
}
接收端逻辑(支持粘包/半包)
#include <arpa/inet.h> // ntohl
#include <unistd.h> // read
#include <vector>
#include <string>bool recvExact(int sockfd, void* buffer, size_t length) {size_t total = 0;while (total < length) {ssize_t n = read(sockfd, (char*)buffer + total, length - total);if (n <= 0) return false; // 连接关闭或出错total += n;}return true;
}bool recvMessage(int sockfd, std::string& outMessage) {uint32_t len_net;if (!recvExact(sockfd, &len_net, sizeof(len_net))) return false;uint32_t len = ntohl(len_net);if (len > 10 * 1024 * 1024) return false; // 限制最大消息长度,防止攻击std::vector<char> buffer(len);if (!recvExact(sockfd, buffer.data(), len)) return false;outMessage.assign(buffer.begin(), buffer.end());return true;
}
客户端完整用法
std::string msg = "hello world";
sendMessage(sockfd, msg);std::string received;
if (recvMessage(sockfd, received)) {std::cout << "Received: " << received << std::endl;
}
说明与扩展建议
项目 | 说明 |
---|---|
字节序 | 使用 htonl/ntohl 保证跨平台兼容 |
粘包支持 | 多条消息合并也能正确拆分 |
半包支持 | recvExact 保证完整读取 |
安全性 | 应添加最大长度检查,防止恶意攻击 |
异步扩展 | 可结合 epoll 实现非阻塞版本 |
7 总结
问题 | 表现 | 原因 | 解决方式 |
---|---|---|---|
-------- | |||
粘包 | 多条消息合并 | TCP 合并包 | 定长、分隔符、长度前缀 |
半包 | 一条消息被拆开 | TCP 拆包 | 接收端维护 buffer,支持多次接收拼接 |