第1章:粘包拆包现象演示——先看看问题长什么样
说实话,我教网络调试这么多年,发现很多开发者对粘包拆包的理解都停留在「听说过」的阶段。有人觉得这是底层的事,应用层不用管;有人一遇到数据对不上就怀疑是粘包。这两种想法,我都踩过坑。
这一章,咱们不聊理论,先动手。用 Python 写两个小脚本,亲眼看看 TCP 粘包和拆包到底是怎么回事。你只有亲眼见过问题,后面学解决方案时才不会发懵。
1.1 先搭个实验环境
我习惯用 Python 自带的 socket 模块做演示,干净、没依赖。你只要装了 Python 3.6+ 就能跑。
咱们要模拟的场景很简单:
- 服务端:接收数据,打印收到的内容
- 客户端:连续发送多条消息
但有个关键点——服务端每次只读固定字节数。这样才能模拟出「读多读少」的情况,拆包和粘包就藏在这里。
1.2 服务端代码:故意少读一点
import socket
def start_server(host='127.0.0.1', port=8888):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((host, port))
server.listen(5)
print(f'[服务端] 监听 {host}:{port}')
conn, addr = server.accept()
print(f'[服务端] 客户端已连接: {addr}')
while True:
# 故意只读 10 字节,模拟拆包
data = conn.recv(10)
if not data:
break
print(f'[服务端] 收到 {len(data)} 字节: {data}')
conn.close()
server.close()
if __name__ == '__main__':
start_server()
注意看 recv(10) 这一行。我故意把缓冲区设成 10 字节。如果客户端一次发 30 字节,服务端就得分 3 次才能读完——这就是拆包。
1.3 客户端代码:连续发,不等待
import socket
import time
def start_client(host='127.0.0.1', port=8888):
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
messages = [b'Hello', b'World', b'Python', b'TCP', b'Sticky']
for msg in messages:
client.send(msg)
print(f'[客户端] 发送: {msg}')
time.sleep(0.1) # 稍微间隔,但不保证不粘包
time.sleep(1)
client.close()
if __name__ == '__main__':
start_client()
这里我发了 5 条短消息,每条间隔 0.1 秒。你猜服务端会收到几条?
1.4 跑起来看看效果
先启动服务端,再启动客户端。输出大概是这样:
[服务端] 监听 127.0.0.1:8888
[服务端] 客户端已连接: ('127.0.0.1', 54321)
[服务端] 收到 10 字节: b'HelloWorld'
[服务端] 收到 10 字节: b'PythonTCP'
[服务端] 收到 5 字节: b'Sticky'
看到了吗?粘包出现了。客户端明明发了 5 条消息,服务端只收到 3 次。而且 Hello 和 World 粘在了一起,变成了 HelloWorld。
为什么会这样?
因为 TCP 是流式协议。它不关心你分几次 send,只关心字节流。如果发送间隔短、数据量小,TCP 会把多个小包合并成一个 TCP 段发送出去。这就是粘包的本质。
1.5 再试试拆包场景
把服务端的 recv(10) 改成 recv(3),再跑一次:
[服务端] 收到 3 字节: b'Hel'
[服务端] 收到 3 字节: b'loW'
[服务端] 收到 3 字节: b'orl'
[服务端] 收到 3 字节: b'dPy'
[服务端] 收到 3 字节: b'tho'
[服务端] 收到 3 字节: b'nTC'
[服务端] 收到 3 字节: b'PSt'
[服务端] 收到 2 字节: b'ic'
一条 HelloWorld 被拆成了 4 次接收。这就是拆包。
你想想看,如果这是业务数据,比如一条完整的 JSON 被拆成两半,你直接 json.loads() 肯定报错。我在项目中就遇到过这种问题——日志里全是 JSONDecodeError,排查了半天才发现是拆包导致的。
1.6 粘包拆包的本质对比
| 现象 | 发生原因 | 典型表现 |
|---|---|---|
| 粘包 | 多个小包被 TCP 合并发送 | 一次 recv 收到多条消息 |
| 拆包 | 一个大包被 TCP 分片发送 | 一次 recv 只收到半条消息 |
| 正常 | 消息大小和 TCP 段对齐 | 一次 recv 恰好一条消息 |
说白了,粘包和拆包是同一枚硬币的两面。它们都是 TCP 流式传输的必然结果,不是 bug,是特性。但如果你不做处理,这个特性就会变成你调试时的噩梦。
1.7 本章知识结构图
下面这张图帮你理清粘包拆包的核心逻辑:
1.8 小结
这一章我们只做了一件事:亲眼看到粘包和拆包。你不需要记住什么高深的理论,只需要记住两个画面:
- 粘包:多条消息粘在一起,一次收完
- 拆包:一条消息被拆成多段,分次接收
这两个画面,就是后面所有解决方案的出发点。下一章开始,我会带你一步步解决这个问题。但在此之前,我建议你亲手跑一遍上面的代码,改改 recv 的大小,看看不同参数下会有什么表现。