Skip to content

第6章 第一次抓包

在“海量报文”中发现问题

启动 Wireshark 后,屏幕上通常会瞬间涌入大量的 DNS 查询、局域网发现、浏览器后台连接等无关流量。即便我们的测试程序只发送了一句简单的“你好,TCP”,在底层依然会触发连接建立、数据确认、连接关闭等多组 TCP 报文。

本章的目标是从这片流量汪洋中,精准定位第5章运行的程序,并将代码的执行与网络报文在时间线上逐一对齐。在正式动手前,我们可以先做几个预测:

  • 服务端执行 listen 时,网络上会立刻出现 TCP 报文吗?
  • 客户端的 connect 成功返回前,抓包里能看到哪些 TCP 标志位?
  • 客户端只调用了一次 sendall(16 bytes),抓包时是否一定只对应一个携带16字节数据的 TCP 报文段 (Segment)?
  • 程序结束触发断开连接时,固定的会产生几个报文?

带着这些疑问,我们将在抓包实验中逐一验证。

选对抓包接口 (Interface) 决定你能看到什么

本次实验我们将服务端和客户端都运行在本地,使用 127.0.0.1 通信。由于数据只在本地 TCP/IP 协议栈内部流转,并不会真正通过物理网卡发出,因此不能抓取无线网卡或以太网卡的流量。

如果在 Windows 下安装了 Npcap,Wireshark 的首页会显示名为 Adapter for loopback traffic capture(或 Npcap Loopback Adapter)的虚拟接口。只有抓取这个回环接口,才能看到 127.0.0.1 两端的 TCP 交互报文。

如果你将程序改成了两台电脑间通信,就必须选择实际承载流量的物理网卡(如 Wi-Fi 或局域网接口),并在防火墙允许的前提下进行实验。

提示:抓包文件会记录真实的明文网络数据。因此,在日常抓包排查问题时,请务必使用测试账号和无意义的测试文本,避免真实密码或个人敏感数据泄露到 .pcap 文件中。

捕获过滤器与显示过滤器

在面对海量报文时,Wireshark 提供了两道强大的过滤防线:

  • 捕获过滤器 (Capture Filter):在数据写入内存或硬盘生效,语法采用经典的 BPF 语法。例如 tcp port 50007 表示只抓取该端口的流量,不符合条件的报文会直接丢弃。
  • 显示过滤器 (Display Filter):在已抓取的数据包进行筛选,使用 Wireshark 专属语法。例如 tcp.port == 50007 表示保留所有抓好的报文,只是在界面上仅展示该端口相关的记录。

初次实验建议先抓取全量回环流量,再用显示过滤器慢慢找。这样做的好处是,即使过滤条件写错了,原始数据依然都在,随时可以重来。如果在高并发服务器上抓包,为了避免抓包文件过大,就必须在捕获前设置好 tcp port 50007

完整实验步骤

第一步:确认端口当前空闲

在 PowerShell 中运行以下命令,检查我们准备使用的 50007 端口是否被占用:

powershell
Get-NetTCPConnection -LocalPort 50007 -State Listen -ErrorAction SilentlyContinue

如果没有任何输出,说明当前没有服务在监听该端口。当然,最终能否绑定成功还是要看代码中 bind 的执行结果。若该端口碰巧被占用,请在 server.pyclient.py 中同步修改为其他端口,并调整后续的过滤规则。

第二步:启动 Wireshark 捕获

  1. 打开 Wireshark。
  2. 在首页接口列表中找到回环捕获接口 (Loopback Adapter)。
  3. 双击该接口,Wireshark 将立即开始捕获。
  4. 确认捕获已经开始(如果本地网络比较安静,数据包计数可能暂时不会跳动,这是正常现象)。

第三步:运行服务端与客户端

先打开一个 PowerShell 窗口,启动第5章的服务端:

powershell
python -X utf8 .\server.py

再打开另一个 PowerShell 窗口,启动客户端:

powershell
python -X utf8 .\client.py

等待双方日志输出完毕。请留意并记录服务端日志中 accept() 发生的时间、客户端日志中 connect() 发生的时间,以及客户端分配到的临时端口号

第四步:停止捕获并筛选端口

回到 Wireshark,点击左上角的红色方块停止按钮。在顶部的显示过滤器栏输入:

text
tcp.port == 50007

按下回车应用后,报文列表应该变得非常干净,仅剩下本次实验的连接。如果你重复运行了多次程序,可能会看到多组 TCP 交互流(客户端每次都会分配不同的临时端口)。

第五步:锁定唯一 TCP 流

为了避免多条连接互相干扰,我们需要聚焦到单条 TCP 流上。 选中一条属于本次最新运行的报文,展开中间详情窗格的 Transmission Control Protocol 层,找到 Stream index 字段。假设这个值是 3,我们在显示过滤器中追加或修改为:

text
tcp.stream == 3

tcp.stream 是 Wireshark 为了方便分析,在当前抓包文件中为每条完整的 TCP 连接自动分配的编号。使用它过滤,可以保证你看到的永远是同一条双向连接的报文。

此外,你还可以右键点击任意一条实验报文,选择 Follow → TCP Stream。Wireshark 会弹出一个窗口,按红蓝两色展示双向重组后的应用层字节流。由于我们在代码里加了4字节的二进制长度前缀(不可打印,十六进制显示为 00 00 00 0c),后面紧跟的就是 UTF-8 编码的“你好,TCP”。两端各发送了一份。

提示:Follow TCP Stream 展示的是 Wireshark 作为分析器对流量进行重组后的结果。颜色代表传输方向,而应用消息的边界实际上是由我们程序自己定义的 4字节长度字段决定的。

认识 TCP 报文的关键字段

选中任意一条 TCP 报文,Wireshark 核心窗格会展示以下关键信息:

  • Source / Destination:源 IP 与目标 IP 地址;
  • Src Port / Dst Port:源端口与目标端口号;
  • Flags:控制标志位,如 SYN、ACK、FIN、RST 等,决定了 TCP 状态机的走向;
  • Seq / Ack:序列号 (Sequence Number) 与确认号 (Acknowledgment Number);
  • Len (或 tcp.len):该 TCP 报文段 (Segment) 实际承载的应用层数据字节数
  • Stream index:Wireshark 给这条连接分配的本地流编号。

重点:真实网络中的初始序列号是一个随机生成的32位整数,非常难记。为了方便人类阅读,Wireshark 默认会将其换算成相对序列号 (Relative Sequence Number)。因此,握手的第一个 SYN 报文通常会显示友好的 Seq=0,紧接着的 SYN+ACK 会显示 Seq=0, Ack=1。关于相对值和绝对值的换算,我们会在第10章详细讲解。

一次完整的连接时间线

在本地回环、无丢包的理想环境下,一次典型的 TCP 通信通常会呈现以下顺序(C 代表客户端,S 代表服务端):

方向常见标志 (Flags)tcp.len当前意义
C → SSYN0客户端发起连接请求
S → CSYN, ACK0服务端确认请求,并发出自己的连接参数
C → SACK0客户端确认服务端的参数,三次握手完成
C → SPSH, ACK 或 ACK16客户端发送 4字节长度 + 12字节应用数据
S → CACK0服务端的 TCP 层返回数据确认(有时会与下一条响应合并)
S → CPSH, ACK 或 ACK16服务端回显相同的应用数据
C → SACK0客户端的 TCP 层确认收到响应(有时会与下一条 FIN 合并)
双向FIN, ACK 与 ACK 的组合0双方各自发送 FIN 结束报文,四次挥手断开

请注意,上表只是一种“常见”的观测结果,而非死板的规定: TCP 协议允许将“确认 (ACK)”与“数据”合并发送;三次握手的最后一次 ACK 甚至可以直接携带应用数据;FIN 报文也可以和 ACK 甚至数据合并。操作系统的调度策略、TCP 延迟确认机制 (Delayed ACK)、发送时机以及网卡硬件卸载功能,都会让最终抓到的报文行数有所不同。

例如,第5章的客户端调用 sendall 一次性传入了 16 字节的数据。在回环实验中,它通常作为一个完整的 16 字节 TCP 报文段被发送。但协议同样允许将其拆分成多个更小的报文段发送;无论底层怎么拆,接收端我们自己封装的 recv_exact 都能完美应对。

用过滤器精准回答特定问题

(在下面的过滤器中,请将 N 替换为你实际环境的流编号,如 tcp.stream == 3

寻找握手相关报文

text
tcp.stream == N && tcp.flags.syn == 1

如果只想看最初由客户端发起的那个 SYN 报文

text
tcp.stream == N && tcp.flags.syn == 1 && tcp.flags.ack == 0

寻找携带应用层数据的报文

text
tcp.stream == N && tcp.len > 0

选中那条客户端发出的 16 字节报文,在 Wireshark 下方的 TCP payloadData 区域,你就能直观地看到我们构造的长度字段与 UTF-8 字节。如果这 16 字节被分成了多段,这几段的 tcp.len 总和必定等于完整帧的长度。

寻找正常关闭与强制重置报文

text
tcp.stream == N && tcp.flags.fin == 1
text
tcp.stream == N && tcp.flags.reset == 1

代码正常执行完毕退出时,通常能抓到双方优雅关闭的 FIN 报文,此时通过 RST 过滤器往往什么也搜不到。至于在什么场景下会触发粗暴的 RST 断连,我们将留到第19章专门探讨。

寻找分析器推测的丢包重传

text
tcp.stream == N && tcp.analysis.retransmission

在极其稳定的回环网络中,这个过滤器的结果通常为空。需要强调的是,tcp.analysis.retransmission 是 Wireshark 分析器根据当前的抓包上下文推断出来的一个虚拟标记,并非网络报文实际携带的字段。如果抓包是从连接中途开始的、或者抓包工具自身漏抓了报文、亦或是网卡开启了卸载特性,都有可能导致 Wireshark 产生误判。因此,在诊断问题时,这个标记只能作为参考,必须结合序列号、时间间隔等硬指标综合判定。

将 API 调用栈与报文抓取对齐

程序日志记录的是**应用层(用户态 API)的调用时间,而 Wireshark 记录的是内核网络栈(捕获点)**看到报文的时间。它们观察的是紧密相邻的系统分层,我们可以非常自然地建立如下映射关系:

程序事件抓包中的对应现象证据所在层次
服务端 listen 返回抓包列表通常保持原样(没有报文产生)Socket API 与操作系统内部状态
客户端进入并完成 connect出现 SYN、SYN+ACK、ACK 三次握手API 与线上 TCP 连接建立
服务端 accept 返回握手早已完成,连接已放入就绪队列供应用提取API 与内核监听队列
客户端 sendall(16 bytes) 返回抓包中出现合计16字节、发往服务端的 TCP 数据段API 与线上实际报文
服务端 recv_frame 返回16字节已被组装成完整帧,交给了服务端应用态应用层边界读取
服务端 sendall(16 bytes) 返回抓包中出现合计16字节、发往客户端的 TCP 数据段API 与线上实际报文
Socket 上下文管理器结束看到 FIN 挥手,或释放连接相关的组合报文API 与 TCP 状态机迁移

提示:对比日志和抓包时间戳时,允许存在细微的延迟差异。 应用层的 sendall 返回,仅仅意味着应用已将数据成功塞入内核发送缓冲区,此时报文可能已经或者即将在网卡上发送。而对端日志里的 recv 返回,则意味着网卡早已收到数据并经过了 TCP 重组,终于交付到了应用手中。将上述三者(发送端 API、线上抓包、接收端 API)按时间线拼合在一起,一条完整的网络请求路径就彻底清晰了。

常见排障与观测偏差

抓包列表空空如也

首先确认监听接口。只要目标地址是 127.0.0.1,就必须选用 Loopback 回环接口。确认无误后,清空显示过滤器,看看 Wireshark 到底有没有抓到任何其他背景流量,最后再仔细核对程序实际绑定的端口号。

怎么重新捕获完整的握手过程?

如果 Wireshark 是在客户端启动之后才开始抓包,就会错过前面的三次握手。解决办法很简单:保持 Wireshark 捕获状态,重启一遍服务端和客户端进程即可。一个标准的抓包分析必须包含从首个 SYN 发起到最后连接关闭的完整生命周期。

同一个端口为什么有好几组流?

每次重新运行客户端,操作系统都会给它分配一个新的随机临时端口。请先通过终端日志找到最新一次的客户端临时端口号(即四元组),定位到对应报文后,再使用该报文的 tcp.stream == N 过滤,这样就能过滤掉历史干扰。

实际看到的数据报文数量和预期的不一样?

如前文所述,TCP 的 ACK 合并、数据分段(Segmentation)、FIN 合并以及操作系统复杂的调度策略,都会导致报文在物理层面的打包方式产生变化。此时不要纠结于具体的“行数”,而是应该沿着相对序列号(字节范围)去确认应用数据的收发是否完整。

Wireshark 为什么大面积提示校验和 (Checksum) 错误?

在本地抓包时,由于数据还没有真正走出网卡,抓包工具可能会在网卡硬件(Checksum Offload)或者协议栈计算校验和之前截获报文,导致 Wireshark 报错。这个问题完全正常,第13章我们会结合“伪首部”和硬件卸载功能做深入解析。

保存实验材料

为了养成良好的排障习惯,建议给本次实验的材料起个清晰的文件名并妥善保存:

text
tcp-ch06-server.log
tcp-ch06-client.log
tcp-ch06-connections.txt
tcp-ch06-loopback.pcapng

抓包文件可以通过 Wireshark 的 File → Save As 保存为 .pcapng 格式。 程序的运行日志可以使用 PowerShell 的 Tee-Object 命令,在屏幕显示的同时写入文件:

powershell
python -X utf8 .\server.py 2>&1 | Tee-Object -FilePath .\tcp-ch06-server.log -Encoding UTF8

客户端也采用同样的方式重定向日志。由于本次实验的程序执行速度极快,连接转瞬即逝,你也可以换成第5章中“先完成握手,后调用 accept”的带断点暂停版本,从而有充足的时间在终端查询连接状态,保存完证据后再按回车继续。

理解检查

  1. 程序连接 127.0.0.1 时应优先选择哪类抓包接口?
  2. tcp port 50007tcp.port == 50007 分别属于哪类过滤器?
  3. 同一抓包里多次运行程序,怎样锁定其中一条连接?
  4. tcp.len == 0 的报文是否仍可能推进连接状态?
  5. tcp.analysis.retransmission 属于线上首部字段还是分析器推断?
  6. Follow TCP Stream 中的颜色与应用消息边界分别由什么决定?

参考答案

  1. 应选择 Npcap 提供的 Windows 回环捕获接口 (Adapter for loopback traffic capture)。
  2. 前者(采用 BPF 语法)是捕获过滤器,后者(采用 Wireshark 专属语法)是显示过滤器
  3. 先通过应用日志里的四元组(特别是客户端临时端口)找到报文,再利用该报文关联的 tcp.stream 编号进行专门过滤。
  4. 完全可以。比如纯 SYN、纯 ACK、FIN 等控制报文,其负载数据长度常常为 0,但它们携带的标志位和序列号空间依然在有效推进 TCP 状态机流转。
  5. 它是 Wireshark 作为应用层工具,基于抓包上下文智能生成的分析推测标记,并非真实的 TCP 报文首部字段。
  6. Follow 界面中,颜色仅用来区分 TCP 报文的两端传输方向;而真正的应用层消息边界,是由本程序自行设计的 4 字节长度字段(应用协议)决定的。

本章小结

经过第一次抓包实战,我们可以将网络排查的核心方法浓缩为四个标准动作:选对抓包点用端口号筛选候选流用四元组和 tcp.stream 锁定唯一连接,最后将报文的时间线与两端日志对齐

完成这套动作后,应用层的 Socket API 调用、操作系统内核的连接状态迁移,以及线上的真实 TCP 报文,就完美融合成了一张透明的运行剖面图。在第三篇中,我们将复用这份抓好的报文,并在此基础上,带你逐字逐句拆解每一个 TCP 报文首部字段的奥秘。

上一章:第5章 用一个最小程序建立 TCP 连接 · 所属篇:第二篇 · 下一章:第7章 网络包的分层结构 · 教程总览

Licensed under CC BY-NC-SA 4.0.