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)上的流量。

操作步骤:

  1. 打开Wireshark,选择 Adapter for loopback traffic capture(Windows)或 lo0(macOS/Linux)
  2. 设置过滤条件:tcp.port == 8888
  3. 先启动服务端 python server.py
  4. 再启动客户端 python client.py
  5. 观察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的完整路径,以及粘包发生的位置:

粘包产生过程示意图 客户端应用层 10次 send() 调用 客户端TCP发送缓冲区 Nagle算法合并小包 网络传输(TCP段) 实际只发了3~5个段 服务端TCP接收缓冲区 数据堆积等待读取 服务端应用层 3~5次 recv() 返回 10条小消息 合并后发送 接收 多条粘在一起 粘包发生的根本原因 1. 客户端连续send,Nagle算法将小包合并为一个TCP段 2. 服务端recv(1024)一次性读取了多个消息的拼接数据 3. 应用层没有定义消息边界,无法区分每条消息的起止位置

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次时,你就知道:粘包来了。