☰
TCP四次挥手全解析:Wireshark抓包逐包拆解与异常排查
2026/10/26 12:39:21 网站建设 项目流程

1. 为什么“四次挥手”值得单独拎出来讲

搞网络的人都有一个共同的体会:三次握手背得滚瓜烂熟,四次挥手却总是含糊。面试被问到“为什么关闭连接要四次而建立只要三次”,很多人能背出“因为TCP是全双工的”,但再往下追问一句“TIME_WAIT到底等多久、为什么是2MSL”,就开始支支吾吾了。我自己刚接触HCIA那会儿也是这样,书上的状态迁移图看了一遍又一遍,FIN、ACK、FIN、ACK四个包画得清清楚楚,可一到Wireshark里抓包,看到满屏的黑色、红色条目就发懵,根本对不上号。

这篇内容就是把我自己从“背概念”到“真看懂”这个过程里踩过的坑、总结的方法完整摊开。核心围绕TCP四次挥手这个连接拆除过程,配合Wireshark抓包做逐包拆解,把状态机、报文细节、异常现象一次性讲透。它解决的是“知道结论但不会验证”的问题——你不仅要知道挥手有四个包,还要能在抓包文件里一眼定位每个包、读懂每个字段、判断这次关闭是否正常。适合正在备考HCIA/CCNA这类入门认证的朋友,也适合日常做运维、开发、测试,需要排查连接异常的同学。哪怕你之前完全没碰过抓包工具,跟着走一遍也能上手。

我始终认为,四次挥手是理解TCP可靠性设计的一把钥匙。它不像握手那样只是“建立”,挥手涉及资源释放、延迟确认、半关闭、异常终止等一堆边角情况,把这些搞明白了,你对TCP的理解会上一个台阶。下面就从设计思路开始,一层层往下拆。

2. 四次挥手的整体设计与思路拆解

2.1 全双工连接为什么必须拆两次

要理解四次挥手,先得接受一个前提:TCP连接是全双工的,意思是A到B和B到A这两个方向的数据通道是相互独立的。你可以把它想象成两个人打电话,你说的话走一条线,对方说的话走另一条线,两条线互不干扰。当一方说“我说完了”(发送FIN),只代表他这个方向不再发数据了,但对方可能还有话没说完,所以对方要先回一个“知道了”(ACK),等自己那边也说完了,再单独发一个“我也说完了”(FIN),最后这边再回一个“知道了”(ACK)。这就是四个包的根本来源。

对比三次握手就很好理解了。握手时双方都没有任何数据要传,SYN和ACK可以合并成一个包发出去,所以三次就够。挥手时因为两个方向的数据流是分别关闭的,FIN和ACK在大多数情况下没法合并(除非对方恰好也没有数据要发,且开启了延迟确认的某些优化),于是就成了四次。这里有个容易被忽略的点:四次挥手不是协议强制规定的固定次数,而是“两个方向各关一次、各确认一次”这个逻辑下的自然结果。实际抓包里偶尔能看到三次甚至五次的变体,后面讲异常的时候会细说。

2.2 半关闭状态:容易被忽视的中间态

四次挥手过程中有一个特殊状态叫半关闭(Half-Close)。当A发出FIN并收到对方的ACK之后,A到B这个方向就关闭了,A不能再发数据,但B到A方向还开着,B仍然可以继续给A发数据。这个设计非常有用——比如一个客户端把请求发完了,发个FIN告诉服务器“我的请求发完了”,但服务器还要把大量响应数据传回来,这时候连接就处于半关闭状态,客户端能收但不能发。

很多初学者以为发了FIN连接就断了,其实不是。真正断开要等双方都完成关闭。这个认知差异在排查问题时特别关键:如果你在抓包里看到一方发了FIN,但之后还有数据从另一方传过来,不要惊讶,这是完全合法的半关闭行为。Wireshark里判断半关闭,看的就是FIN之后、对方FIN之前这段时间里,是否还有单向的数据传输。

2.3 状态迁移的完整链路

四次挥手涉及的状态比握手多,我把主动关闭方和被动关闭方分开列,这样对照抓包时不容易乱。

主动关闭方(假设是客户端)的状态路径是:ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED。被动关闭方(服务器)的路径是:ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED。

这里有两个状态是重点。第一个是FIN_WAIT_2,主动方发了FIN、收到ACK后就进入这个状态,此时它在等对方的FIN。如果对方一直不发FIN,主动方就会卡在FIN_WAIT_2,这个状态本身没有超时机制(要靠应用层或系统参数干预),是很多“连接泄漏”问题的根源。第二个是TIME_WAIT,主动方收到对方FIN并回完最后一个ACK后进入,要等2MSL才真正关闭。这两个状态在抓包分析里出现频率极高,后面会专门展开。

2.4 为什么主动关闭方要进TIME_WAIT

这是面试高频题,也是实操中最容易困惑的点。TIME_WAIT存在的理由有两个,缺一不可。

第一是保证最后一个ACK能到达对方。假设主动方回完最后一个ACK就直接关闭,而这个ACK在网络中丢了,被动方会以为自己的FIN没被确认,于是重传FIN。可这时候主动方已经关了,收到重传的FIN只会回一个RST,被动方就会以异常方式关闭,而不是正常的CLOSED。有了TIME_WAIT,主动方在2MSL内还能收到重传的FIN并再次回ACK,保证对方正常关闭。

第二是让本次连接的旧报文在网络中自然消亡。一个TCP报文在网络中的最大生存时间叫MSL(Maximum Segment Lifetime),2MSL就是往返一趟的最大时间。等够2MSL,本次连接的所有报文都从网络中消失了,新连接就不会收到旧连接的残留报文,避免数据错乱。这个设计思路很朴素但很严谨,是TCP可靠性哲学的典型体现。

注意:不同系统的2MSL取值不同,常见的是60秒(即MSL=30秒),也有系统用更短的值。抓包里看到TIME_WAIT持续一分钟左右是正常的,不要误判为异常。

3. 核心报文细节与Wireshark实操要点

3.1 四个包的标志位与序号变化

四次挥手的四个包,标志位组合和序号变化是有规律的,记住这个规律,抓包时一眼就能认出来。假设客户端主动关闭,初始序号相关字段如下:

包序方向标志位关键字段含义
第1包客户端→服务器FIN, ACKSeq=x, Ack=y客户端说“我没数据了”
第2包服务器→客户端ACKSeq=y, Ack=x+1服务器说“知道了”
第3包服务器→客户端FIN, ACKSeq=z, Ack=x+1服务器说“我也没数据了”
第4包客户端→服务器ACKSeq=x+1, Ack=z+1客户端说“知道了”

这里有几个细节值得抠。第一,FIN包本身会消耗一个序号,所以第2包的Ack是x+1,而不是x。这一点和SYN一样,SYN也消耗一个序号。第二,第3包的Seq是z而不是y+1,因为服务器在第2包之后可能还发过数据,z是它实际发到的位置。第三,第4包的Ack是z+1,同样因为FIN消耗了一个序号。这些序号关系是判断挥手是否正常的第一道关卡,如果Ack对不上,说明中间有丢包或乱序。

3.2 Wireshark过滤器的精准用法

抓包文件动辄几千个包,不会过滤等于大海捞针。针对四次挥手,我常用的过滤器有这么几个,按使用场景分。

最直接的是按TCP标志位过滤:tcp.flags.fin == 1,这条能筛出所有带FIN的包,四次挥手里的第1包和第3包都会出来。如果想看某个具体连接的完整挥手过程,用tcp.stream eq N(N是流编号),先把整条流的所有包拉出来,再顺着看。更精准一点,可以组合过滤:tcp.flags.fin == 1 or tcp.flags.reset == 1,这样连异常终止的RST包也能一起看到,排查问题时很有用。

还有一个我特别推荐的技巧:用tcp.port == 端口号 && tcp.flags.fin == 1锁定特定服务的挥手。比如排查某个Web服务的连接关闭,把端口限定住,避免被其他连接的FIN干扰。另外,Wireshark的“Follow TCP Stream”功能配合过滤器使用效果很好——先过滤出目标流,右键Follow,能看到应用层数据的完整交互,再回到包列表对照挥手位置,上下文一目了然。

提示:过滤器里的==和eq等价,但写tcp.flags.fin == 1比写tcp.flags.fin更明确,后者在某些版本里行为不一致,建议养成写全的习惯。

3.3 逐包拆解:从字段到状态

光看标志位还不够,真正要吃透得把每个包的字段展开看。以第1包(客户端FIN)为例,展开TCP层,重点看这么几个字段:Sequence Number是当前发送位置,Acknowledgment Number是对对方数据的确认,Flags里FIN和ACK都置1,Window Size反映接收窗口。这个包的Window Size往往能反映客户端当时的接收缓冲情况,如果窗口很小,可能说明客户端处理不过来。

第2包(服务器ACK)相对简单,Flags只有ACK,Ack等于第1包的Seq+1。但这里要注意一个现象:如果服务器在第2包之后还有数据要发,你会看到ACK包之后跟着一串数据包,然后才是第3包的FIN。这段“ACK到FIN之间”的数据传输,就是半关闭状态的直观体现,抓包里非常值得观察。

第3包(服务器FIN)和第1包结构类似,Flags是FIN+ACK,Seq是服务器当前发送位置,Ack仍然是x+1(因为客户端在FIN之后没再发数据)。第4包(客户端ACK)是纯ACK,Ack等于第3包Seq+1。看完这四个包,再回到Wireshark的“TCP stream graph”里看时序图,四个包的先后关系和方向一目了然,比纯看列表直观得多。

3.4 用“专家信息”快速定位异常

Wireshark有个“Expert Information”面板(分析菜单里),会把抓包文件里的异常自动归类,比如重传、乱序、零窗口、RST等。排查挥手问题时,先扫一眼这个面板,能快速发现线索。比如看到大量“TCP Retransmission”,说明有丢包,挥手可能不完整;看到“TCP RST”,说明连接被异常终止,不是正常四次挥手。

我个人的习惯是:打开抓包文件先看Expert Information,再看过滤器筛出的FIN包,最后逐包展开。这个顺序能保证既不漏掉异常,又能聚焦到挥手本身。Expert Information里的条目按严重程度分级,Error和Warning优先看,Note级别的可以稍后。这个面板是Wireshark里被低估的功能,很多人只盯着包列表,其实专家信息能省大量时间。

4. 完整抓包实操与关键环节实现

4.1 抓包环境搭建与准备

要复现一次干净的四次挥手,环境不用复杂,一台机器上跑客户端和服务器就行,但有几个准备动作不能省。首先是选抓包点:如果客户端和服务器在同一台机器,抓loopback接口(Windows上是\Device\NPF_Loopback或Npcap Loopback Adapter,Linux上是lo);如果分处两台机器,抓靠近主动关闭方的那张网卡,这样四个包都能抓到,不会因为抓包点位置导致某个方向的包缺失。

其次是选工具。Wireshark是首选,图形化、字段解析全、过滤器强大。命令行场景可以用tshark,适合服务器上没有图形界面的情况。抓包权限方面,Linux下需要root或给dumpcap加capability,Windows下装Npcap时要勾选支持抓包的模式。这些准备工作看着琐碎,但抓包抓不全、权限不够是最常见的翻车点,提前搞定能省很多事。

最后是构造一个可控的关闭场景。最简单的办法是用一个短连接请求,比如用curl访问一个HTTP服务,请求完成后连接自然关闭,就能抓到标准的四次挥手。如果想控制哪一方主动关闭,可以用nc(netcat)手动建连再手动断开,或者写个几行的Python脚本,用socket的close()触发。脚本方式最灵活,能精确控制关闭时机,适合反复练习。

4.2 一次标准挥手的抓包实录

我用一个Python小脚本做演示,客户端连上服务器、发一条消息、然后主动关闭。服务器端用nc监听。整个过程在loopback上抓包,抓到的四个包非常干净。

import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 9999)) s.sendall(b'hello') s.close() # 触发四次挥手,客户端主动关闭

抓包结果按时间顺序排列,四个包的标志位和序号完全符合前面讲的规律。第1包客户端发FIN+ACK,第2包服务器回ACK,第3包服务器发FIN+ACK,第4包客户端回ACK。在Wireshark里选中第1包,展开TCP层,能看到Flags字段里FIN和ACK都打了勾,Seq和Ack的数值和预期一致。

这里有个实操细节:第2包和第3包之间往往有时间间隔。因为服务器收到FIN后,应用层可能还要处理一下才关闭自己的发送方向,这个间隔在抓包里表现为两个包的时间戳差了几毫秒到几十毫秒。如果间隔特别长(比如几秒),说明服务器应用层关闭逻辑有问题,值得排查。这个观察点是纯看理论看不出来的,只有实际抓包才会注意到。

4.3 用IO Graph观察挥手节奏

Wireshark的IO Graph(统计菜单里)能把抓包文件里的包按时间画成曲线,对观察挥手节奏很有帮助。设置过滤条件为tcp.flags.fin == 1,看FIN包在时间轴上的分布,能直观看出连接关闭是集中发生还是分散发生。如果某个时间段FIN包密集,说明有大量连接在关闭,结合业务日志能定位到具体操作。

更进一步,可以用tcp.analysis.flags相关的过滤条件观察重传和乱序,配合IO Graph看异常包的时间分布。这个技巧在排查“为什么挥手很慢”这类问题时特别有效——如果FIN之后紧跟着重传,说明ACK丢了,挥手被拖长了。IO Graph不是必需工具,但它能把“时间”这个维度可视化,比盯着时间戳列强得多。

4.4 对照状态机验证抓包结果

抓完包、看完字段,最后一步是拿状态机做交叉验证。在Wireshark里,选中某个包后,可以在TCP层看到Wireshark自己推断的连接状态(部分版本支持),或者手动对照:第1包发出后主动方进FIN_WAIT_1,第2包到达后进FIN_WAIT_2,第3包到达后进TIME_WAIT,第4包发出后仍在TIME_WAIT等待2MSL。被动方则是第1包到达进CLOSE_WAIT,第3包发出进LAST_ACK,第4包到达进CLOSED。

这个对照过程能帮你发现很多隐藏问题。比如如果抓包里第2包之后迟迟没有第3包,被动方就一直卡在CLOSE_WAIT,这就是典型的“CLOSE_WAIT堆积”,通常是应用层忘了调用close()。再比如主动方发完第4包后,如果很快又收到一个FIN(对方重传),说明第4包可能丢了,这时候TIME_WAIT的价值就体现出来了。把抓包和状态机对上号,才算真正看懂了挥手。

5. 常见异常与排查技巧实录

5.1 挥手只有三个包是怎么回事

实际抓包里经常看到“三次挥手”,别慌,这通常是正常的优化。当被动关闭方收到FIN时,如果它恰好也没有数据要发,且开启了延迟确认,它可以把ACK和随后的FIN合并成一个包发出去,于是第2包和第3包合二为一,四次变三次。判断依据是:合并后的那个包Flags同时有ACK和FIN,Ack等于对方FIN的Seq+1。这种合并是协议允许的,不是异常。

真正需要警惕的是另一种“三包”情况:主动方发了FIN,对方回了ACK,然后就没有然后了,既没有FIN也没有数据。这说明被动方应用层没有正确关闭连接,连接卡在CLOSE_WAIT。这种时候抓包里看不到第3包,但用tcp.flags.fin == 1过滤只能看到一个FIN,结合netstat看CLOSE_WAIT数量就能确认。两种情况表面都是三个包,性质完全不同,一定要区分开。

5.2 TIME_WAIT过多到底要不要处理

这是运维里被问得最多的问题之一。先说结论:TIME_WAIT是正常状态,数量多本身不是故障。它是主动关闭方的必经状态,高并发短连接场景下TIME_WAIT堆积是必然的。真正要关注的是它有没有影响到新连接的建立——如果本地端口耗尽导致无法建连,那才需要处理。

处理手段上,常见的有调整系统参数缩短TIME_WAIT时间、开启端口复用等,但这些都有副作用,不能无脑上。缩短2MSL会削弱TIME_WAIT的两个保护作用,端口复用可能带来连接混淆风险。我的经验是:先确认是不是真的端口耗尽(看netstat的统计和错误日志),如果是,优先从架构上减少短连接(用连接池、长连接),而不是直接改内核参数。参数调整是最后手段,且要经过测试。

注意:抓包里看到TIME_WAIT持续约60秒是正常的,不要因为“等太久”就去改参数。先搞清楚业务是否真的受影响。

5.3 RST代替FIN的异常终止

有时候抓包里看不到四次挥手,取而代之的是一个带RST标志的包,连接直接断了。RST是“重置”,表示异常终止,不走正常挥手流程。常见触发场景有:向一个已关闭的端口发数据、应用层调用带SO_LINGER且超时为0的close、接收缓冲区有未读数据时关闭连接、防火墙拦截等。

排查RST要看它出现在哪个方向、什么时机。如果RST紧跟在某个数据包之后,可能是对端认为这个数据非法;如果RST出现在连接空闲很久之后,可能是中间设备清理了会话表。Wireshark的Expert Information会把RST标出来,结合前后包看上下文,基本能定位原因。RST不是“错误”,它是TCP的一种合法机制,但频繁出现RST说明连接管理有问题,值得深挖。

5.4 常见问题速查表

把上面这些整理成一张表,排查时对着看能省不少时间。

现象可能原因排查方向
只有FIN没有后续被动方应用层未关闭查CLOSE_WAIT数量、应用日志
四次变三次ACK与FIN合并看合并包Flags是否含FIN+ACK
大量TIME_WAIT短连接高并发评估端口耗尽风险,考虑连接池
出现RST异常终止看RST前后包、SO_LINGER设置、防火墙
FIN后紧跟重传ACK丢失看重传包、网络质量
卡在FIN_WAIT_2对方不发FIN查对端应用是否挂起

这张表是我自己排查时反复用的,覆盖了入门阶段八成以上的挥手异常。真正遇到复杂情况,还是要回到抓包逐包看,表格只是快速定位的起点。

5.5 几个只有实操才会知道的坑

第一个坑是抓包点选错导致只看到半个挥手。如果抓包点在中间设备上,可能只抓到单向的包,看起来像挥手不完整,其实是抓包位置问题。解决办法是尽量在端点抓,或者两端同时抓对比。

第二个坑是虚拟化环境的时钟问题。在虚拟机或容器里抓包,时间戳可能因为时钟同步出现跳变,导致包的先后顺序看起来混乱。遇到这种情况先确认时钟源,别急着下结论说协议异常。

第三个坑是Wireshark的“相对序号”默认开启。默认情况下Wireshark显示的是相对序号(从0开始),和理论上的绝对序号对不上,初学者容易懵。可以在TCP协议首选项里关掉相对序号,看绝对序号,或者干脆习惯相对序号的视角——反正Ack和Seq的加减关系是不变的。

第四个坑是延迟确认干扰判断。接收方可能等一小会儿再发ACK,导致抓包里FIN和ACK之间有明显间隔,看起来像卡住了。这通常是正常的,除非间隔超过几百毫秒才需要怀疑。理解延迟确认机制,能避免大量误判。

6. 从抓包到理解:我的几点体会

把四次挥手和Wireshark抓包结合起来练,最大的收获不是记住了四个包,而是建立了一种“用抓包验证理论”的习惯。书本上的状态机是静态的,抓包里的状态是动态的,两者对上号的那一刻,理解才真正落地。我建议每个学网络的人,都亲手抓一次完整的挥手,逐包展开字段看一遍,再对照状态机走一遍,这个过程的收获远超背十遍书。

另外,别把TIME_WAIT、CLOSE_WAIT这些状态当成“要消灭的敌人”。它们是TCP设计的一部分,存在即合理。排查问题的正确姿势是先判断“这个状态是否影响了业务”,而不是看到数量多就动手改参数。很多所谓的“优化”反而破坏了TCP的可靠性保证,得不偿失。

最后分享一个我常用的练习方法:用脚本批量建立和关闭连接,同时抓包,然后用过滤器统计FIN包和RST包的比例。比例正常说明关闭流程健康,RST偏多就要查原因。这个练习成本很低,但能把挥手、抓包、异常判断串成一条线,练几轮下来,四次挥手就真的成了肌肉记忆。后续如果想深入,可以接着研究TCP的拥塞控制、窗口缩放这些话题,抓包工具都是同一套,迁移成本很低。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询