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次send → 1次recv Client (发送端) Server (接收端) send: "Hello" TCP流 send: "World" send: "Python" 接收缓冲区 HelloWorldPython (3条数据粘在一起) recv(1024) 结果:3次send → 1次recv,数据粘包 Client发送了3条独立消息,但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通信都可能发生