下面把 TCP 粘包/拆包问题一次性讲透,给出"原理 → 根因 → 方案 → 代码级流程",拿来即可落地。
- 什么是粘包 & 拆包
- 粘包:发送端连续调用 send() 多次,接收端一次 recv() 把多条报文当成一条读出来。
- 拆包:一条报文被 TCP 切分成多次 recv() 才能读完(或一次只读出部分)。
现象:业务层拿到的"字节流"与期望的"消息边界"不一致,解析失败。
- 根本原因(TCP 语义导致)
- 字节流协议:TCP 只保证字节顺序与可靠到达,不保留应用层消息边界。
- Nagle/延迟确认:发送端合并小片段,接收端合并确认,加剧粘包。
- MSS/分片:路径 MTU 限制,大于 MSS 的报文被 IP 层切片,造成拆包。
- 接收窗口:接收端缓存不足时,内核只拷贝部分数据到用户空间。
- 解决思路(4 种主流方案)
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| ① 固定长度 | 每条报文长度固定为 L | 解析极简 | 浪费流量/扩展性差 | 二进制传感数据 |
| ② 分隔符 | 每条报文结尾加特殊字节(如 \n、\r\n\0) |
协议直观 | 需要转义/扫描 | 文本协议(Redis、HTTP) |
| ③ 消息头+长度字段 | 报文=Header(长度)+Body | 通用、可扩展、无转义 | 需要编解码 | 大多数私有 RPC、游戏、IM |
| ④ 短连接 | 一次请求→一次 TCP 连接 | 无粘包 | 三次握手+TIME_WAIT 开销大 | 低频、低并发场景 |
- 代码级处理流程(以"消息头+长度字段"举例)
报文格式(大端)
diff
+--------+---------+
| 4字节 | N 字节 |
| length | payload |
+--------+---------+
发送端(Python 伪码)
python
def send_msg(sock, data: bytes):
length = len(data)
header = length.to_bytes(4, byteorder='big')
sock.sendall(header + data) # 一次系统调用即可
接收端(非阻塞/异步通用解码器)
python
class Decoder:
def __init__(self):
self._buf = b''
self._expect = 4 # 先读头
def feed(self, chunk: bytes) -> list[bytes]:
self._buf += chunk
msgs = []
while True:
if self._expect == 4 and len(self._buf) >= 4:
self._expect = int.from_bytes(self._buf[:4], 'big')
self._buf = self._buf[4:]
if self._expect > 0 and len(self._buf) >= self._expect:
msgs.append(self._buf[:self._expect])
self._buf = self._buf[self._expect:]
self._expect = 4
else:
break
return msgs
使用方式(asyncio 示例)
python
async def handle(reader, writer):
dec = Decoder()
while True:
chunk = await reader.read(4096)
if not chunk:
break
for msg in dec.feed(chunk):
process(msg) # 拿到完整业务报文
- 额外注意点
- 关闭 Nagle:对延迟敏感的场景
TCP_NODELAY=1。 - 内核接收缓冲区:
SO_RCVBUF调到 ≥ 带宽×RTT。 - 大文件/流式场景:采用"分片+校验"方式,避免一次性加载到内存。
- 双协议栈:公网走 TLS(防篡改),内网走明文+长度字段,减少 CPU。
一句话总结
"粘包不是 TCP 的错,是没用好消息边界。"
记住:"定长、分隔符、长度字段、短连接"四大武器,按场景选一个,把解码器写成状态机,粘包问题就彻底解决了。