1. 抓包前的准备工作与思路
说实话,干网络这一行,早晚都得跟 WireShark 打交道。不管是排查应用响应慢、接口超时,还是定位数据包丢失、TCP 重传,最终都得落到抓包分析这一步。而在所有抓包场景里,TCP 三次握手和四次挥手又是最基础、也最值得亲手抓一次的东西——因为这两个过程把 TCP 状态机的核心逻辑全演出来了。我之前带过不少新人,让他们做的第一个实操练习就是抓一次完整的 HTTP 请求,然后从第一个 SYN 开始,一路看到最后一个 FIN,整个过程走一遍,很多之前看书看不懂的概念立刻就通了。
这次分享就以“使用 WireShark 分析 TCP/IP 三次握手和四次挥手”为主线,把我自己的抓包步骤、报文解读方法、以及踩过的几个坑都写出来。适用于刚入门 TCP/IP 协议的开发者、运维人员,也适合那些想彻底搞清楚 seq、ack、标志位到底怎么变化的朋友。我不打算罗列 WireShark 的所有菜单,只挑真正用得上的操作来聊。
先说说环境。抓包最怕的是网络环境嘈杂,办公室局域网里广播包、ARP、其他主机的流量一大堆,抓下来的包虽然没几个是你要分析的,反而干扰判断。所以我一直推荐一个思路:尽量在隔离环境里抓包。最省事的是抓本机回环(loopback)流量,也就是访问 127.0.0.1 上的服务,这样产生的 TCP 流量完全不受物理网卡和交换机的影响,干净得一塌糊涂。另一个选择是两台虚拟机之间通信,或者用一台测试服务器单独跑一个 Web 服务,然后从客户端抓包。
我自己最常用的组合是这样:本地起一个最简单的 HTTP 服务,然后用浏览器或者 curl 访问一次,同时在 WireShark 里选择回环接口抓包。这种方式的优点是流量纯净、可控,你能清晰地看到三次握手、HTTP 请求响应、四次挥手的过程,不会有杂七杂八的包干扰视线。如果你需要用远程服务器做测试,那就在抓包机上加上过滤条件,比如tcp.port == 8080,只看目标端口的包。
打开 WireShark 后,第一步是选择抓包接口。我通常会在主界面先看一眼各接口的流量趋势,选择一个网卡或者 Loopback 接口,然后点击开始捕获。注意,WireShark 在 Windows 上抓 Loopback 用的是 Npcap 的虚拟接口,名字一般是“Npcap Loopback Adapter”,在 Linux 上则是lo。抓包开始后,再去触发 HTTP 请求,然后回到 WireShark 点停止,这一步别搞反了,不然啥都抓不到。
等抓完包,要做的就是过滤。这里给新手一个建议:不要一上来就盯着全部报文看,先用过滤表达式把范围缩小。比如你要分析 TCP 握手,直接输入tcp就能只看 TCP 报文;如果你想精确到某个连接,可以通过tcp.stream == 0来过滤出第一个 TCP 流的全部报文,这是我最常用的操作。右键任意一个 TCP 包,选择“Follow” -> “TCP Stream”,WireShark 会自动帮你筛选出该连接的所有双向报文,而且会弹出一个窗口显示应用层的数据内容,特别适合配合 HTTP 请求一起分析。
2. 三次握手抓包过程与报文逐包拆解
2.1 如何抓到一次完整的三次握手
我拿一次最简单的 HTTP 访问举例。假设我在本机 127.0.0.1 的 8000 端口跑了一个 Web 服务,然后使用 curl 命令发请求:
curl http://127.0.0.1:8000/抓包完成后,在 WireShark 的过滤栏输入tcp.port == 8000,就能看到这一次连接产生的所有 TCP 报文。为了分析方便,最好再输入tcp.stream eq 0或者右键 Follow TCP Stream,确保只看这一条连接。
正常的三次握手会呈现出四个包:第一个是客户端发出的 SYN,第二个是服务端回复的 SYN+ACK,第三个是客户端发出的 ACK,紧接着往往还有一个 HTTP GET 请求包(这个属于三次握手完成后的数据包)。我见过不少人把 GET 请求当作第三次握手的一部分,其实不对——第三次握手的 ACK 包是纯 ACK,不携带应用层数据。但在实际抓包里,因为 TCP 的延迟确认(Delayed ACK)机制,第四个包可能会把 ACK 和 HTTP 请求一起发出去,这种现象在抓包中很常见,一会儿我会单独说。
2.2 第一个包:SYN 报文怎么看
点开第一个 SYN 包,在中间的面板区域展开 TCP 协议详情。你会看到几行关键信息,依次是 Source Port(源端口)、Destination Port(目的端口)、Sequence Number(序列号)、Acknowledgment Number(确认号)、Flags(标志位)。这里我直接说结论:
第一次握手,客户端发送一个 TCP 报文段,标志位只有 SYN 被置 1,seq 被设置为一个随机初始值。这个初始值在抓包里通常显示为一个很大的数字,比如 3867294321。很多人第一次看到会觉得奇怪,为什么不是从 0 开始?因为 TCP 为了防止旧连接的报文干扰新连接,使用随机初始序列号(ISN),这是 RFC 规范要求的。你不用记住具体数值,但要知道这是客户端选择的起始序列号,后续所有从客户端发往服务端的字节都会以这个数字为起点进行编号。
在 WireShark 的报文详情里,你还会看到一行 “Sequence Number: 0” 之类的内容。注意,WireShark 默认开启了相对序列号功能,也就是说它会把抓到的第一个包的 seq 显示为 0,方便阅读。如果你要看真实序列号,可以在 WireShark 的偏好设置里关闭“Enable sequence number analysis”或者启用“Relative sequence numbers”的开关。我个人建议新手先保持相对序列号,因为分析起来更直观。
顺便说一句,这个 SYN 包本身不携带任何应用层数据,它的负载几乎为零,TCP header 后面直接跟可选的 MSS、窗口扩展等 TCP Options。
2.3 第二个包:SYN+ACK 回应了什么
第二个报文是服务端返回的 SYN+ACK。在 Flags 区域,你会看到 SYN 和 ACK 两个标志位同时为 1。它的作用有两层:第一,确认收到了客户端的 SYN;第二,同步服务端自己的初始序列号。
所以这个包里的 Acknowledgment Number 是多少?它是客户端初始 seq 加 1。为什么加 1?因为 SYN 标志位本身要占用一个序列号。即使 SYN 包没有携带数据字节,它也被视为占用了一个序列号空间。同理,后续的 FIN 标志位也要占一个序列号。这是初学者最容易忽略的点。打个比方,就像你在排队时取了一个号,虽然你还没开始办业务,但这个号已经属于你了。TCP 里 SYN 和 FIN 都是“虚拟字节”,它们的 seq 会被消耗。
同时,服务端会在自己的 SYN 字段里设置另一个随机初始序列号,比如 1846912345。这个值会在 WireShark 里显示出来。你注意到没,这个包同时包含 seq 和 ack 两个序号,因为一个报文里可以既确认对方,又同步自己。扩展一下知识:SYN+ACK 包里的窗口大小、MSS 等参数也很关键,它告诉对方自己所能接收的报文段大小上限,这会影响后续传输的报文分片情况。
2.4 第三个包:ACK 确认以及可能合并的 HTTP 请求
第三个包是客户端发出的 ACK。它的标志位只有 ACK,seq 是客户端的初始序列号加 1,ack 是服务端的初始序列号加 1。这个包发出后,连接进入 ESTABLISHED 状态,双方开始正式传输数据。
但在实际抓包里,第三个包往往不是单独的 ACK,它经常会和 HTTP GET 请求合并成一个包。这是因为 TCP 协议栈做了优化:客户端等收到 SYN+ACK 后,如果紧接着有数据要发送,它会直接把 ACK 和数据一起发出去,而不是干等一个定时器。从 WireShark 上看,这个包在 TCP 层标志位是 ACK,但往下展开 HTTP 层,能看到GET / HTTP/1.1。我一般就跟新人说:第三个 ACK 和第一个应用数据包“搭车”了,你只要认准 seq 和 ack 都对得上就行,至于它是单独 ACK 还是携带数据,并不影响连接建立。
到这里,三次握手的三个包就解析完了。为了让你更清楚 seq 的递增逻辑,我把关键字段整理如下(以相对序列号为例):
| 方向 | 标志位 | seq | ack | 说明 |
|---|---|---|---|---|
| 客户端 → 服务端 | SYN | 0 | 0 | 初始序列号,占用一个序号 |
| 服务端 → 客户端 | SYN+ACK | 0 | 1 | 服务端初始序列号,确认客户端 SYN |
| 客户端 → 服务端 | ACK | 1 | 1 | 确认服务端 SYN,完成握手 |
你有没有发现,在这个表格里,SYN 包的 seq 显示为 0,但它在逻辑上占用了序号 0,所以第二个包的 ack 就变成了 1。第三个包发出去时,客户端的 seq 也从 0 变成了 1。这条规则贯穿整个 TCP 生命周期,后面分析四次挥手时同样适用。
2.5 三次握手背后的关键设计思想
聊完报文,我想多说几句设计层面的东西。为什么需要三次?为什么两次不行?因为 TCP 想要解决一个核心问题:让双方确认彼此的收发能力都正常。第一次握手,服务端知道客户端能发;第二次握手,客户端知道服务端能收能发;第三次握手,服务端知道客户端能收。这样双方才达成一致——“你能收,我也能收,可以开始传数据了”。
如果只用两次握手,服务端在收到 SYN 后直接认为连接建立,可万一客户端已经超时重传了一个 SYN,或者客户端并未真正准备好接收数据,服务端就会白白建立一堆半连接。三次握手相当于给服务端一个验证机会:如果客户端不回应最终的 ACK,服务端就不认为连接建立成功。这也是为什么那些 SYN Flood 攻击会让服务端满内存都是半连接的原因——攻击者只发 SYN,不回应 ACK,服务端只能一直等。
另外一个小细节:第三次握手是可以携带数据的。也就是说,客户端可以在发送 ACK 的同时携带 HTTP 请求,这种优化对性能有明显帮助,减少了交互轮次。抓包时看到的三次握手后立刻就是 GET 请求,就是这个原理。
3. 四次挥手的抓包流程与报文状态解读
3.1 挥手的发起方不一定总是客户端
三次握手一定是客户端先发起 SYN,但四次挥手不一定。主动关闭连接的一方可以是客户端,也可以是服务端。HTTP/1.1 默认开启 Keep-Alive,连接不会在每次请求后立即断开。当客户端决定不再复用连接时,通常由客户端主动发送 FIN;但如果服务端配置了超时关闭,也可能是服务端先发送 FIN。抓包分析时,不要理所当然地认为第一个 FIN 一定是客户端发出来的。
在我上面的 curl 场景里,由于 curl 默认行为是请求结束后主动关闭连接,所以你抓到的挥手动作发起方基本都是客户端。而在浏览器场景里,因为浏览器会复用连接,你可能会看到多个 HTTP 请求复用一个 TCP 流,直到空闲超时后某一方才发起关闭。为了抓到四次挥手,我建议先用 curl 做一次性的短请求,抓包结果干净又清晰。
3.2 第一次挥手:客户端发送 FIN
抓到一个 HTTP 请求完成后,在 WireShark 里继续看这条 TCP 流的后续报文。第一个挥手报文一般是客户端发出一个 FIN,标志位为 FIN,同时可能带着一个 ACK(如果它还有未确认的数据需要确认)。这个包的作用是告诉服务端:“我的数据已经发送完了,我这边不会再有新的数据发给你了,但我还能接收数据。”
注意 FIN 包本身的 seq 也需要占用一个序号。这个包里的 seq 通常是最后一个数据字节序号加 1。你可能会看到 FIN 和 ACK 同时置位,这并没什么好奇怪的,因为 TCP 标志位本来就是独立的,可以随意组合。主动关闭的一方发出 FIN 后,它的 TCP 状态从 ESTABLISHED 变成 FIN_WAIT_1。
3.3 第二次挥手:服务端回应 ACK
服务端收到 FIN 后,需要先确认这个 FIN。于是它发送一个 ACK 包,ack 值为客户端 FIN 的 seq 加 1。这个 ACK 的语义是:“我收到你的关闭请求了,但我这边可能还有数据要发,你先等一下。”
这个包就是第二次挥手。从状态机看,服务端此时从 ESTABLISHED 进入 CLOSE_WAIT 状态,而客户端从 FIN_WAIT_1 进入 FIN_WAIT_2 状态。在抓包里,这个 ACK 包和第一次 FIN 之间通常隔得极短。但如果服务端还有数据没发完,那么在 FIN_WAIT_2 之后,可能会看到服务端陆续发来一些数据包,这些都属于关闭连接前的最后数据传输。
3.4 第三次挥手:服务端发送 FIN
当服务端把所有剩余数据都发完,并且确认客户端已经接收完毕后,它才发送自己的 FIN 包。这个包可以单独是 FIN,也可以携带 ACK 标志。它的 seq 是服务端最后一个数据字节序号加 1,ack 是客户端 FIN 的 seq 加 1。这个包发出后,服务端进入 LAST_ACK 状态。
这里我想特别强调一个常见误区:很多人以为挥手必须是“FIN、ACK、FIN、ACK”这样严格两两对应。其实第三次挥手不一定要等第二次挥手之后很久,如果服务端在收到 FIN 时已经没有数据要发了,它完全可以立刻回复一个 FIN+ACK 包,把第二次和第三次挥手合并成一个报文。所以抓包里你有时会看到三次挥手(FIN、FIN+ACK、ACK),而不是四次。TCP 规范并不强制要求 ACK 和 FIN 分开发送,只是很多实现为了性能会把它们合并。如果你看到只有三个挥手包,不要怀疑抓包出错,这属于正常的优化行为。
3.5 第四次挥手:客户端发送最后的 ACK
服务端发出 FIN 后,客户端需要回应一个 ACK,确认收到服务端的 FIN。这个包的 ack 是服务端 FIN 的 seq 加 1。发完这个 ACK 后,客户端并不立即关闭,而是进入 TIME_WAIT 状态,等待 2MSL(Maximum Segment Lifetime,最大报文生存时间)后才最终关闭连接。TIME_WAIT 是很多人容易忽略但非常关键的机制。
为什么需要 TIME_WAIT?两个原因:第一,保证最后的 ACK 如果丢失,服务端能够重发 FIN,客户端还有机会再补发 ACK,否则服务端会一直收不到确认而导致关闭异常;第二,让旧连接的延迟报文在网络中自然消失,避免它们干扰新连接。从这个角度看,四次挥手其实也是一次“可靠关闭”的协商过程。
我还是用一个表格来展示挥手过程的序号变化:
| 方向 | 标志位 | seq | ack | 状态变化 |
|---|---|---|---|---|
| 客户端 → 服务端 | FIN | 101 | 201 | 客户端进入 FIN_WAIT_1 |
| 服务端 → 客户端 | ACK | 201 | 102 | 服务端进入 CLOSE_WAIT,客户端进入 FIN_WAIT_2 |
| 服务端 → 客户端 | FIN | 201 | 102 | 服务端进入 LAST_ACK |
| 客户端 → 服务端 | ACK | 102 | 202 | 客户端进入 TIME_WAIT,服务端进入 CLOSED |
这里的 seq 和 ack 只是示意值,但你发现规律没有:每次 FIN 的确认号都是对端 seq 加 1。只要把这个规律记牢,哪怕不看标志位,单看 seq 增长都能猜出来当前是哪一步。
3.6 抓包验证 TIME_WAIT
如果你想亲眼看到 TIME_WAIT 状态,可以在抓包结束后,用命令行查看连接状态。Linux 下执行ss -tan或netstat -tan,如果过滤127.0.0.1:8000,能看到类似TIME_WAIT的条目。Windows 下用netstat -ano | findstr 8000也能看到。我建议你在抓包结束后尽快执行这个命令,因为 TIME_WAIT 默认持续 60 秒左右,等太久就看不到了。
我印象很深的一次是帮某开发者排查一个高并发短连接服务,发现服务器上出现了海量 TIME_WAIT 连接,原因是服务端主动关闭了连接,而客户端也走了完整的四次挥手。大量短连接导致 TIME_WAIT 堆积,占满了端口资源,进而引发新连接无法建立。后来通过调整 keep-alive 和复用连接解决了问题。这就是抓包分析之外延伸出来的实战经验,也是理解四次挥手价值的地方。
4. 常见问题与排查技巧实录
4.1 抓包只看到 SYN,没有 SYN+ACK
这种情况通常意味着客户端发起的连接根本没到达服务端,或者服务端没回应。先看 IP 和端口对不对,再看服务端口是否处于监听状态。如果被测服务没有启动,客户端会收到一个 RST 包(连接被拒绝),而不是 SYN+ACK。如果连 RST 都看不到,那可能是防火墙把包丢了。
我排查过不少案例,最后发现是云服务厂商的安全组规则没有放行对应端口,服务器本机 netstat 显示 LISTEN,但外部死活连不上。抓包就能看出来:SYN 发出去了,但服务端没回复,也没有 RST。这种时候去检查防火墙策略比盯着 WireShark 有用得多。
4.2 大量 TCP Retransmission 出现
如果抓包窗口里出现很多黑底红字的 Retransmission,或者 Fast Retransmission,说明网络丢包比较严重,或者接收方窗口满了。你可以配合 TCP 层的[Analysis Flags]查看丢包原因。也可能是发送方设置的 TCP 窗口太小,导致吞吐量上不去。
如果是在本地回环抓包,基本不会出现重传;如果是在跨网段测试中出现,先检查是否开启了网卡 offload 功能。我在某些 Linux 虚拟机上遇到过,抓包工具显示很多异常校验和错误,但实际业务却正常,后来发现是网卡 TCP 校验和卸载(Checksum Offloading)导致的误报。解决办法是在抓包时选择关闭该网卡的校验和卸载,或者直接忽略校验和错误。
4.3 抓不到四次挥手包
很多人用浏览器访问一个网址后,发现只抓到三次握手和 HTTP 请求响应,之后就一直没有 FIN 包。这很正常,因为浏览器为了复用连接,不会在请求结束后立刻关闭。你可以强制用Connection: close请求头,或者在服务端设置不使用 keep-alive。用 curl 加一个参数也行:
curl -H "Connection: close" http://127.0.0.1:8000/另外,如果你是用 WireShark 的tcp.stream过滤器逐条看,但在多条连接复用时,抓包会显得特别乱。这种情况下建议先抓完包再过滤,这样能看到整个会话的完整脉络。
4.4 如何根据状态排查连接卡住
对付连接卡住还有一个技巧:在 WireShark 里配合 TCP 状态分析,选中某个包后,右下角的 Expert Info 会提示异常,比如 Window Full、ACKed lost segment、Out-of-order 等。这些提示在快速定位问题时有奇效。注意不要被红字吓到,TCP 存在乱序和重传是网络正常现象,需要结合出现频率和业务表现综合判断。
如果你需要查看某个 TCP 连接当前处于什么状态,可以在 Linux 上用ss -tanp | grep 8000看状态,但抓包更直观。把三次握手和四次挥手的抓包过程走一遍,其实你已经把 TCP 状态机的绝大部分路径都验证了一遍。
4.5 时间戳与选项字段不要忽略
在报文详情的 TCP Options 里,你会看到TCP Timestamps。这个选项用于计算往返时间 RTT 和防止序列号回绕。如果多台设备时间不同步,那么在分析 RTT 和重传时可能会产生误导。我一般会先确认抓包时间和系统时间基本一致。另外,在分析高延迟链路时,一定要看 Timestamp 值而不要光看包到达时间的间隔,因为网络抖动会让包的到达时间不规律。
我还遇到过一个情况:某服务和客户端之间经过了一个代理,抓包时看到 SYN 的 MSS 是 1460,但服务端回复的 MSS 只有 1400,说明链路上某个设备限制了报文长度。这种细节在抓包中一眼就能看到,而在业务层面根本察觉不到。所以每次抓包都养成看 TCP Options 的习惯,会有意想不到的收获。
5. 实操扩展:完整抓取一次 HTTP 请求的连接生命周期
把基础握手和挥手都弄明白后,不如再做一次综合练习:抓取一次完整的 HTTP 请求,把所有 TCP 段按顺序列出来,观察三次握手、HTTP 请求、HTTP 响应、四次挥手这些事件在整个连接中是怎么穿插的。
我建议的步骤如下:
- 启动 WireShark,选择回环接口,开始抓包。
- 用 curl 访问本机服务,加参数
-v可以看到 TCP 连接过程的时间点:curl -v -H "Connection: close" http://127.0.0.1:8000/ -o /dev/null - 停止抓包,过滤
tcp.port == 8000。 - 把时间排序,从第一条 SYN 开始数,记录每个报文的相对时间、seq、ack 和标志位。
你最终看到的报文顺序应该类似:
- 第 1 包:客户端 SYN(seq=0)
- 第 2 包:服务端 SYN+ACK(seq=0,ack=1)
- 第 3 包:客户端 ACK(seq=1,ack=1)
- 第 4 包:客户端 HTTP GET(通常和 ACK 合并)
- 第 5 包:服务端 HTTP 200 OK 响应,分多个数据包发送
- 第 6 包:客户端 ACK 确认服务端数据
- 第 7 包:客户端 FIN(seq=某值)
- 第 8 包:服务端 ACK(ack=客户端 seq+1)
- 第 9 包:服务端 FIN(seq=某值)
- 第 10 包:客户端 ACK(ack=服务端 seq+1)
如果你用-H "Connection: close",服务端发完响应后会在响应头里带上Connection: close,然后主动或被动触发关闭。每个包的相对时间也可以观察,正常情况下握手三个包的时间间隔都在毫秒级甚至微秒级,如果超过 200 毫秒,就要怀疑中间设备有延迟。
这个练习做完后,你还可以尝试用浏览器访问一个大型网页,观察 TCP 连接如何复用、多个 HTTP 请求如何在同一个连接上排队传递、服务端什么时候发起 keep-alive 超时关闭。这些都是面试里常问的“TCP 长连接”话题,抓几次包之后,你就能用自己的话讲清楚。
我还想补充一个技巧:WireShark 的 “Statistics” -> “Flow Graph” 功能可以生成时间序列图,展示每个包的方向和标志位,非常直观。不过我的建议是先用基础列表自己手动分析一遍,再用图表验证,这样印象才深。别一上来就依赖可视化工具,否则你会发现换个环境、换个抓包工具就什么都看不懂了。
6. 一些个人体会与建议
抓包这事,看着简单,但真正要熟练,得多动手。我建议每个做网络相关工作的同学都亲自抓一次三次握手和四次挥手,并且尝试在抓包文件里逐字段解释每个包的 seq、ack、flags、窗口大小、MSS、时间戳。这个过程可能只需要半小时,但它带给你的收益远比看十篇理论文章大。
我第一次抓包时也闹过笑话:以为三次握手后的 HTTP GET 是第三次握手的一部分,后来对照文档才发现那是第四个包。也曾经因为只看 IP 包而忽略 TCP 连接的概念,导致分析半天不知道这个流是哪个进程发起的。后来我总结出一条经验:抓包分析一定要有“流”的概念,WireShark 里的 stream 就是一条完整的 TCP 双向会话。看问题不要只看单个包,要把它放在整个流转过程中去理解。
如果非要说一个最值得记住的抓包技巧,那就是:先过滤,后分析。过滤字段用好了,抓包效率能提升一大截。我最常用的是tcp.stream过滤器,它能让你瞬间从嘈杂的全局流量里抽出一条完整连接。还有http、dns、tcp.flags这些常见过滤器,建议都练一练。
另外提醒一句,WireShark 版本之间菜单位置会有差异,但核心功能差异不大。如果你用的版本和我描述的路径不一样,直接在过滤栏输入表达式即可,不一定非得通过菜单操作。抓包文件不要急着删,留着以后遇到类似问题可以对比着看。我自己就留着好几个典型的抓包文件:正常握手、带重传的握手、RST 拒绝连接、半关闭、TIME_WAIT 堆积,都是不同场景下的宝贵样本。
最后我想说的是,TCP/IP 协议栈本身很复杂,但三次握手和四次挥手是理解它的最好起点。把这十个包看懂,你对 TCP 可靠传输、连接状态、超时控制的理解就建立起来了。后面再去研究拥塞控制、窗口滑动的原理,会轻松得多。希望这篇基于我实际抓包经验的分享,能帮你少走一些弯路。