粘包问题本质是tcp无消息边界,需靠协议头长度字段自行拆包;必须先循环recv收满固定长度包头(如4字节),再解析body,否则易越界崩溃;持续处理半包、多包等异常需状态机与日志。

粘包问题本质是 TCP 没有消息边界
Socket 收到的数据流里,recv 或 read 返回的字节数不等于一个完整业务包长度——这根本不是 bug,而是 TCP 的设计特性。你不能指望网络层替你切分逻辑消息,必须自己靠协议头里的长度字段做拆包。关键不在“怎么读”,而在“读到哪算一包”。
解析包头前必须收齐固定长度头
绝大多数二进制协议(比如 4 字节 length + N 字节 body)要求先读满包头长度,才能知道 body 要读多少。常见错误是直接 recv(buf, 1024, 0) 然后硬解析,结果 buf 开头只有 2 字节,ntohl(*(uint32_t*)buf) 就会读越界或解出垃圾值。
- 用循环
recv直到收满包头长度(例如 4 字节),每次检查返回值是否 > 0,== 0表示对端关闭,且 <code>errno == EAGAIN/EWOULDBLOCK才该等待 - 别用
MSG_WAITALL:Linux 下它在非阻塞 socket 上无效,Windows 行为也不一致,跨平台不可靠 - 包头字段要用网络字节序解析,
ntohl/ntohs不可省;如果协议定义为小端,得用memcpy+ 手动拼字节,不能直接类型强转
拆包状态机比缓冲区拼接更稳
用一个动态缓冲区(如 std::vector<uint8_t></uint8_t>)不断 push_back 新收数据,再反复扫描切包,看似简单,实则容易在边界上出错:比如包头长度字段本身被截断、body 长度超限、或连续多个小包合并后解析错位。
- 推荐用三状态机:
WAITING_HEADER→WAITING_BODY→PACKAGE_READY,每个状态只关心当前要什么 - 进入
WAITING_BODY后,只从缓冲区头部取expected_body_len字节,取完立刻erase对应范围,避免下一次误判 - 务必校验
body_len合理性(比如> 0 && ),防止恶意构造超大长度导致内存耗尽
recv 返回值和 errno 的实际含义常被误解
很多人以为 recv 返回 0 就是“没数据”,其实它只表示对端已 close;而 errno == EAGAIN 在阻塞 socket 上根本不会出现——只有设了 O_NONBLOCK 或 SOCK_NONBLOCK 才可能触发。
- 阻塞 socket 下,
recv返回 -1 且errno == EINTR是正常现象(被信号中断),应重试,不是错误 - 非阻塞 socket 下,
recv返回 -1 且errno == EAGAIN/EWOULDBLOCK才表示“暂时没数据”,此时应让出 CPU 或等待 epoll/kqueue 事件 - 永远检查
recv返回值是否> 0再做解析,否则拿未初始化内存当包头用,崩溃只是时间问题
真正难的不是写对一个包的解析,而是在连接生命周期内持续处理各种异常组合:半包、多包、乱序重传(TCP 层已保证顺序,但应用层仍可能因重连/闪断产生逻辑乱序)、以及协议头字段被中间设备篡改。这些地方没日志、没断点,只能靠状态机每一步都存现场快照才容易定位。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











