16. TCP 粘包与拆包(热度 182 · 进阶)
Q: 什么是 TCP 粘包?为什么会产生?怎么解决?
A:
本质
TCP 面向字节流,不保留消息边界。 两次 write 可能被合并到一个报文段(粘包),一条消息也可能被拆到多个段(拆包)。这不是 TCP 的 bug,而是字节流的固有特性。 UDP 是数据报,天然有边界,不会粘包。
产生原因
Nagle 等发送端优化合并小包;接收缓冲一次 read 读到多段;MSS/MTU 分片;应用读写节奏不匹配。TCP 只保证顺序与可靠,不知道应用层「一条消息」的边界。
解决方案
| 方案 | 做法 | 优缺点 |
|---|---|---|
| 定长消息 | 每条固定 N 字节 | 简单;短消息浪费填充 |
| 分隔符 | 如 \r\n | 直观;需转义,防恶意分隔符/长度攻击 |
| 长度字段(最通用) | 包头写 body 长度,先读头再按长度读体 | 灵活;必须校验最大长度与字节序 |
| 协议自带边界 | HTTP Content-Length/chunked、gRPC(HTTP/2 帧自带长度前缀的 delimited message) | 生态成熟;裸 Protobuf 单条消息在字节流上不自定界,仍要应用层加长度前缀 |
长度头方案工程要点:
- 约定字节序(网络序 big-endian)。
- 限制
max_body_size,防止恶意超长包头导致 OOM。 - 处理半包:循环缓冲直到收满一条。
- 可加魔数/CRC 提升健壮性。
设计动机
- 为何 TCP 不提供消息边界: 字节流模型对文件、管道类应用最自然;消息边界属于应用语义,协议无法替所有应用决定。
- 为何 UDP 提供边界: 数据报模型本身就是“一次发送一次接收”,代价是不可靠/大小限制。
常见追问
追问 1:粘包是 TCP 的 bug 吗?
- 答题要点: 不是。是字节流的固有特性;应用层负责定义并解析消息边界。UDP 无此问题是因为数据报模型。
追问 2:关掉 Nagle 能彻底解决粘包吗?
- 答题要点: 不能。只能降低小包合并概率;接收缓冲一次读多段、网络聚合等仍会造成“多条消息一次读到”。根本解决靠应用层协议。
追问 3:HTTP 是怎么解决的?
- 答题要点:
Content-Length定长 body;或Transfer-Encoding: chunked分块边界;响应读到长度满足即一条消息结束。HTTP 头部以空行结束,也是一种边界约定。
易错点
| 易错说法 | 纠正 |
|---|---|
| 「粘包只在长连接出现」 | 短连接也可能,只是边界被连接关闭掩盖 |
| 「UDP 也会粘包」 | UDP 保留数据报边界 |
| 「固定 read(n) 就一定对」 | 仍可能半包;必须按协议状态机拼包 |
| 「长度头可以无上限」 | 必须限制并校验,防内存攻击 |
| 「序列化格式自动解决粘包」 | 裸 Protobuf 在字节流上不自定界,仍要传输层加长度前缀或帧;gRPC 的边界由 HTTP/2 帧给(见上面「协议自带边界」那行),不是 Protobuf 自己解决的 |
与选择题对照
- 主库 150: 第 122 题(TCP 面向字节流的含义)、第 123 题(粘包主要原因)、第 121 题(TCP/UDP 对比)、第 64 题(UDP 特点)。
- 补题 16: 补-13(UDP 叙述,边界/校验相关对照)。
- 口述联动: 选择题考“原因与含义”;面试要解决方案细节与安全长度限制。
工程 / 抓包背景
- Wireshark: 同一
tcp.stream中观察多个应用消息是否同段/被拆;“TCP segment of a reassembled PDU”。 - 服务端: Java Netty
LengthFieldBasedFrameDecoder;Go 手写bufio按长度读;Pythonstruct.unpack读长度。 - Nginx: HTTP 层已帮你定界;自定义 TCP 协议需自行处理。
- 压测: 发小包高频 write 粘包更明显;可
tcpdump对照发送时序与到达段。 - 性能相关:
TCP_NODELAY关 Nagle,用于低延迟小消息(交互式协议)。