Skip to content

第31章 系统化阅读一份 TCP 抓包

当你打开一份陌生的*网络抓包(PCAP)*时,Wireshark 往往会“热情地”向你展示一堆重传(Retransmission)、乱序(Out-of-Order)、窗口变化,外加交织在一起的多条 TCP 连接。面对这么多信息,最稳妥的排查策略是沿着固定步骤收集证据:先确认抓包的客观条件,再找出目标连接与生命周期,接着计算字节进度的序列号,最后与主机状态、应用日志交叉比对。

PCAP 是什么?

PCAP 既可泛指数据包捕获结果,也指经典的 .pcap 文件格式。文件通常保存每条记录的时间戳、原始长度、捕获长度和已保存字节;.pcapng 是支持多接口及更多元数据的新一代容器格式。

一份 PCAP 只覆盖指定时间、接口和抓包点看到的流量,缺失的方向、截断数据或捕获丢包都会限制结论。

为了方便说明,本章会用一个典型的贯穿案例:客户端 10.0.0.21:51514 连接服务端 10.0.0.8:9000,发出 64 字节的请求后,大约过了 2 秒才收到 32 字节的响应。

一、先给证据分个类

一份经得起推敲的排查报告,至少应该把排查过程中的信息明确分为五类:

类别示例记录方式
客观事实*帧 4(Frame Number 4)*的 tcp.len 为 64,过了 1.2 毫秒后,帧 5 返回了对 Ack 65 的确认引用具体的帧号、协议字段和时间
分析器标记Wireshark 将帧 18 标记为 TCP Retransmission标明使用了什么工具,以及具体的标记名称
逻辑推断请求数据已经在帧 5 之前到达了服务端的 TCP 协议栈列出支撑该推断的报文帧以及推理逻辑
待验证假设服务端的业务逻辑处理大概占用了 2 秒给出能够验证或推翻该假设的排查手段
最终结论结合服务端日志,确认业务处理阶段确实耗时了 1.98 秒将抓包证据与日志事件关联起来陈述
Frame Number 是什么?

Frame Number 是 Wireshark 按抓包文件记录顺序分配的帧号,便于在同一文件中引用具体记录。它不来自线上协议,也不会随报文在网络中传输。

不同抓包点或不同文件会各自编号,因此跨文件对齐应使用四元组、原始 Seq 范围、时间和载荷特征。

要记住,Wireshark 的专家信息(Expert Info)与分析器标记是非常有价值的破案线索,但它依赖于已捕获到的报文以及工具内置的算法。我们给出的最终结论,必须能够沿着证据链,一步步回溯到最原始的数据包字段上。

Expert Info 是什么?

Expert Info 是 Wireshark 汇总提示、警告和错误的分析视图,例如疑似重传、乱序、校验和异常或协议格式问题。它帮助快速定位可疑帧,结论来源仍是分析器基于当前捕获上下文的判断。

抓包缺失、Offload 和从会话中途开始捕获都可能制造误报,需回到原始字段和多点证据复核。

二、步骤零:弄清抓包时的上下文

在开始死磕单个报文之前,首先要建立一个背景档案——“这份抓包文件是怎么来的”:

  • 抓包的主机、网卡接口和网络位置:比如是在客户端以太网口、服务端回环网卡(Loopback),还是负载均衡、代理的前端抓的包?
  • 时间信息:捕获的起止时间、机器时区,以及主机的时钟是如何同步的。
  • 抓包参数:是否配置了捕获过滤器(Capture Filter)、显示过滤器(Display Filter)以及截断长度(snaplen)
  • 丢包统计:Npcap、dumpcap 或其他抓包工具报告的捕获包数与丢弃包数。
  • 底层环境:网卡硬件卸载(Offload)、虚拟交换机、容器网络或 VPN 的使用情况。
  • 业务关联:测试的具体操作、预期的网络四元组(源 IP、源端口、目的 IP、目的端口)、业务请求 ID 以及对应的应用日志文件。
snaplen 是什么?

snaplen(snapshot length,抓包快照长度)规定每个报文最多保存多少字节。它小于线上帧长时,记录仍可保留原始长度,但后半部分内容会被截断。

截断能降低磁盘和复制开销,也可能让 TCP 重组、TLS 解密和应用协议解析失去必要字节;-s 0 通常表示保存完整报文。

抓包的位置直接决定了你的“视野”。比如,在客户端抓包,你可以清楚地知道客户端何时把数据发出去、何时看到服务端的响应,但你通常无法单靠这份抓包,就准确推断出报文在远端网卡、对端 TCP 栈以及应用接收队列里到底堵了多久。如果在客户端和服务端同时抓包,并且时钟是同步的,就能进一步锁定延迟发生的具体网段或组件。

养成好习惯,留存原始的 pcapng 文件并计算它的哈希值,后续所有的过滤和注释操作都只在副本上进行:

powershell
Get-FileHash -Algorithm SHA256 .\case.pcapng
Get-Item .\case.pcapng | Select-Object FullName, Length, LastWriteTimeUtc

三、锁定连接、角色与生命周期

首先,通过 Wireshark 菜单 Statistics → Conversations → TCP 找出疑似目标连接的四元组,或者直接在搜索栏敲入显示过滤器:

text
ip.addr == 10.0.0.21 && ip.addr == 10.0.0.8 && tcp.port == 9000
tcp.flags.syn == 1 && tcp.flags.ack == 0
tcp.stream eq 7

在 TCP 握手阶段,第一个只带 SYN 标志位的报文,往往标志着主动发起连接的一方(通常是客户端)。服务端的端口一般是固定的,而客户端的临时端口每次连接都会变。结合应用日志和监听端口状态,可以把方向彻底敲定。需要注意的是,tcp.stream 只是 Wireshark 为当前抓包文件分配的内部序号,换个文件再打开,这编号可能就变了。所以在写分析报告时,一定要把真实的 IP 和端口四元组写上去。

tcp.stream 是什么?

tcp.stream 是 Wireshark 根据报文端点和时序为每条 TCP 会话分配的内部索引,可用 tcp.stream eq 7 快速筛出某一条流。它方便交互分析,但不属于 TCP 首部字段。

重新组合文件、改变解析条件或换一份抓包后索引可能变化,报告中还应写明真实四元组和时间范围。

如果是生产环境抓下来的大型包,里面往往混杂着 DNS 解析、ARP、ICMP 报错、健康检查、连接池复用以及并发的多个 TCP 数据流。面对这种大包,可以先用 Statistics → Protocol Hierarchy(协议分层统计)、Endpoints(端点统计)和 Conversations 摸个底,根据故障发生的时间窗口、数据传输持续时间、字节数或者服务端口,把排查范围圈小。

你可以把故障时间、用户 IP 和服务端口作为第一层筛子,再把请求 ID 和数据载荷里的特征字符串作为第二层筛子。建议保留两个视图:一个是包含了相关辅助流量的全局视图,另一个是只针对目标 TCP 流的过滤视图。全局视图能帮你看清 DNS 解析耗时、ICMP 报错或是代理握手的上下文;而单一的 TCP 流视图,则能让你集中精力死磕 Seq 和 Ack。

遇到现代浏览器、连接池复用或是应用的重试机制时,一次用户操作往往会触发好几条底层的 TCP 连接。针对每一条连接,都要单独记录它的四元组、tcp.stream、开始时间和终结方式。然后,通过业务请求 ID、TLS 会话特征、报文载荷摘要甚至应用层日志,把网络层的连接和业务层的操作对应起来。另外,客户端的同一个本地临时端口在不同时间可能会被复用,所以时间范围是区分“前世今生”连接实例的重要依据。

锁定目标流之后,接着就要判断这份抓包有没有覆盖 TCP 连接完整的生命周期:

  • 开头有没有捕捉到完整的 SYN 握手?
  • 握手完成后的 Seq 和 Ack 能不能严丝合缝地对上?
  • 结尾有没有正常的 FIN 挥手,或者 RST 强制重置?
  • 抓包是不是从会话中间才开始截取的,或者提前掐断了?抓包工具本身有没有提示丢包(Dropped packets)?
  • 捕获过滤器是不是一不小心把某些网卡接口、IP 地址或者某个方向的流量给漏掉了?

哪怕真的漏掉了开头的握手包,其实也不妨碍我们分析中间的数据传输阶段。这时候,只要在报告里老老实实地备注一句:“由于缺失握手,本连接的 MSS(最大报文段长度)、Window Scale(窗口缩放因子)和 SACK Permitted(允许选择性确认)等协商参数未知”就可以了。

四、解读握手与协商选项

在看三次握手(SYN、SYN+ACK、第三次 ACK)时,我们需要依次记录以下关键信息:

  1. 双方初始的序列号(ISN)和互相确认的关系;
  2. 双方各自声明的 MSS;
  3. 双方是否支持 Window Scale(窗口缩放)、SACK(选择性确认)以及 Timestamps(时间戳);
  4. SYN 报文是否发生过重传,以及握手各个阶段的耗时(网络往返延迟 RTT);
  5. 客户端发出的第三次 ACK 报文,是否已经顺带捎上了第一批应用层请求数据。

为了方便人眼阅读,Wireshark 会默认开启相对序列号(Relative Sequence Numbers),并保留可查看的原始序列号(Raw Sequence Number)。开启后,第一个 SYN 的序列号通常显示为 Seq=0,对方回应的 SYN+ACK 会显示 Ack=1。由于 SYN 标志位在 TCP 协议里需要消耗掉一个序列号,所以握手完成后,真正承载应用数据的第一个字节,其相对序列号是从 Seq=1 开始的。在分析接收窗口(Window Size)时,记得要看 Wireshark 换算后的“缩放后窗口大小(Calculated window size)”,并把三次握手期间协商的缩放因子作为重要依据记录下来。

相对序列号是什么?

相对序列号是 Wireshark 为便于阅读,把连接每个方向的初始序列号平移到零附近后显示的值。它让握手和载荷长度更容易手工计算,不会修改抓包中的原始字节。

原始序列号是什么?

原始序列号是 TCP 首部在线上实际携带的 32 位值。安全分析、规范核查和两端抓包精确关联时通常需要它;Wireshark 字段如 tcp.seq_rawtcp.ack_raw 可直接导出。

五、利用 Seq 和 Ack 画出字节时间线

排查 TCP 问题时,最硬核的基本功就是对数据的双向流动分别维护两笔账:“这一端到底发出去了多少数据”和“对方目前已经连续确认收到了哪里”。一个 TCP 报文段对下一个序号的期望值,可以套用这样一个公式:

NextSeq=Seq+TCP Len+ISYN+IFIN

公式里,只要报文携带了 SYN 或者 FIN 标志位,对应的指示量取 1。如果是纯粹的 ACK 确认包,它的数据载荷长度(TCP Len)通常是 0,因此它自己发出去的 Seq 是不会推进的。而报文里的 Ack 值,永远代表着接收方“下一步眼巴巴期待收到的那个字节序号”。

回到我们开头的贯穿案例,把它最关键的几个包捋出来,就能整理成下面这张时间线表格:

相对时间传输方向Seq / AckTCP Len动作解释
10.0000 s客户端 → 服务端0 / 0,SYN0客户端发起连接请求
20.0008 s服务端 → 客户端0 / 1,SYN+ACK0服务端确认请求并响应
30.0010 s客户端 → 服务端1 / 1,ACK0客户端确认,三次握手完成
40.0040 s客户端 → 服务端1 / 1,PSH+ACK64客户端发出业务请求(第 1 到 64 字节)
50.0052 s服务端 → 客户端1 / 65,ACK0服务端 TCP 栈累计确认收到请求数据
62.0090 s服务端 → 客户端1 / 65,PSH+ACK32服务端发出业务处理结果(响应数据)
72.0098 s客户端 → 服务端65 / 33,ACK0客户端累计确认收到响应数据

看帧 5,客户端把请求发出去之后仅仅过了 1.2 毫秒,就收到了服务端的 Ack=65。这个客观事实证明,这 64 字节的请求数据已经顺利钻进了服务端底层的 TCP 接收缓冲队列里。再看帧 6,客户端足足等了 2 秒钟才看到返回的响应。

但这 2 秒钟去哪了呢?这段时间可能消耗在服务端的应用程序处理、操作系统发送队列排队,也可能消耗在网络回程路径上。在这个节骨眼上,“服务端的业务处理耗时太长”成为了我们需要重点验证的假设。如果服务端日志里清晰地记录着:同一请求 ID 对应 request_received=00:00.004response_ready=00:02.006,而且两台机器的时钟是对齐的,那么这就坐实了——是业务逻辑自己处理慢了。如果这时候手里还有一份在服务端网卡上抓的包,我们甚至能进一步弄清楚,服务端程序把数据塞进 Socket 之后,网卡排队和网络回程又占用了多少毫秒。千万不要把“TCP 层面回了个 ACK”和“业务层面处理完毕”混为一谈,它们在时间轴上是两个完全独立的事件。

六、排查异常、窗口变化与连接关闭

当你把正常的字节时间线顺下来之后,再去看那些刺眼的异常线索,就不会轻易被分析器的满屏爆红带偏节奏了:

text
tcp.analysis.retransmission || tcp.analysis.fast_retransmission
tcp.analysis.out_of_order || tcp.analysis.lost_segment
tcp.analysis.duplicate_ack || tcp.option_kind == 5
tcp.window_size == 0 || tcp.analysis.zero_window_probe
tcp.flags.fin == 1 || tcp.flags.reset == 1

这些 tcp.analysis.* 派生字段由 Wireshark 根据当前流的已捕获历史计算出来。

tcp.analysis.* 字段是什么?

tcp.analysis.* 是 Wireshark 的 TCP 分析派生字段族,例如 retransmissionout_of_orderduplicate_acklost_segment。它们不在网络报文中占用比特,而是分析器比较 Seq、Ack、时间和窗口后生成的标签。

使用它们筛选线索很高效,最终结论仍应引用对应帧的原始字段并说明抓包完整性。

看到 Wireshark 标红的重传包(Retransmission),不要慌,把原始报文和重传报文的 Seq 范围、长度、发生间隔时间,以及对端此时反馈的 Ack 放在一起对比一下。遇到零窗口(Zero Window)问题,要重点关注接收方抛出的通告窗口大小(Calculated window size)、发送方当时正在飞行的在途字节数(Bytes in flight),以及随后抛出的零窗口探测包(Zero Window Probe)。

查看连接关闭过程时,要明确到底是哪一端先沉不住气发了 FIN,对端过了多久才给出确认,以及双方在断开前发出的最后一笔数据有没有被对端稳妥地确认接收。如果遇到粗暴断开的 RST(Reset),你必须结合它的 Seq、Ack,往回看它前面的报文动作,并且去翻翻应用层的错误日志,才能解释清楚是谁、因为什么原因掐断了连接。

七、将抓包视角与主机、应用无缝对齐

在 Windows 上排查,你可以用 PowerShell 轻松找到某个连接背后到底是谁在起作用:

powershell
$tcpConn = Get-NetTCPConnection -RemotePort 9000 | Select-Object -First 1
$tcpConn | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess
Get-Process -Id $tcpConn.OwningProcess

一份合格的应用日志,最好能把单调时钟(Monotonic clock)算出来的耗时、绝对时间(墙上时间)、网络连接 ID、业务请求 ID、Socket 读取完成时间、业务逻辑处理完成时间、数据写入调用(Write/Send)的返回时间,以及连接关闭事件通通记下来。不同服务器之间的绝对时间总是存在偏差的,但你可以利用 TCP 握手的时间点、日志里的请求 ID 顺序来做辅助校对。

日志里打出了一句“写入调用(Write/Send)成功返回”,在系统内核层面,这仅仅意味着这批字节成功被塞进了本机的 Socket 发送缓冲区。至于这批数据究竟什么时候真正被网卡发射到线上、什么时候被远端 TCP 接收并确认,这些都得靠抓包或是内核级遥测工具才能看到。同样,日志里的“读取调用(Read/Recv)返回”,也只代表应用程序终于从系统缓冲区里取走了数据。把这两层边界画进你的时间线里,你就能刀下见菜,准确地区分到底是应用在卡顿、内核在排队,还是网络在拥塞。

为了把证据固化下来,你可以用 TShark(tshark 命令行直接把关键字段倒出来,作为排查报告的附件:

TShark 是什么?

TShark 是 Wireshark 项目的命令行协议分析器,使用相同的协议解析器和显示过滤字段。它适合脚本化读取 pcap/pcapng、过滤报文并按字段导出 CSV 或文本证据。

版本和首选项仍会影响解析结果,自动化报告应记录所用 TShark 版本与完整命令。

powershell
tshark -r .\case.pcapng -Y "tcp.stream eq 7" -T fields `
  -e frame.number -e frame.time_relative -e ip.src -e tcp.srcport `
  -e ip.dst -e tcp.dstport -e tcp.seq -e tcp.ack -e tcp.len `
  -e tcp.window_size -e tcp.flags -e _ws.col.info

八、排查报告模板示例

text
问题描述:客户端发送请求后,感受到的延迟高达 2 秒。
排查范围:客户端网卡抓包;10.0.0.21:51514 ↔ 10.0.0.8:9000;时间窗口 12:00:00~12:00:03。
数据完整性:包内含有完整的 TCP 三次握手与四次挥手;抓包 snaplen=65535(全包抓取);抓包工具报告丢包数为 0。
客观事实:帧 4 客户端发送 64 字节;1.2 毫秒后,帧 5 服务端确认到 Ack=65;约 2.005 秒后,帧 6 服务端发回 32 字节响应。
分析器标记:当前 TCP 流中未出现重传、乱序或零窗口警告。
逻辑推断:请求数据很快便获得了服务端底层 TCP 的累计确认;这段长达 2 秒的延迟,必然落在服务端的业务处理、发送队列排队或网络回程路径上。
进一步验证:根据业务请求 ID,反查服务端的应用日志,确认具体是读取请求耗时,还是准备响应耗时。
最终结论:服务端业务处理阶段耗时达到了 1.98 秒;网络传输并未出现同等量级的延迟,瓶颈在服务端业务逻辑。
排查边界:由于缺少服务端侧的网卡抓包,网络回程的单向时延无法做到毫秒级精确拆分。

实验:手把手建立一份抓包证据链

  1. 本地启动一个小服务程序,逻辑是:收到完整请求后,硬核等待(Sleep)2 秒钟再返回响应,并在“读取完成”、“处理完成”这两个节点打印时间日志。
  2. 开启抓包,捕获从三次握手到连接关闭的全过程,记录下抓包网卡、TCP 四元组和业务请求 ID。
  3. 把 Wireshark 那些花里胡哨的着色规则(Coloring Rules)先关掉,只盯着帧号、相对时间、Seq、Ack 和 TCP Len,手动拉出一个表格。
  4. 试着在白纸上写出:至少一个客观事实、一个逻辑推断和一个待验证的假设。
  5. 最后,翻出服务端的那两行日志,看看你之前的假设是被证实了、被推翻了,还是需要继续找证据。

理解检查

  1. 当你看到服务端对一段请求数据回了 ACK 时,你到底能确认报文走到了计算机网络模型里的哪一层?
  2. 如果你的抓包是从会话中途开始截的(没抓到 SYN),有哪些重要的 TCP 选项参数就成了“未解之谜”?
  3. 排查问题时,为什么必须分别为发送方向和接收方向独立维护 Seq 与累计 Ack 的进度条?
  4. 当你在 Wireshark 里看到一条 TCP Retransmission 标记时,在把它写进最终报告之前,你还需要核对哪些具体的协议字段?
  5. 只有一份客户端视角的抓包,并且看到了 2 秒的响应延迟。你要怎样设计一个成本最低的验证实验,来证明到底是网络卡了,还是服务端程序卡了?

延伸阅读


上一章:第30章 TLS、HTTP 与 TCP 的关系 · 所属篇:第七篇 · 下一章:第32章 常见抓包假象 · 教程总览

Licensed under CC BY-NC-SA 4.0.