TCP 粘包与拆包:字节流没有消息边界
更新时间:2026-09-17。网络协议域·传输层。这是写自定义 TCP 协议/长连接 RPC 的第一个坑。它和 Nagle 算法无关,根因是 TCP 的字节流模型。
"粘包/拆包"不是 TCP 的 bug,而是使用方的认知偏差:TCP 保证字节按序、可靠到达,但不保留应用层一次写入的边界。对端 read() 拿到的是一段连续字节,可能是半条、一条或多条消息拼在一起。
一、为什么会粘/会拆
发送端:
- 应用连续
send两条小消息,内核按 MSS、Nagle、发送时机合并成一个段发出(粘包)。 - 一条大消息超过 MSS,被内核切成多个 TCP 段(拆包/半包)。
接收端:
read只从接收缓冲区里当前已有的字节里取,取多少由缓冲情况和传入长度决定,与对端写了几次毫无对应关系。
发送方依次写入: [请求A][请求B]
接收方 read 可能得到:
情况1 一次读到 "[请求A][请求B]" -> 粘在一起(粘包)
情况2 第一次 "[请求" 第二次 "A][请求B]" -> 拆开(拆包/半包)
情况3 甚至跨多条、带半个下一条 -> 粘+拆混合对比 UDP:UDP 是面向数据报的,
recvfrom一次恰好拿到一个完整报文(有天然边界),不会粘包;但不保证可靠、有序。TCP 用"无边界的字节流"换来了可靠性与全双工。
二、read/write 还可能"部分"处理
即使一次发送,write/send 也可能只接受了部分字节(发送缓冲区满),read 也可能只返回部分数据。健壮的网络代码必须循环直到处理完期望字节数:
// 写满 n 字节(简化)
ssize_t write_all(int fd, const char* buf, size_t n) {
while (n > 0) { ssize_t k = write(fd, buf, n); if (k<=0) return -1; buf+=k; n-=k; }
return 0;
}非阻塞 socket 下还会遇到 EAGAIN/EWOULDBLOCK(现在没数据/发不出去,稍后再试),这是 epoll 边沿触发编程必须配合循环 read/write 的原因。
三、三种解决办法(核心是应用层定义帧格式)
既然 TCP 不给边界,就在应用层协议里自己定义"一条消息怎么界定"。
1. 固定长度(定长帧)
每条消息都补齐到固定 N 字节,接收方每次凑满 N 个字节就是一条。简单但浪费带宽、定长难定,只适合简单场景。
2. 特殊分隔符
用约定字符(如 \n、\r\n)分隔,类 HTTP 头、Redis RESP、行协议。接收方不断在缓冲里找分隔符:
SET key value\nGET key\n缺点是消息体若含分隔符要转义;适合文本协议。
3. 长度字段 + 内容(长度前缀 / TLV,最通用)
消息头放一个固定宽度的长度字段(可再加类型、版本),后接该长度的 body。接收方先读满头部拿到 length,再精确读 length 字节 body:
+--------+----------+----------------+
| length | type/... | payload |
| 4 bytes| 头部 | length 字节 |
+--------+----------+----------------+// 伪代码:维护应用层接收缓冲 buf
while (1) {
if (buf.len < HEADER_LEN) break; // 头部都没收全,继续读
uint32_t len = decode_header(buf);
if (buf.len < HEADER_LEN + len) break; // body 没收全,等更多数据
process(buf + HEADER_LEN, len); // 完整一条,处理
buf.consume(HEADER_LEN + len); // 移除,剩余可能已是下一条
}这正是 Netty 的 LengthFieldBasedFrameDecoder、DelimiterBasedFrameDecoder、FixedLengthFrameDecoder,以及 gRPC(HTTP/2 的 DATA 帧有 length)、Protobuf varint 长度前缀做的事。
四、正确心智模型:接收是"流 → 切帧"
把网络接收想成一个不断追加字节的缓冲区 + 一个切帧器:
- 每次能 read 多少就读多少,追加到应用层接收缓冲(不要假设一次一条)。
- 在缓冲里按协议(定长/分隔符/长度字段)尝试切出 0~N 条完整消息。
- 不完整就保留残包等下次,完整就处理并移除,剩下的继续切。
这样无论内核怎么分段、Nagle 是否合并、一次 read 返回多少,应用都能正确还原消息边界。
五、常见误区
- 以为关 Nagle(TCP_NODELAY)就能防粘包——不能。它只改发送时机,字节流仍无边界,仍需应用层帧。
- 以为"一次 write 对方一次 read"——不成立,本地测试可能恰好成立,一上生产/跨网络就错乱。
- 用
sleep或每次只发一点来"避免粘包"——依赖时序,极不可靠,正确做法永远是定义消息格式。 - HTTP 本身就是范例:请求行+头部用 CRLF 分隔,空行后用
Content-Length或 chunked 表达 body 长度——就是分隔符 + 长度的组合。
相关阅读
发送时机与 40ms 延迟见 Nagle/延迟 ACK,非阻塞循环读写配合 epoll,HTTP/2 的帧与多路复用见 HTTP/2。
总结
"粘包拆包"源于 TCP 是无消息边界的字节流,read 可能返回半条、一条或多条,且 read/write 可能部分完成;唯一正解是在应用层定义帧——固定长度、分隔符、或最通用的长度前缀(length+payload),用"接收缓冲+切帧循环"还原消息;TCP_NODELAY 只影响发送时机,解决不了边界问题。