3. 粘包拆包示例:用 Python 亲手摸一摸

理论讲完了,咱们得动动手。我个人习惯是,学网络协议一定要亲手抓包、写代码验证。光看书总觉得懂了,一跑起来全是坑。

这一节,我用 Python 的 socket 模块,给你演示粘包和拆包到底长什么样。还会聊聊 Nagle 算法是怎么掺和进来的。

3.1 粘包现象:发送方太快,接收方来不及拆

先写一个最简单的服务端和客户端。服务端每次收 1024 字节,客户端连续发 3 条短消息。

# server.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 8888))
s.listen(1)
conn, addr = s.accept()

data = conn.recv(1024)
print(f"收到: {data}")

conn.close()
s.close()
# client.py
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))

s.send(b'Hello')
s.send(b'World')
s.send(b'TCP!')

s.close()

运行一下,服务端打印的是:

收到: b'HelloWorldTCP!'

看到了吗?三条消息粘在一起了。这就是典型的粘包。

核心原因:TCP 是流式协议,它不关心你分几次 send。内核缓冲区里数据攒够了,一次 recv 就全拿走了。

为什么会这样?因为服务端只 recv 了一次,而客户端三次 send 的数据在传输层被合并了。你想想看,如果这是两个不同的业务请求,服务端根本分不清哪段是 "Hello",哪段是 "World"。

3.2 拆包现象:消息太大,一次收不完

反过来,如果消息太大,TCP 会把它切成多个 TCP 段发送。接收方需要多次 recv 才能拼完整。

# server.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 8888))
s.listen(1)
conn, addr = s.accept()

# 分多次接收
parts = []
while True:
    chunk = conn.recv(1024)
    if not chunk:
        break
    parts.append(chunk)
    print(f"收到片段: {chunk}")

full_msg = b''.join(parts)
print(f"完整消息: {full_msg}")

conn.close()
s.close()
# client.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))

# 发一条很大的消息
big_msg = b'A' * 5000
s.send(big_msg)

s.close()

服务端输出可能是这样的:

收到片段: b'AAAA...'  (1024字节)
收到片段: b'AAAA...'  (1024字节)
收到片段: b'AAAA...'  (1024字节)
收到片段: b'AAAA...'  (1024字节)
收到片段: b'AAAA...'  (880字节)
完整消息: b'AAAA...'  (5000字节)

这就是拆包。一条 5000 字节的消息,被拆成了 5 个 TCP 段。服务端必须循环 recv,直到收完为止。

我曾经在做一个文件传输服务时,没处理好拆包。客户端发了个 64KB 的图片,服务端只 recv 了一次就认为收完了。结果图片只显示了左上角一小块。排查了半天才发现是拆包导致的。

3.3 Nagle 算法对粘包的影响

说到粘包,不得不提 Nagle 算法。它是 TCP 的一个优化策略,目的是减少小包数量。

Nagle 算法的规则很简单:

  • 如果发送的数据很小(小于 MSS),并且之前还有未确认的包,那就先攒着
  • 等到收到 ACK 了,或者攒到足够大了,再一起发出去

说白了,就是宁等勿发。这个算法对粘包有什么影响?

场景 Nagle 开启 Nagle 关闭
连续发送小消息 容易粘包,小包被合并 每个 send 立即发出,可能不粘
大消息 + 小消息 大消息可能等小消息一起发 大消息立即发,小消息也立即发
网络延迟 延迟增加,但带宽利用率高 延迟低,但可能产生大量小包

你可以用 TCP_NODELAY 选项关闭 Nagle 算法:

# 关闭 Nagle 算法
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

关闭之后,每次 send 都会立即发送。但注意,这并不能完全避免粘包。因为即使 Nagle 关了,TCP 的流式特性还在,接收方一次 recv 仍然可能读到多个 send 的数据。

我的建议:实时性要求高的场景(比如游戏、视频通话),关掉 Nagle。吞吐量优先的场景(比如文件传输、日志上报),保持默认开启。

3.4 粘包拆包的核心逻辑图

下面这张图,把粘包和拆包的核心流程画清楚了。我建议你多看两眼,理解数据在发送端、内核、接收端之间的流转。

TCP 粘包与拆包核心流程 发送端应用层 send("Hello") send("World") 发送端内核缓冲区 数据合并: "HelloWorld" Nagle 算法 (可能延迟合并) TCP 传输层 流式传输,无边界 可能拆包 (MSS 限制) 可能粘包 (Nagle + 延迟) 接收端内核缓冲区 数据到达,等待 recv 接收端应用层 recv(1024) → "HelloWorld" 粘包场景 多个 send 在内核合并,一次 recv 全收 常见于小消息 + Nagle 开启 拆包场景 大消息被 MSS 限制,拆成多个 TCP 段 接收端需多次 recv 才能拼完整 核心结论:TCP 是流,没有消息边界,粘包拆包是常态 应用层必须自己设计协议来区分消息边界

3.5 小结:粘包拆包的本质

通过这几个例子,你应该能感受到:

  • 粘包:多个消息被合并,接收方一次读到多个消息
  • 拆包:一个消息被拆分,接收方需要多次读取
  • Nagle 算法:加剧了小消息的粘包,但提高了网络利用率

嗯,这里要注意:粘包和拆包不是 bug,是 TCP 流式传输的固有特性。你不能指望 TCP 帮你分消息。应用层必须自己设计协议——比如加长度头、用分隔符、或者固定消息长度。