计算机网络:什么是 TCP 的粘包、拆包问题?

#计算机网络#操作系统#面试八股

计算机网络:什么是 TCP 的粘包、拆包问题?

结论:TCP 是字节流,没有消息边界。发送方多次 write 可能被合并进一个段,一次 write 也可能被拆成多段, 接收方一次 read 拿到的可能是半个消息,也可能是一个半消息。 "粘包 / 拆包"是应用层如何划界的问题,不是 TCP 的缺陷。

1. 边界去哪了

应用写入: write("hello")   write("world")
线路上:   [hello][world]          ← 合并(Nagle / 发送缓冲)
或:       [hel][lo][world]        ← 拆分(MSS / 窗口不足)

接收方一次 read 可能得到:
  "helloworld"        合并
  "hello" + "world"   恰好对齐
  "hel" + "loworld"   拆开
现象触发原因代价
合并Nagle 算法攒小包、发送缓冲合并延迟最多增加约 40 ms
合并接收缓冲里堆了多个消息,一次 read 全读走业务必须自己切分
拆分超过 MSS,或路径 MTU 触发 IP 分片解析要支持半包
拆分拥塞窗口 / 接收窗口只允许发一部分大消息必然被切开

所以"粘包"这个叫法本身不准确:TCP 里没有"包",只有连续的字节,边界必须由协议自己定义。

2. 三种划界方案

定长:     [64B 消息][64B 消息][64B 消息]
分隔符:   {"a":1}\n{"b":2}\n{"c":3}\n
长度前缀: [len=5][hello][len=5][world]
方案优点缺点适用
定长解析最简单,可随机访问短消息浪费带宽,长度不灵活传感器、固定格式协议
分隔符可读性好,抓包易排查内容出现分隔符要转义,需防超长日志、行协议(Redis)
长度前缀可预分配,天然支持二进制要约定字节序与上限RPC、IM、私有协议

3. 用长度前缀写一个编解码器

const maxFrame = 1 << 20 // 1 MB,防止超长帧把内存打爆

func WriteFrame(w io.Writer, payload []byte) error {
	var hdr [4]byte
	binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
	if _, err := w.Write(hdr[:]); err != nil {
		return err
	}
	_, err := w.Write(payload)
	return err
}

func ReadFrame(r io.Reader) ([]byte, error) {
	var hdr [4]byte
	if _, err := io.ReadFull(r, hdr[:]); err != nil { // 一次 Read 不保证读满
		return nil, err
	}
	n := binary.BigEndian.Uint32(hdr[:])
	if n == 0 || n > maxFrame {
		return nil, fmt.Errorf("invalid frame length: %d", n)
	}
	buf := make([]byte, n)
	if _, err := io.ReadFull(r, buf); err != nil {
		return nil, err
	}
	return buf, nil
}

两个细节最容易被忽略:用 io.ReadFull 而不是 conn.Read,以及校验长度上限。 前者决定高延迟或小包场景下会不会偶发解析错位,后者决定一个恶意长度字段能不能让进程 OOM。

4. 常见追问

Q:UDP 有粘包吗? 没有。UDP 面向报文,一次发送对应一次接收,边界由协议保留。 但报文超过 MTU 会 IP 分片,丢一片整个报文作废,所以业务通常把单报控制在 1200~1400 字节。

Q:为什么 HTTP 不需要业务代码处理粘包? HTTP 自带定界:Content-Length 给出长度,Transfer-Encoding: chunked 用块长度,HTTP/2 每帧有 9 字节帧头。 框架把这些解析完了,所以业务层感觉不到边界问题。

Q:Nagle 算法要不要关? 延迟敏感的小包协议(游戏、RPC、IM)通常设 TCP_NODELAY,避免最多 40 ms 的攒包等待; 大流量顺序写则保留 Nagle 更省带宽。Nagle 与延迟 ACK 叠加时会出现经典的"等 40 ms"卡顿。

Q:分隔符方案要注意什么? 内容里出现分隔符要转义,或干脆改用长度前缀;还要限制单条消息的最大长度, 否则客户端可以一直不发分隔符,把服务端缓冲撑爆。

Q:为什么自定义协议偏爱长度前缀? 可以一次预分配缓冲、支持二进制、解析无歧义,还能顺带塞版本号与校验和,后续扩展成本最低。

Q:半包处理放在哪一层? 通常放在编解码器(codec)里,用 bufio.Reader + 状态机维护"已收到多少、还差多少", 业务层只面对完整的 frame,避免每个 handler 都重复处理半包逻辑。

小结

TCP 只保证字节按序到达,不保证"消息"完整到达,边界是应用层协议的责任。 三种方案里长度前缀最通用;实现时盯住两点:io.ReadFull 读满、长度上限设防。 凡是出现"偶发解析错位""高延迟下必现"的故障,先怀疑代码把一次 Read 当成了一条消息。

笔记加载中…