2. 粘包拆包问题演示:用 Python socket 编写一个简单的客户端和服务端,演示粘包现象。
理论说再多,不如动手跑一遍。这一章,我们就来亲手制造一个粘包现场。
我会带着你写一个最简的 TCP 服务端和客户端。代码不长,但能让你亲眼看到:明明发送了两次数据,接收端却只收到一次——这就是粘包。
2.1 先搭个服务端骨架
我个人习惯,写网络程序先从服务端开始。因为服务端是“守株待兔”的那一方,逻辑更直观。
下面这个服务端,监听本地的 8888 端口。收到客户端连接后,它什么都不做,只是把收到的数据原样打印出来。
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)
print('服务端已启动,等待连接...')
conn, addr = server_socket.accept()
print(f'客户端已连接:{addr}')
# 接收数据
data = conn.recv(1024)
print(f'服务端收到:{data}')
# 再收一次
data = conn.recv(1024)
print(f'服务端再次收到:{data}')
conn.close()
server_socket.close()
注意看,我调用了两次 recv(1024)。按常理,如果客户端发送了两次数据,服务端就应该收到两次。但结果会让你意外。
2.2 再写个客户端
客户端更简单。连接服务端后,连续发送两条消息。
import socket
import time
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 8888))
# 发送两条消息
client_socket.send(b'Hello, ')
client_socket.send(b'World!')
# 等一会再关闭,确保服务端能收到
time.sleep(1)
client_socket.close()
这里我故意把 "Hello, " 和 "World!" 分两次发送。你想想看,如果 TCP 是“发一次收一次”,服务端应该分别打印出这两段。但事实呢?
2.3 运行结果:粘包出现了
先启动服务端,再启动客户端。控制台输出如下:
服务端已启动,等待连接...
客户端已连接:('127.0.0.1', 54321)
服务端收到:b'Hello, World!'
服务端再次收到:b''
看到了吗?第一次 recv 就把两条消息全收走了。第二次 recv 只拿到了空数据。
为什么会这样?因为 TCP 是流式协议。它不关心你分了几次 send,只关心字节流。两次发送的数据在传输层被合并成了一个包,接收端一次性全读走了。
核心结论:TCP 粘包的本质,是接收端无法区分消息的边界。你分两次发,它可能一次收;你分一次发,它也可能分两次收(拆包)。
2.4 换个角度:拆包又是怎么回事?
刚才演示的是粘包。那拆包呢?其实原理一样,只是方向相反。
我举个例子。你在客户端一次发送 5000 字节的数据。服务端 recv(1024) 一次最多读 1024 字节。那剩下的 3976 字节怎么办?
答案是:它们留在内核缓冲区里,等下一次 recv 再读。这就叫拆包——一次发送,多次接收。
| 现象 | 发送端行为 | 接收端行为 | 本质原因 |
|---|---|---|---|
| 粘包 | 多次 send | 一次 recv 收完 | Nagle 算法 / 缓冲区合并 |
| 拆包 | 一次 send | 多次 recv 才能收完 | TCP 缓冲区大小限制 |
嗯,这里要注意:粘包和拆包不是 bug,是 TCP 的固有特性。你没法“关闭”它,只能“处理”它。
2.5 用一张图看清整个过程
下面这张 SVG 流程图,展示了客户端两次 send 到服务端两次 recv 的完整路径。你可以看到数据在传输层是如何被合并的。
一个小技巧:你可以在客户端两次 send 之间加一个 time.sleep(0.5),再运行看看。有时候延迟足够大,粘包就不会发生。但这只是侥幸,不能依赖。
2.6 避坑指南:我曾经踩过的坑
我曾经在一个物联网项目里,用 TCP 传输传感器数据。设备每 100 毫秒发一次数据,服务端用 recv(1024) 接收。结果呢?服务端经常一次收到好几条数据,解析出来全是乱码。
当时我第一反应是“数据丢了”,查了半天网卡、抓包、看日志。最后才发现,不是丢了,是粘了——多条数据粘在一起,解析逻辑没处理好。
从那以后,我养成了一个习惯:只要用 TCP,就必须考虑粘包拆包。不管协议多简单,都得加边界标记。
警告:千万不要在 send 之后立刻 close 连接。TCP 的 close 会触发 FIN 包,可能导致数据还没发完就被截断。我见过不少新手因为这个丢数据。
2.7 小结
这一章,我们用不到 30 行代码,亲手复现了粘包现象。你看到了:
- 两次
send被合并成一次recv收走 - 第二次
recv只能读到空数据 - 粘包和拆包是 TCP 的固有特性,不是 bug