3. 粘包现象演示:用Python写一个简单的Server/Client,发送多条小数据,观察粘包
理论讲完了,咱们直接动手。我习惯先写一个会出问题的Demo,亲眼看看粘包长什么样,比背一百遍定义都管用。
你想想看,如果TCP不会粘包,那网络编程就简单多了。但现实是——它偏偏会。这一节我们就用Python的socket模块,写一个最朴素的Server和Client,故意制造粘包场景。
3.1 先搭一个最简单的Server
Server这边,我让它接收数据,然后原样打印出来。不做任何拆包逻辑,纯粹观察。
# server.py
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8888))
server.listen(5)
print('Server listening on 127.0.0.1:8888')
conn, addr = server.accept()
print(f'Client connected: {addr}')
# 循环接收,每次最多收1024字节
while True:
data = conn.recv(1024)
if not data:
break
print(f'Received: {data}')
conn.close()
server.close()
这段代码很简单。注意recv(1024)——我设了一个缓冲区大小。但TCP不保证一次recv正好拿到一条完整消息。
3.2 Client连续发送多条小数据
Client这边,我连续发送3条短消息。每条消息之间不加任何间隔或分隔符。
# client.py
import socket
import time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))
# 连续发送3条小数据
client.send(b'Hello')
time.sleep(0.1)
client.send(b'World')
time.sleep(0.1)
client.send(b'Python')
client.close()
这里我故意加了time.sleep(0.1),让三次send之间有点间隔。但即便如此,粘包依然会发生。为什么?往下看。
3.3 运行结果:粘包出现了
先启动Server,再启动Client。Server端打印的结果是:
Server listening on 127.0.0.1:8888
Client connected: ('127.0.0.1', 54321)
Received: b'HelloWorldPython'
看到了吗?三条消息粘在一起了。Hello、World、Python被合并成了一条数据。这就是典型的粘包。
关键观察:Client调用了3次send,Server只触发了1次recv。3条消息被TCP合并成了一个包。
3.4 为什么会粘包?
我当年第一次看到这个结果时也很困惑。明明send了三次,怎么Server只收到一次?
原因其实不复杂。TCP是流式协议,它不关心你应用层怎么切分消息。它只负责把字节流从A端搬到B端。至于哪些字节属于哪条消息——TCP不管。
具体来说,粘包发生在两个层面:
| 层面 | 原因 | 说明 |
|---|---|---|
| 发送端 | Nagle算法 | TCP会把多个小数据合并成一个报文段再发送,提高网络利用率 |
| 接收端 | 缓冲区合并 | 接收方缓冲区积压了多条数据,应用层一次recv全读走了 |
说白了,TCP就像一根水管。你往里面倒三杯水,但水在水管里是连成一片的。另一端接水的人,不知道哪口水是第几杯的。
我的经验:Nagle算法在大多数场景下是好事,它能减少小包数量。但在实时性要求高的场景(比如游戏、音视频),我通常会把它关掉:client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)。
3.5 粘包与拆包的关系
有粘包就有拆包。拆包是TCP的另一个特性——一条大消息可能被拆成多个TCP段发送。
比如你send了一个10KB的数据,TCP可能把它拆成两个包:一个6KB,一个4KB。Server第一次recv只拿到6KB,第二次recv才拿到剩下的4KB。
粘包和拆包,本质上是同一个问题的两面:TCP不保证应用层消息的边界。
我曾经踩过的坑:刚做网络编程时,我天真地以为每次recv都能拿到一条完整的消息。结果线上服务频繁出现解析错误。后来才发现,有时候一条消息被拆成了两半,有时候多条消息粘在一起。从那以后,我再也不敢依赖TCP的消息边界了。
3.6 用一张图看清粘包过程
下面这张SVG图,展示了Client三次send和Server一次recv的完整流程。你可以直观地看到数据是怎么粘在一起的。
3.7 换个角度:拆包现象
刚才演示的是粘包。那拆包呢?其实只要把Client的send数据改大一点就能看到。比如send一个100KB的数据,Server用recv(1024)去收,肯定要收很多次才能收完。
我建议你自己试一下:把Client改成client.send(b'A' * 100000),然后看Server会触发多少次recv。结果会让你印象深刻。
一个小技巧:调试粘包/拆包问题时,我经常在Server端打印每次recv的数据长度。如果长度忽大忽小,基本可以断定发生了粘包或拆包。
3.8 小结:记住两个核心结论
- TCP是流,不是消息——它不保留应用层的消息边界
- 粘包和拆包是常态,不是异常——任何TCP通信都可能发生