TCP 四次挥手
TCP 四次挥手是 TCP 协议中用于优雅、可靠地断开一个已建立连接的标准化过程。这个过程之所以需要四次报文交互,核心原因在于 TCP 是全双工通信协议,即数据在两个方向上可以独立传输,因此关闭连接时,每个方向的通道都必须被单独关闭。
我们假设客户端(Client)主动发起关闭连接,服务端(Server)被动关闭。
第一次挥手:客户端发起关闭
- 动作 :客户端的应用程序调用
close()方法,其 TCP 协议栈会向服务端发送一个FIN(Finish) 标志位为 1 的报文段。- 含义:这个报文的意思是"我已经没有数据要发送了,请求关闭从我这里到你的数据通道"。
- 状态变化 :发送完毕后,客户端进入
FIN_WAIT_1状态。
第二次挥手:服务端确认收到
- 动作 :服务端收到客户端的
FIN报文后,会立即回复一个ACK(Acknowledgment) 确认报文。- 含义:这个报文的意思是"好的,我知道你那边已经没有数据要发了,我已经收到了你的关闭请求"。
- 关键点 :此时,服务端可能还有数据需要发送给客户端。因此,TCP 连接进入**半关闭(Half-Close)**状态。客户端到服务端的方向已关闭,但服务端到客户端的方向依然开放,可以继续传输数据。
- 状态变化 :服务端进入
CLOSE_WAIT状态。客户端收到这个ACK后,进入FIN_WAIT_2状态,等待服务端的关闭请求。
第三次挥手:服务端发起关闭
- 动作 :当服务端的应用程序也处理完所有剩余数据后,会调用
close()方法,其 TCP 协议栈向客户端发送一个FIN报文。- 含义:这个报文的意思是"我这边也没有数据要发送了,现在请求关闭从我这里到你的数据通道"。
- 状态变化 :发送完毕后,服务端进入
LAST_ACK状态,等待客户端的最终确认。
第四次挥手:客户端最终确认
- 动作 :客户端收到服务端的
FIN报文后,会回复最后一个ACK报文作为确认。- 含义:这个报文的意思是"好的,我知道你那边也关闭了,连接可以彻底断开了"。
- 状态变化 :服务端收到这个
ACK后,立即进入CLOSED状态,连接彻底关闭。而客户端在发送完ACK后,并不会立刻关闭,而是进入TIME_WAIT状态。
为什么是四次而不是三次?
关键在于第二次和第三次挥手不能合并。
- 三次握手时,服务端收到客户端的连接请求(SYN)后,可以立刻同意并把自己的连接请求(SYN)一起发回去(SYN+ACK),因为"同意连接"是一个即时动作。
- 四次挥手时 ,当服务端收到客户端的关闭请求(FIN)后,它可能还有数据没发完,所以它只能先回复一个
ACK表示"收到了",但不能立刻发送自己的FIN。只有等自己的数据也发完后,才能发送FIN。因此,ACK和FIN必须分开发送,导致了四次交互。
为什么主动关闭方要等待 2MSL (TIME_WAIT 状态)?
MSL 是报文最大生存时间,网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
客户端在发送完最后一个
ACK后,会进入TIME_WAIT状态并等待 2MSL(Maximum Segment Lifetime,报文段最大生存时间)的时间,主要有两个目的:
- 确保最后一个 ACK 能可靠到达 :如果这个
ACK在网络中丢失了,服务端会因超时而重传FIN报文。客户端在TIME_WAIT期间如果收到重传的FIN,就可以重发ACK,保证服务端能正常关闭。- 让本连接的所有报文都从网络中消失:等待 2MSL 时间,可以确保当前连接产生的所有报文(包括可能延迟的旧报文)都已失效。这样可以防止这些"过期"的报文干扰到之后可能建立的、使用相同四元组(源IP、源端口、目的IP、目的端口)的新连接,避免数据混乱。
第一次挥手丢失了,会发生什么?
客户端行为:超时重传
- 等待确认 :客户端在发送出
FIN报文后,会进入FIN_WAIT_1状态,并启动一个定时器,等待服务端返回ACK确认。- 触发重传 :由于
FIN报文丢失,客户端迟迟收不到服务端的ACK。当定时器超时后,客户端会认为这个FIN报文可能已经丢失,于是触发超时重传机制 ,再次发送FIN报文。- 重传次数限制 :这个重传过程不会无限进行。在 Linux 系统中,重传的次数由内核参数
tcp_orphan_retries控制。- 最终放弃 :如果客户端在重传了指定次数后,依然没有收到服务端的任何回应,它会认为服务端已经不可达或崩溃。此时,客户端会放弃关闭连接的尝试,单方面地关闭连接,释放本地资源,并进入
CLOSED状态。
服务端状态:毫不知情
在整个过程中,由于第一次挥手的
FIN报文始终没有到达,服务端完全不知道客户端想要关闭连接的意图。
- 状态不变 :服务端的连接状态会一直保持在
ESTABLISHED,认为这个连接是完全正常且活跃的。- 资源占用:服务端会继续为这个连接占用着系统资源,如内存缓冲区、文件描述符等。
最终结果:半连接(孤儿连接)
最终会导致一个状态不一致的"半连接"或"孤儿连接"(Orphaned Connection):
- 客户端:认为连接已经彻底关闭。
- 服务端 :认为连接依然处于
ESTABLISHED状态。
这种只在服务端单方面存在的无效连接会持续占用系统资源。为了清理这类"僵尸连接",TCP 协议提供了 Keepalive(保活)机制。当一个连接长时间没有数据交互时,服务端会主动发送保活探测包,如果客户端已经关闭,它会回复一个 RST(重置)报文,服务端收到后便会强制关闭这个连接。
TCP 的 Keepalive
TCP 的 Keepalive(保活)机制就像是一个"心跳检测器",专门用来探测长时间没有数据传输的连接是否还活着。它的详细工作流程可以分为触发 、探测 和裁决三个阶段。
1. 触发阶段:漫长的等待
Keepalive 机制默认是关闭 的,需要应用程序显式开启(设置
SO_KEEPALIVE选项)。一旦开启,当 TCP 连接在空闲时间 (即没有数据传输)超过设定阈值(Linux 默认为 7200秒 / 2小时)后,保活定时器才会被激活。
2. 探测阶段:发送"探针"
当定时器触发后,服务端(或开启保活的一端)会向对端发送一个特殊的保活探测报文(Keepalive Probe)。
- 报文特征:这是一个纯 ACK 包,不包含任何数据。它的序列号(Seq)通常是上一个已确认序列号减 1(例如上一个确认的是 100,探测包 Seq 就是 99)。
- 目的:这个序列号"过时"的包不会干扰正常的数据传输窗口,但能强迫对端 TCP 协议栈做出反应。
3. 裁决阶段:三种结局
发送探测包后,根据对端的不同反应,连接会走向三种不同的结局:
结局 A:连接正常(收到 ACK)
- 现象 :对端主机正常运行且网络通畅。虽然它可能也没发数据,但它的 TCP 协议栈收到这个"过时"的探测包后,会按照协议规范回复一个当前的 ACK 确认包。
- 结果 :发起端收到 ACK,判定连接存活。它会重置保活定时器,继续等待下一个 2 小时(或设定的空闲周期)。
结局 B:对端崩溃或不可达(无响应)
- 现象:对端主机崩溃、断电,或者中间网络中断。发起端发送探测包后,石沉大海,收不到任何回应。
- 重试机制 :发起端不会立刻放弃,而是会每隔一定时间(Linux 默认 75秒)重发探测包。
- 结果 :如果连续发送了指定次数(Linux 默认 9次 )后依然没有响应,发起端就会判定连接已断开(死亡) 。此时,发起端会自动关闭连接,释放资源,并通知应用层(通常报错
ETIMEDOUT)。结局 C:对端重启(收到 RST)
- 现象:对端主机之前崩溃了,现在刚刚重启。它收到了探测包,但因为已经重启,它完全不记得之前有过这个连接。
- 结果 :对端会直接回复一个 RST(复位) 报文段,意思是"你是谁?我不认识这个连接"。发起端收到 RST 后,会立即强制关闭 连接并释放资源(通常报错
ECONNRESET)。
Linux 系统的默认参数配置:
| 参数名 | 默认值 | 含义 |
|---|---|---|
| tcp_keepalive_time | 7200秒 (2小时) | 连接空闲多久后,开始发送第一个探测包。 |
| tcp_keepalive_intvl | 75秒 | 探测包发送失败后,每隔多久重试一次。 |
| tcp_keepalive_probes | 9次 | 最多重试多少次,超过次数则判定死亡。 |
如果没有 Keepalive,服务端会永远认为连接是 ESTABLISHED 的,资源永远无法释放。
有了 Keepalive,服务端在 2 小时 11 分钟后(默认配置下)会发现客户端"失联"了,从而自动清理掉这个"僵尸连接",防止内存泄漏和文件描述符耗尽。
第二次挥手丢失了,会发生什么?
如果 TCP 四次挥手中的第二次挥手(即服务端回复的 ACK 确认报文)丢失了,情况会变得稍微复杂一些,这会导致客户端和服务端出现短暂的"认知不同步"。
1. 客户端(主动关闭方)的反应:死等与重传
- 状态 :客户端此时处于
FIN_WAIT_1状态,正在等待服务端的ACK以进入FIN_WAIT_2。- 动作 :由于
ACK丢失,客户端迟迟收不到确认。客户端的定时器超时后,它会误以为是**第一次挥手(FIN)**丢失了。- 重传机制 :客户端会触发超时重传,再次发送 FIN 报文。
- 最终结局 :
- 如果重传的 FIN 被服务端收到,服务端会补发
ACK,连接恢复正常关闭流程。- 如果一直收不到
ACK,客户端在达到最大重传次数(由tcp_orphan_retries控制)后,会认为服务端已死,单方面强制关闭连接,进入CLOSED状态。
服务端(被动关闭方)的反应:忙碌与补发
- 状态 :服务端其实已经收到了客户端的 FIN,并且内核已经回复了
ACK,状态已变为CLOSE_WAIT。- 动作 :
- 关于 ACK :TCP 协议规定,ACK 报文是不会主动重传的。所以服务端不会因为 ACK 丢了就重发 ACK。
- 应对重传 :当服务端收到客户端重传的 FIN 报文时,它会意识到之前的 ACK 可能丢了,于是它会再次发送一个 ACK 给客户端。
- 后续流程 :服务端此时正在等待应用程序调用
close()。一旦应用程序处理完数据并调用close(),服务端就会发送第三次挥手(FIN)。
为什么 ACK 丢了比较麻烦?
与第一次挥手(FIN)丢失不同,第二次挥手(ACK)丢失后,服务端不会主动去探测或重试。
- FIN 丢失:发送方(客户端)没收到 ACK,会主动重传 FIN。
- ACK 丢失 :发送方(服务端)绝不重传 ACK。只能依靠接收方(客户端)重传 FIN 来"唤醒"服务端补发 ACK。
双方状态对比
| 角色 | 当前状态 | 心理活动 | 动作 |
|---|---|---|---|
| 客户端 | FIN_WAIT_1 |
"我发了 FIN,怎么没回音?是不是丢了?" | 重传 FIN,直到收到 ACK 或超时关闭。 |
| 服务端 | CLOSE_WAIT |
"我已经回过 ACK 了,我在等应用程序关连接。" | 不重传 ACK。若收到重传的 FIN,则补发 ACK。 |
第三次挥手丢失了,会发生什么?
如果 TCP 四次挥手中的第三次挥手(即服务端发送的 FIN 报文)丢失了,连接关闭的流程会受阻,双方都会通过各自的机制来应对。
服务端(被动关闭方)的视角:重传 FIN
- 状态 :服务端的应用程序调用
close()后,发送了FIN报文,并进入LAST_ACK状态。- 动作 :服务端开始等待客户端的第四次挥手(
ACK)来最终确认关闭。- 应对丢失 :由于
FIN报文丢失,服务端迟迟收不到ACK。此时,服务端的超时重传机制会被触发,它会重新发送FIN报文。- 重传限制 :这个重传过程不是无限的。在 Linux 系统中,重传次数由内核参数
tcp_orphan_retries控制。- 最终结局 :如果重传达到最大次数后,服务端依然没有收到客户端的
ACK,它就会判定客户端已不可达,于是单方面强制关闭连接,释放本地资源,进入CLOSED状态。
客户端(主动关闭方)的视角:超时等待
- 状态 :客户端在收到第二次挥手后,早已进入
FIN_WAIT_2状态。这个状态的含义就是"我已经关闭了发送通道,现在正在等待对方也关闭它的发送通道"。- 动作 :客户端会一直等待服务端的
FIN报文。- 应对丢失 :由于
FIN_WAIT_2状态有一个超时计时器(由tcp_fin_timeout参数控制,默认通常是 60 秒),如果在这个时间内客户端一直没收到服务端的FIN报文,它就会认为服务端可能已经崩溃或网络不通。- 最终结局 :计时器超时后,客户端会单方面关闭连接,释放资源,进入
CLOSED状态。
双方状态对比
| 角色 | 当前状态 | 心理活动 | 动作 |
|---|---|---|---|
| 服务端 | LAST_ACK |
"我发了 FIN,怎么没收到最后的 ACK?我再发一次 FIN 试试。" | 重传 FIN,直到收到 ACK 或达到重传上限后强制关闭。 |
| 客户端 | FIN_WAIT_2 |
"我已经准备好了,就等对方的 FIN 了。怎么这么久还没来?" | 等待 FIN,直到等待超时后强制关闭。 |
简单来说,第三次挥手丢失后,服务端会主动重试,而客户端则会耐心等待,但两者都有各自的"耐心极限"。一旦超过极限,双方都会单方面关闭连接,最终导致连接在两端都被释放,但关闭过程不够优雅。
第四次挥手丢失了,会发生什么?
如果 TCP 四次挥手中的第四次挥手(即客户端发送的最后一个 ACK 报文)丢失了,TCP 协议会通过其可靠性机制来确保连接最终能够正确关闭。这个过程主要涉及服务端的超时重传和客户端的 TIME_WAIT 状态。
服务端(被动关闭方)的视角:重传 FIN
- 状态 :服务端在发送完第三次挥手(
FIN)后,会进入LAST_ACK状态,等待客户端的最后一个ACK来确认关闭。- 动作 :服务端启动一个定时器,等待
ACK的到来。- 应对丢失 :由于
ACK报文丢失,服务端在定时器超时后仍未收到确认。它会认为自己的FIN报文可能丢失了,于是触发超时重传机制 ,重新发送FIN报文。- 重传限制 :这个重传过程同样受到
tcp_orphan_retries参数的控制。如果重传多次后依然收不到ACK,服务端会单方面强制关闭连接,进入CLOSED状态。
客户端(主动关闭方)的视角:处理重传与 TIME_WAIT
- 状态 :客户端在发送完第四次挥手(
ACK)后,会立即进入TIME_WAIT状态,并启动一个时长为 2MSL(报文段最大生存时间)的定时器。- 动作 :客户端处于
TIME_WAIT状态,等待可能出现的、迟到的报文。- 应对重传 :
- 如果客户端收到了服务端因超时而重传的
FIN报文,它会认为之前的ACK丢失了。- 于是,客户端会再次发送
ACK报文作为回应。- 同时,它会重置
TIME_WAIT的 2MSL 定时器,从头开始计时。- 最终结局 :客户端会一直停留在
TIME_WAIT状态,直到 2MSL 的定时器超时。这段时间足以让服务端收到ACK并正常关闭。定时器超时后,客户端也进入CLOSED状态,连接彻底关闭。
双方状态对比
| 角色 | 当前状态 | 心理活动 | 动作 |
|---|---|---|---|
| 服务端 | LAST_ACK |
"我发了 FIN,怎么没收到最后的 ACK?我再发一次 FIN 试试。" | 重传 FIN,直到收到 ACK 或达到重传上限后强制关闭。 |
| 客户端 | TIME_WAIT |
"我发了 ACK,但我不确定对方收到没。我得等一会儿,如果对方重传 FIN,我就再回一个 ACK。" | 等待并重传 ACK,重置 2MSL 定时器,确保服务端能可靠关闭。 |
简单来说,第四次挥手丢失后,服务端会主动重试,而客户端则通过 TIME_WAIT 状态来"兜底",确保能响应服务端的重传,从而保证连接能够被可靠、优雅地关闭。