4. 抓包实战准备:编写一个会粘包的Demo
好,到了真正动手的时候了。
前面我们聊了那么多理论,什么Nagle算法、MSS、延迟确认……说实话,光看这些概念,你很难真正理解粘包到底长什么样。我个人习惯是:先写个会出问题的Demo,亲眼看到它粘了,再回头分析原因,印象才深刻。
这一节我们就来干三件事:
- 写一个服务端,故意不处理边界
- 写一个客户端,疯狂发小包
- 启动Wireshark,把粘包现场抓下来
核心目标:制造一个“发送方发了10条消息,接收方只读到3次”的典型粘包场景。
4.1 服务端代码:一个“偷懒”的接收者
先看服务端。我故意让它每次收到数据就直接打印,不做任何拆包逻辑。
# server.py - 粘包演示服务端
import socket
def start_server(host='0.0.0.0', port=8888):
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind((host, port))
server_sock.listen(5)
print(f"[Server] 监听 {host}:{port}")
while True:
conn, addr = server_sock.accept()
print(f"[Server] 客户端连接: {addr}")
while True:
data = conn.recv(1024) # 一次最多读1024字节
if not data:
break
# 注意:这里直接打印,没有按消息边界拆分
print(f"[Server] 收到 {len(data)} 字节: {data}")
conn.close()
if __name__ == '__main__':
start_server()
这段代码有什么问题?recv(1024) 的意思是“最多读1024字节”,但TCP是流协议,它不会告诉你“这是一条完整的消息”。如果客户端连续发了10个100字节的小包,服务端可能一次recv就把它们全收进来了——这就是粘包。
我的经验:早期我做网关项目时,就吃过这个亏。客户端明明发了100条交易记录,服务端只收到了30次recv事件,每条里面都粘着好几条记录。当时排查了一整天,最后才发现是recv缓冲区大小设得太大了。
4.2 客户端代码:一个“急性子”的发送者
客户端这边,我让它连续发送10条短消息,每条之间只间隔极短的时间(甚至不间隔)。
# client.py - 粘包演示客户端
import socket
import time
def start_client(host='127.0.0.1', port=8888):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
print(f"[Client] 已连接 {host}:{port}")
messages = [f"MSG-{i:03d}: Hello TCP World!" for i in range(1, 11)]
for msg in messages:
data = msg.encode('utf-8')
sock.send(data) # 连续发送,不等待
print(f"[Client] 发送: {msg} ({len(data)} 字节)")
# 故意不加延迟,让多个小包在TCP缓冲区里合并
# time.sleep(0.01) # 如果取消注释,粘包概率会降低
# 等待一会,让服务端处理完
time.sleep(1)
sock.close()
if __name__ == '__main__':
start_client()
关键点在于 time.sleep(0.01) 被注释掉了。这意味着10条消息会在极短时间内全部塞进TCP发送缓冲区。你想想看,TCP为了效率,会把它们合并成一个大的TCP段发出去——粘包就这样产生了。
注意:如果你在客户端加了sleep,粘包现象可能就不明显了。因为每条消息之间有足够的时间间隔,TCP会把它们分别封装成独立的报文段。这也是为什么很多初学者在本地测试时没发现问题,一上生产环境就崩了——生产环境的网络延迟和缓冲区大小跟本地完全不一样。
4.3 启动抓包:用Wireshark捕获现场
代码写好了,接下来就是见证奇迹的时刻。启动Wireshark,抓取本机回环接口(loopback)上的流量。
操作步骤:
- 打开Wireshark,选择 Adapter for loopback traffic capture(Windows)或 lo0(macOS/Linux)
- 设置过滤条件:
tcp.port == 8888 - 先启动服务端
python server.py - 再启动客户端
python client.py - 观察Wireshark中的数据包列表
你会看到什么?客户端明明调用了10次 send(),但在Wireshark里可能只看到3~4个TCP数据段。每个数据段里都包含了多条消息。
4.4 粘包现象的直观对比
为了让你看得更清楚,我整理了一个对比表:
| 维度 | 理想情况(无粘包) | 实际情况(有粘包) |
|---|---|---|
| 客户端send次数 | 10次 | 10次 |
| 服务端recv次数 | 10次 | 3~5次 |
| 每次recv的数据内容 | 1条完整消息 | 多条消息拼接在一起 |
| Wireshark中TCP段数 | 10个 | 3~5个 |
| 应用层能否正确解析 | 能 | 不能(需要拆包) |
为什么会这样?说白了就是TCP的流式特性 + Nagle算法 + 接收缓冲区共同作用的结果。客户端发得快,服务端读得慢,数据就在缓冲区里堆起来了。
4.5 核心逻辑流程图
下面这张图展示了从客户端send到服务端recv的完整路径,以及粘包发生的位置:
4.6 运行结果验证
启动服务端和客户端后,你会看到类似这样的输出:
服务端输出示例:
[Server] 监听 0.0.0.0:8888
[Server] 客户端连接: ('127.0.0.1', 54321)
[Server] 收到 320 字节: b'MSG-001: Hello TCP World!MSG-002: Hello TCP World!MSG-003: Hello TCP World!MSG-004: Hello TCP World!'
[Server] 收到 240 字节: b'MSG-005: Hello TCP World!MSG-006: Hello TCP World!MSG-007: Hello TCP World!'
[Server] 收到 160 字节: b'MSG-008: Hello TCP World!MSG-009: Hello TCP World!MSG-010: Hello TCP World!'
看到了吗?客户端明明发了10条消息,服务端只收到了3次。而且每次收到的数据里都包含了多条消息的拼接——这就是典型的粘包现象。
避坑指南:我曾经在调试一个金融交易系统时,发现服务端偶尔会解析出“半条消息”。后来抓包一看,不仅是粘包,还有拆包——消息被TCP切成了两半。所以粘包和拆包往往是同时存在的,我们后面会专门讲拆包的处理。
4.7 小结
这一节我们亲手制造了一个粘包现场。你有了这个Demo,后面分析Wireshark抓包结果时,就能对照着看:哪些包是粘在一起的,为什么粘,粘了多少条消息。
记住这个感觉——当你在Wireshark里看到服务端只recv了3次,而客户端send了10次时,你就知道:粘包来了。