资讯动态

TCP四次挥手:状态机、CLOSE_WAIT与TIME_WAIT排障

发布时间:2026/10/13 3:51:07 来源:尧图企业网站定制
上周处理一个线上接口耗时飙升的问题登录服务器一看连接状态里躺着几百个CLOSE_WAIT同事第一反应是“TCP四次挥手出问题了”。说实话TCP四次挥手这个名词背起来很简单——FIN、ACK、FIN、ACK——但真正排障的时候状态、时序、超时参数全都搅在一起光靠背概念根本不够用。这篇博客就把四次挥手从原理到抓包、从状态机到线上排查完整过一遍。适合刚接触网络协议的后端开发者也适合被CLOSE_WAIT/TIME_WAIT折腾过、想系统梳理一遍的运维或SRE同学。内容是老话题但我会把网上讲得含糊的地方都展开尤其是那些“为什么”和“怎么办”。1. 四次挥手到底在挥什么1.1 为什么断开连接需要四个报文你可能会想建立连接要三次握手断开连接搞四次是不是有点浪费其实不是。TCP是全双工协议一条连接相当于两个独立方向的数据通道。打开连接时双方都要确认“你能听到我说话吗、我能听到你说话吗”两个方向所以三次握手是建立双方收发能力的必要交互而关闭连接时由于数据可以单向停止TCP允许先关一个方向、保留另一个方向继续传数据这就是“半关闭”。因此每个方向都要经历一次“我想结束了”的通知FIN和一次“我收到你的结束了”的确认ACK所以就出现了一方发出FIN、另一方回ACK然后另一方也发出FIN、先发起方再回ACK的四个报文。这四个报文不是冗余而是TCP针对全双工通道做优雅关闭的最低成本。举个例子。假设A和B正在通电话A说完自己的事情之后说“我说完了你可以继续说”这时候A的通知就是FINB回一句“知道了”就是ACK。但B可能还有一肚子话没说完所以连接还不能完全断开要等B也说完“我说完了”A再回一句“知道了”通话才真正结束。这个类比虽然简单但很贴近TCP设计里的“半关闭”A关闭的是A到B这个方向B到A方向还在工作。如果B说完自己的话后直接挂电话相当于ACK和FIN在时间上很接近但语义上依然是两个动作。这也解释了为什么教科书普遍强调“四次挥手至少需要四步”。假如被动关闭方在收到FIN后刚好也想关闭理论上可以尝试把ACK和FIN放在同一个报文里捎带抓包时确实可能看到三个包或两个包但那是报文合并与延迟确认带来的现象不是TCP四次挥手的基本形态。基本形态里ACK代表“我收到了你的关闭请求”FIN代表“我也要关闭我的方向”两者的含义不同不能因为时间接近就混为一谈。1.2 三次握手和四次挥手的对称关系三次握手大家都很熟SYN、SYNACK、ACK。为什么建立连接时SYN和ACK能合并成SYNACK断开连接时FIN和ACK却不能随意合并关键在于合并的时机不同。建连时服务端收到客户端的SYN只需要确认“我可以接受你这个初始序号并且我也要建立连接”此时服务端还没有开始向客户端发送业务数据所以立即用一个报文回SYNACK是天然合理的。断开连接时主动关闭方发出FIN后被动关闭方虽然收到了关闭请求但它自己这边的数据可能还没发完所以只能先回复ACK表示“我收到你要断开的请求了”然后继续把剩余数据发出去发完之后才能发FIN。这个“可能还有数据要发”的窗口是无法预知的因此四次挥手不能像三次握手那样把确认和发起合并成一次。有经验的面试官经常在这个点上追问“如果被动关闭方在收到FIN时数据也恰好发完了呢能不能三次”答案是从报文语义上仍然不建议合并因为如果被动关闭方把ACK和FIN放在同一个报文里主动关闭方收到后需要同时理解“我收到你的FIN”和“我也要关闭”但从状态机上看这个组合报文到达时主动关闭方应直接进入FIN_WAIT_2之后再收到FIN还是直接处理两个事件虽然TCP实现里可以处理这种情况但协议设计为了保持状态转移清晰、兼容半关闭才普遍采用四段式关闭。实际抓包里经常看到只出现三个报文或两个报文主要是因为延迟确认、捎带确认和接收缓冲区的状态而不是协议本身变成了三次。另外要强调一点主动关闭方不一定是客户端。很多讲解默认“客户端发FIN、服务端回ACK、服务端再发FIN、客户端回ACK”那只是最常见场景。服务端同样可以主动断开连接比如服务端空闲超时踢掉某个连接。这时候状态迁移还是会走四次挥手只是发起方换成了服务器。真正需要记住的是谁先发FIN谁就是主动关闭方谁进入TIME_WAIT。2. 从状态到数据包把四次挥手看成一个过程2.1 状态机变化与四个报文的时间线如果只用一句话描述四次挥手那就是一个状态机的推进过程。假设A是主动关闭方B是被动关闭方初始状态双方都是ESTABLISHED。完整过程如下表所示步骤报文方向报文内容A状态B状态初始无无ESTABLISHEDESTABLISHED1A → BFINFIN_WAIT_1ESTABLISHED2B → AACKFIN_WAIT_2CLOSE_WAIT3B → AFINFIN_WAIT_2或TIME_WAITLAST_ACK4A → BACKTIME_WAITCLOSED等待2MSL无无CLOSEDCLOSED注意第三步里A的状态在发出最后一个ACK之前其实一直处于FIN_WAIT_2只有发出最后一个ACK后才进入TIME_WAIT。所以更准确的说法是A收到B的FIN后回复ACK随即进入TIME_WAIT。我把这个状态变化拆开讲一下第一步A的应用层调用close()内核发送FINA进入FIN_WAIT_1。这个状态表示“我已经发出关闭请求正在等待对端确认”。如果中间网络丢包A会在超时后重传FIN。第二步B收到FIN后内核立刻回复ACK同时B进入CLOSE_WAIT。CLOSE_WAIT这个状态非常关键它意味着被动关闭方已经知道了对端要断开但自己这个方向的数据传输还可以继续。应用层仍然可以往这个连接上写数据B写完所有数据后才会调用close()发送FIN。第三步B发送完所有数据并调用close()发送FINB进入LAST_ACK。LAST_ACK就是“我已经发了FIN正在等待对方最后的ACK”。如果这个FIN丢失B会重传直到收到ACK或超时。第四步A收到B的FIN后回复一个ACKA进入TIME_WAIT。B收到这个ACK后进入CLOSED连接从被动关闭方视角彻底关闭。A在TIME_WAIT中等待2MSL后也会进入CLOSED。这里面最容易被忽略的是FIN_WAIT_2。如果B一直没有发送FINA会一直卡在FIN_WAIT_2直到触发tcp_fin_timeout超时。网上很多教程把TIME_WAIT讲得很细但很少提FIN_WAIT_2实际上生产环境里FIN_WAIT_2堆积常见于对端进程卡死或网络单通和CLOSE_WAIT一样值得关注。2.2 抓包确认四次挥手状态机是理论想要直观看到四次挥手最好的方式就是抓包。我平时最常用tcpdump命令大概是这样的tcpdump -i any -nn -s0 tcp port 8080 and ((tcp[13] 1 ! 0) or (tcp[13] 16 ! 0))这个表达式的意思是抓取目标端口或源端口为8080的报文同时只关心FINtcp[13] 1或ACKtcp[13] 16置位的包。如果嫌表达式太长直接抓全量流量也不是不行只是在流量大的时候输出会比较吵tcpdump -i any -nn tcp port 8080 -c 30抓包时可以找一个测试环境用curl或nc模拟一次HTTP请求比如在一台机器执行curl http://10.0.0.20:8080/另一台机器上抓包。因为HTTP请求结束后连接会关闭通常能看到SYN建连的三包然后经过若干数据报文之后出现四次挥手。简化后的抓包输出大概是这样的1 10.0.0.10.54321 10.0.0.20.8080: Flags [F.], seq 421, ack 322 2 10.0.0.20.8080 10.0.0.10.54321: Flags [.], ack 422 3 10.0.0.20.8080 10.0.0.10.54321: Flags [F.], seq 322, ack 422 4 10.0.0.10.54321 10.0.0.20.8080: Flags [.], ack 323这里[F.]表示FIN和ACK同时置位[.]表示纯ACK。平均看到三步甚至两步都很正常因为延迟确认会把第二个ACK推迟到与FIN一起发送。但注意第三个报文里的ACK字段是422代表B已经确认了第421字节之后的FIN第四个报文里的ACK字段是323代表A确认了B的FIN。看抓包时要关注sequence和ack的变化才能真正理解这四个报文是不是在推进关闭流程。抓包还有一个作用判断谁先主动关闭。谁发出的第一个[F.]报文谁就是主动关闭方。许多线上问题定位到最后都会发现不是“客户端主动断开”或“服务器主动断开”的固定答案而是从抓包里直接确认的。建议把抓包作为排障的第一动作而不是登录服务器看一眼ss就下结论。3. TIME_WAIT与CLOSE_WAIT线上排障的重灾区3.1 主动关闭方的TIME_WAIT为什么它存在为何讨厌TIME_WAIT是主动关闭方在回复最后一个ACK后进入的状态持续时间是2MSL。MSL是“报文最大生存时间”Linux上常见实现把MSL定为30秒所以TIME_WAIT一般持续60秒。这个状态存在的意义有两个缺一不可。第一个意义是保证最后一个ACK能够到达对端。假设A回复的ACK丢失B在LAST_ACK状态等待一段时间后会重发FIN。如果A已经直接进入CLOSED那么收到重传FIN后就只能回复RST这不仅会破坏优雅关闭还对端根本不知道连接已经关闭。A留在TIME_WAIT里收到重传FIN后就能重新发送ACK保证B正常关闭。第二个意义是让旧报文在网络中彻底消失。TCP报文可能在内网里意外延迟重放。如果主动关闭方立刻关闭并随后新建一条相同四元组连接旧连接中延迟到达的报文就可能混进新连接造成数据错乱。TIME_WAIT等待2MSL可以确保网络中所有属于旧连接的报文都已经消亡。但这带来一个实际问题高并发短连接场景下主动关闭方会积累大量TIME_WAIT。因为TIME_WAIT持续约60秒每秒上千个短连接就会同时存在数万个TIME_WAIT占用本地端口和连接表项严重时会出现“Cannot assign requested address”也就是本地端口耗尽。遇到TIME_WAIT多我的第一反应不是调参数而是看业务形态。如果应用层大量使用短连接比如每次请求都新建HTTP连接、用完就断开那么TIME_WAIT多是正常现象。根治办法是改用连接池让连接长期复用而不是不断新建销毁。如果业务必须使用短连接再考虑内核参数优化而且必须知道哪些参数能用、哪些不能盲目开。3.2 被动关闭方的CLOSE_WAIT多半是代码BugCLOSE_WAIT出现在被动关闭方语义很明确被动关闭方已经回复ACK正在等待本地应用调用close()但应用层一直没有调。这个状态基本不是内核参数能解决的而是业务代码里的连接泄漏。我处理过一个典型场景。模拟项目X里有个模块定期调用外部服务拉取数据代码用第三方HTTP客户端但异常分支没有关闭响应体。正常情况下连接池会复用连接请求结束后自动归还或关闭一旦某个请求抛了超时异常代码直接return响应体没有被消费也没有被关闭底层连接就一直挂在CLOSE_WAIT。刚开始只有零星几个随着异常请求增多CLOSE_WAIT越积越多最终连接池耗尽服务出现大量超时。定位方法很简单先用ss -antp查看CLOSE_WAIT对应的PID再用lsof -p PID找出具体文件描述符配合日志确认是哪个模块的哪个调用路径没有释放连接。正确的修复方式是在finally块里关闭响应体。比如Java里用try-with-resourcestry (CloseableHttpResponse response httpClient.execute(request)) { // 处理响应 } catch (IOException e) { // 记录异常 } // 不需要手动close异常时也会自动关闭响应Python用with语句with httpx.Client() as client: with client.stream(GET, url) as response: for line in response.iter_lines(): ...用with的好处是无论正常还是异常退出都会关闭底层连接。很多语言的标准库和第三方库都有类似机制关键是养成用完即关的习惯。CLOSE_WAIT排查中的一个细节先确认是哪个进程、哪个目标IP的CLOSE_WAIT。如果CLOSE_WAIT只集中在某个外部服务IP那大概率是调用该服务的代码路径有问题如果分布在各处还要检查是否有全局的拦截器或切面把响应流吞掉了。3.3 被忽略的RST挥手被中断的真相四次挥手是理想模型现实中大量连接并不是被四次挥手正常关闭的而是被RST中断。RST不是四次挥手中的一个步骤而是异常关闭信号一旦出现RST后面的FIN/ACK流程就全部作废。常见产生RST的场景包括设置了SO_LINGER为0后关闭socket系统直接发RST而不是FIN对端已经崩溃本端持续发送数据时收到ICMP错误从而触发RST连接空闲时间超过中间设备的会话超时中间设备伪造RST连接不存在还收到数据接收缓冲区里有未读取的数据时应用层直接close内核也会发RST。抓包时如果看到RST基本说明握手和挥手都没走完。此时不要想着调TIME_WAIT参数要先确认RST是谁发出的。如果RST源是本机可能是应用层主动reset也可能是因为接收缓冲区有残包导致close时触发了RST如果RST源是对端就要结合对端进程日志排查。很多线上“偶发连接中断”的问题最后都会追溯到某台中间设备或某个中间层的空闲超时配置而不是TCP协议自身的Bug。4. 常见问题速查与实践参数4.1 高频问题的标准答法我整理了一张表把最容易被问到的几个点放在一起问题核心答案常见坑为什么需要四次挥手全双工两个方向分别关闭每个方向需要FINACK想把FIN和ACK合并为三次理解不到半关闭TIME_WAIT为什么存在防止最后一个ACK丢失并等待旧报文在网络中消亡把TIME_WAIT当成异常状态CLOSE_WAIT代表什么被动关闭方等待应用层调用close()一看CLOSE_WAIT多就调内核参数FIN_WAIT_2超时怎么办主动关闭方等待对端FIN超时由tcp_fin_timeout控制误以为是TIME_WAIT超时抓包只看到三个包延迟确认和捎带确认可能合并报文直接怀疑对端实现有问题RST和四次的区别RST是异常关闭不经过FIN/ACK流程把RST当成挥手的变种这张表能应付大多数面试和基础排障。但表背后的逻辑比背答案更重要尤其CLOSE_WAIT和TIME_WAIT的区别建议多从主动方/被动方的角度理解。4.2 调整内核参数前先看这些风险网上很多教程会让你直接改sysctl但我建议先搞清楚每个参数到底干什么。下面是Linux上常见的与四次挥手相关的参数示例net.ipv4.tcp_timestamps 1 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_max_tw_buckets 180000 net.ipv4.tcp_fin_timeout 30 net.ipv4.ip_local_port_range 1024 65535tcp_tw_reuse的意思是允许客户端发起connect的一方在新建连接时复用处于TIME_WAIT状态的套接字。它只对主动发起连接的一方有效服务端被动接受的连接不能靠这个参数规避TIME_WAIT堆积。要使用tcp_tw_reuse一般要求tcp_timestamps为1否则时间戳校验无法工作。tcp_tw_recycle是另一个经常被提到的参数它在NAT环境下很容易引发丢包问题。因为不同客户端经过NAT后的时间戳未必单调递增开启快速回收会导致同一NAT后面的连接被误杀。这个参数在新版内核里已经被移除或废弃不建议再使用。tcp_max_tw_buckets限制系统允许存在的TIME_WAIT套接字最大数量。设置得过小超过上限的TIME_WAIT会被直接丢弃短时间内大量短连接时可能导致新连接异常。调整这个参数时最好监控指标不要为了让ss输出好看就压到极低值。tcp_fin_timeout控制的是FIN_WAIT_2状态超时时间默认常为60秒。调小可以让长期不发送FIN的对端连接更快被回收但也会降低对丢包场景的容忍度。一般应用不需要动它。真正需要记住的是内核参数调整只是缓解不是根治。如果应用层能改成连接池TIME_WAIT自然会大幅下降如果CLOSE_WAIT一直上涨任何网络参数都救不了你只能去修代码。生产环境改任何参数都要先灰度并且观察状态分布。4.3 快速查看连接状态的命令排查四次挥手问题我用得最多的命令是ss。下面这些操作基本够用# 按状态统计连接数 ss -tan | awk {print $1} | sort | uniq -c # 只看TIME_WAIT ss -tan state time-wait # 只看CLOSE_WAIT ss -tan state close-wait # 查看进程与本地地址 ss -tanp | grep 8080如果系统比较老netstat -antp也是同样的用途只是输出格式老一点。统计完状态之后重要的是看比例和变化趋势。TIME_WAIT高多见于短连接业务CLOSE_WAIT持续增长说明代码一定有路径没有释放连接。SYN_SENT高则是连接建立阶段的问题比如对端不监听、防火墙丢包或回包被路由丢弃和四次挥手无关。实际排障时我会先用ss找出可疑连接的四元组再配合tcpdump抓几个包确认是正常关闭还是异常重置。如果只凭“状态数量多”就下结论很容易被表面现象带偏。5. 从面试到实战如何把四次挥手讲得有条理5.1 回答的层次面试里问四次挥手其实是想看你对TCP的理解停留在背概念还是能讲出设计意图。我建议按照下面的层次回答既能控制时间又不会漏掉关键点。第一层先回答“为什么不能只有三次”TCP是全双工协议关闭要对两个方向分别确认所以需要一个方向先发FIN对端ACK再反向FIN再ACK。第二层把状态机说清楚主动关闭方进入FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT被动关闭方进入CLOSE_WAIT、LAST_ACK。第三层解释TIME_WAIT存在的两个原因也就是防丢包和防旧报文串扰。第四层如果有余力补充异常场景对端不发FIN会卡FIN_WAIT_2应用不close会堆积CLOSE_WAIT比如某外部服务SDK未释放连接导致大量CLOSE_WAIT。这样层层递进面试官基本能确认你是真懂。一个比较标准的回答示例是“连接建立后是两个独立方向的数据通道。A想关闭时会先发FIN表示A这个方向没有数据要发了。B收到FIN后回ACK进入CLOSE_WAIT此时B还可以继续发数据。B发完数据后也发FIN进入LAST_ACK。A收到FIN后回ACK进入TIME_WAIT等待两个MSL后关闭。TIME_WAIT有两个作用一是防止最后这个ACK丢失后B重发FIN时无人应答二是确保旧连接报文在网络中彻底消失避免干扰新连接。”这个回答大概半分钟已经包含协议、状态、意义三个维度。5.2 千万别踩的常见表述错误我见过很多人在讲四次挥手时犯同样的错误这里集中纠正一下。第一个错误把主动关闭方默认为客户端。服务端完全可以主动断开连接。谁发第一个FIN谁就是主动关闭方和客户端身份没有必然关系。第二个错误说“最后一个ACK发出后连接立即关闭”。主动关闭方只有在TIME_WAIT经过2MSL后才关闭不是立即关闭。如果忽略这一点就很难解释TIME_WAIT堆积的来源。第三个错误把TIME_WAIT和CLOSE_WAIT搞反。TIME_WAIT在主动关闭方CLOSE_WAIT在被动关闭方。简单记先发FIN的进TIME_WAIT后收到FIN的进CLOSE_WAIT。第四个错误认为四次挥手一定发生。对端进程崩溃、网络单通、中间设备RST都会让四次挥手中断实际报文里经常连FIN都看不到。所以讲协议是讲理想模型排障是看现实报文。第五个错误把FIN说成“关闭连接”而不是“关闭本方向的发送通道”。这个细节如果不注意后面理解半关闭、TIME_WAIT就很容易掉坑。6. 最后一点经验遇到CLOSE_WAIT先别急着改内核参数先查代码遇到TIME_WAIT多也别慌用ss统计一下连接频率然后优先考虑应用层连接复用。TCP四次挥手在网络协议里属于“看似简单但坑很多”的内容把状态机和抓包结合起来排障效率会高很多。最后分享一个小技巧抓包时用ss -tanp找到连接两端的进程确认“谁先发出第一个FIN”。很多时候对端会一边发FIN一边发RST如果只看状态不看报文顺序容易把异常误判成正常关闭。只要看到RST先不要分析TIME_WAITRST本身就是“挥手没走完”的最直接证据。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑