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 粘包拆包的核心逻辑图
下面这张图,把粘包和拆包的核心流程画清楚了。我建议你多看两眼,理解数据在发送端、内核、接收端之间的流转。
3.5 小结:粘包拆包的本质
通过这几个例子,你应该能感受到:
- 粘包:多个消息被合并,接收方一次读到多个消息
- 拆包:一个消息被拆分,接收方需要多次读取
- Nagle 算法:加剧了小消息的粘包,但提高了网络利用率
嗯,这里要注意:粘包和拆包不是 bug,是 TCP 流式传输的固有特性。你不能指望 TCP 帮你分消息。应用层必须自己设计协议——比如加长度头、用分隔符、或者固定消息长度。