Appearance
第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.py 和 client.py 中同步修改为其他端口,并调整后续的过滤规则。
第二步:启动 Wireshark 捕获
- 打开 Wireshark。
- 在首页接口列表中找到回环捕获接口 (Loopback Adapter)。
- 双击该接口,Wireshark 将立即开始捕获。
- 确认捕获已经开始(如果本地网络比较安静,数据包计数可能暂时不会跳动,这是正常现象)。
第三步:运行服务端与客户端
先打开一个 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 == 3tcp.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 → S | SYN | 0 | 客户端发起连接请求 |
| S → C | SYN, ACK | 0 | 服务端确认请求,并发出自己的连接参数 |
| C → S | ACK | 0 | 客户端确认服务端的参数,三次握手完成 |
| C → S | PSH, ACK 或 ACK | 16 | 客户端发送 4字节长度 + 12字节应用数据 |
| S → C | ACK | 0 | 服务端的 TCP 层返回数据确认(有时会与下一条响应合并) |
| S → C | PSH, ACK 或 ACK | 16 | 服务端回显相同的应用数据 |
| C → S | ACK | 0 | 客户端的 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 payload 或 Data 区域,你就能直观地看到我们构造的长度字段与 UTF-8 字节。如果这 16 字节被分成了多段,这几段的 tcp.len 总和必定等于完整帧的长度。
寻找正常关闭与强制重置报文
text
tcp.stream == N && tcp.flags.fin == 1text
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”的带断点暂停版本,从而有充足的时间在终端查询连接状态,保存完证据后再按回车继续。
理解检查
- 程序连接
127.0.0.1时应优先选择哪类抓包接口? tcp port 50007与tcp.port == 50007分别属于哪类过滤器?- 同一抓包里多次运行程序,怎样锁定其中一条连接?
tcp.len == 0的报文是否仍可能推进连接状态?tcp.analysis.retransmission属于线上首部字段还是分析器推断?- Follow TCP Stream 中的颜色与应用消息边界分别由什么决定?
参考答案
- 应选择 Npcap 提供的 Windows 回环捕获接口 (Adapter for loopback traffic capture)。
- 前者(采用 BPF 语法)是捕获过滤器,后者(采用 Wireshark 专属语法)是显示过滤器。
- 先通过应用日志里的四元组(特别是客户端临时端口)找到报文,再利用该报文关联的
tcp.stream编号进行专门过滤。 - 完全可以。比如纯 SYN、纯 ACK、FIN 等控制报文,其负载数据长度常常为 0,但它们携带的标志位和序列号空间依然在有效推进 TCP 状态机流转。
- 它是 Wireshark 作为应用层工具,基于抓包上下文智能生成的分析推测标记,并非真实的 TCP 报文首部字段。
- Follow 界面中,颜色仅用来区分 TCP 报文的两端传输方向;而真正的应用层消息边界,是由本程序自行设计的 4 字节长度字段(应用协议)决定的。
本章小结
经过第一次抓包实战,我们可以将网络排查的核心方法浓缩为四个标准动作:选对抓包点,用端口号筛选候选流,用四元组和 tcp.stream 锁定唯一连接,最后将报文的时间线与两端日志对齐。
完成这套动作后,应用层的 Socket API 调用、操作系统内核的连接状态迁移,以及线上的真实 TCP 报文,就完美融合成了一张透明的运行剖面图。在第三篇中,我们将复用这份抓好的报文,并在此基础上,带你逐字逐句拆解每一个 TCP 报文首部字段的奥秘。
上一章:第5章 用一个最小程序建立 TCP 连接 · 所属篇:第二篇 · 下一章:第7章 网络包的分层结构 · 教程总览