TCP粘包和拆包源于其字节流特性,无消息边界;send()可能被Nagle算法合并,recv()可能一次读多包或半包。必须在应用层定义协议,如用4字节定长头+变长体方式明确边界,并循环recv确保收满。

为什么TCP会粘包和拆包
TCP是字节流协议,不保留应用层消息边界。send()调用可能被内核合并(Nagle算法),recv()也可能一次读出多个逻辑包或只读到半个包。这不是Bug,而是协议设计使然——你必须自己定义并解析消息边界。
常见错误现象:recv()返回的数据既不是完整JSON对象,也不是预期的固定长度结构体,而是“半条命令+整条响应+半条日志”混在一起;或者连续两个send()发的小包被合并在一次recv()里返回。
用定长头+变长体解决粘包(最常用)
在每条消息前加一个固定长度的头部(如4字节网络序uint32_t),存实际数据长度。接收端先收齐头部,再按长度收主体。这是跨语言、低开销、易调试的方案。
- 发送端:先
htonl(len)转网络序,再send()头部,最后send()数据体 - 接收端必须循环调用
recv()直到收满头部(4字节),再循环收够len字节——不能假设一次recv()就返回全部 - 注意:
recv()返回0表示对端关闭;返回-1且errno == EAGAIN/EWOULDBLOCK说明非阻塞下暂无数据,需重试 - 示例片段:
uint32_t len = htonl(data.size()); send(sock, (char*)&len, sizeof(len), 0); send(sock, data.data(), data.size(), 0);
遇到分隔符(如\n)时怎么安全切包
适用于文本协议(HTTP、自定义指令),但必须处理分隔符出现在数据体中的情况。不能简单用std::string::find('\n')——二进制数据里完全可能出现\n。
立即学习“C++免费学习笔记(深入)”;
- 如果协议允许,改用更少见的分隔符(如
\r\n\r\n或自定义魔数0x1E 0x0F),并约定数据体中不出现该序列 - 更稳妥的做法是:先收固定头部,头部里明确标注是否启用转义,或直接采用定长头方案
- 若坚持用分隔符,接收缓冲区必须用
std::vector<char></char>维护,并每次从上次扫描位置继续查找,避免重复解析已消费字节 - 错误示范:
buffer.find('\n')后直接substr(0, pos)——没考虑pos == std::string::npos,也没处理粘包导致的多个\n连在一起
select/poll/epoll下如何避免阻塞在recv中
无论用哪种I/O模型,recv()都可能因未收满而阻塞(默认阻塞套接字)或返回EAGAIN(非阻塞)。关键不是“怎么不阻塞”,而是“怎么知道该收多少”。
- 定长头方案下:先确保已收到4字节头部 → 解析出
body_len→ 检查接收缓冲区是否有足够字节;不够就等待下次事件触发,够了就直接切包 - 不要在
recv()前用ioctl(sock, FIONREAD, &n)判断可读字节数——它不可靠(TCP窗口、延迟ACK等会导致误判) - 使用
MSG_PEEK标志临时窥探数据(如检查前4字节是否够头),但别依赖它做主逻辑——仍要配合循环recv() - 真正容易被忽略的是:每个连接必须维护独立的接收缓冲区(
std::vector<char></char>)和已解析偏移量,不同连接的状态绝不共享


















