TCP 4(四次挥手)

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。因此,ACKFIN 必须分开发送,导致了四次交互。

为什么主动关闭方要等待 2MSL (TIME_WAIT 状态)?

MSL 是报文最大生存时间,网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。

客户端在发送完最后一个 ACK 后,会进入 TIME_WAIT 状态并等待 2MSL(Maximum Segment Lifetime,报文段最大生存时间)的时间,主要有两个目的:

  1. 确保最后一个 ACK 能可靠到达 :如果这个 ACK 在网络中丢失了,服务端会因超时而重传 FIN 报文。客户端在 TIME_WAIT 期间如果收到重传的 FIN,就可以重发 ACK,保证服务端能正常关闭。
  2. 让本连接的所有报文都从网络中消失:等待 2MSL 时间,可以确保当前连接产生的所有报文(包括可能延迟的旧报文)都已失效。这样可以防止这些"过期"的报文干扰到之后可能建立的、使用相同四元组(源IP、源端口、目的IP、目的端口)的新连接,避免数据混乱。

第一次挥手丢失了,会发生什么?

客户端行为:超时重传

  1. 等待确认 :客户端在发送出 FIN 报文后,会进入 FIN_WAIT_1 状态,并启动一个定时器,等待服务端返回 ACK 确认。
  2. 触发重传 :由于 FIN 报文丢失,客户端迟迟收不到服务端的 ACK。当定时器超时后,客户端会认为这个 FIN 报文可能已经丢失,于是触发超时重传机制 ,再次发送 FIN 报文。
  3. 重传次数限制 :这个重传过程不会无限进行。在 Linux 系统中,重传的次数由内核参数 tcp_orphan_retries 控制。
  4. 最终放弃 :如果客户端在重传了指定次数后,依然没有收到服务端的任何回应,它会认为服务端已经不可达或崩溃。此时,客户端会放弃关闭连接的尝试,单方面地关闭连接,释放本地资源,并进入 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

  1. 状态 :服务端的应用程序调用 close() 后,发送了 FIN 报文,并进入 LAST_ACK 状态。
  2. 动作 :服务端开始等待客户端的第四次挥手(ACK)来最终确认关闭。
  3. 应对丢失 :由于 FIN 报文丢失,服务端迟迟收不到 ACK。此时,服务端的超时重传机制会被触发,它会重新发送 FIN 报文
  4. 重传限制 :这个重传过程不是无限的。在 Linux 系统中,重传次数由内核参数 tcp_orphan_retries 控制。
  5. 最终结局 :如果重传达到最大次数后,服务端依然没有收到客户端的 ACK,它就会判定客户端已不可达,于是单方面强制关闭连接,释放本地资源,进入 CLOSED 状态。

客户端(主动关闭方)的视角:超时等待

  1. 状态 :客户端在收到第二次挥手后,早已进入 FIN_WAIT_2 状态。这个状态的含义就是"我已经关闭了发送通道,现在正在等待对方也关闭它的发送通道"。
  2. 动作 :客户端会一直等待服务端的 FIN 报文。
  3. 应对丢失 :由于 FIN_WAIT_2 状态有一个超时计时器(由 tcp_fin_timeout 参数控制,默认通常是 60 秒),如果在这个时间内客户端一直没收到服务端的 FIN 报文,它就会认为服务端可能已经崩溃或网络不通。
  4. 最终结局 :计时器超时后,客户端会单方面关闭连接,释放资源,进入 CLOSED 状态。

双方状态对比

角色 当前状态 心理活动 动作
服务端 LAST_ACK "我发了 FIN,怎么没收到最后的 ACK?我再发一次 FIN 试试。" 重传 FIN,直到收到 ACK 或达到重传上限后强制关闭。
客户端 FIN_WAIT_2 "我已经准备好了,就等对方的 FIN 了。怎么这么久还没来?" 等待 FIN,直到等待超时后强制关闭。

简单来说,第三次挥手丢失后,服务端会主动重试,而客户端则会耐心等待,但两者都有各自的"耐心极限"。一旦超过极限,双方都会单方面关闭连接,最终导致连接在两端都被释放,但关闭过程不够优雅。

第四次挥手丢失了,会发生什么?

如果 TCP 四次挥手中的第四次挥手(即客户端发送的最后一个 ACK 报文)丢失了,TCP 协议会通过其可靠性机制来确保连接最终能够正确关闭。这个过程主要涉及服务端的超时重传和客户端的 TIME_WAIT 状态。

服务端(被动关闭方)的视角:重传 FIN

  1. 状态 :服务端在发送完第三次挥手(FIN)后,会进入 LAST_ACK 状态,等待客户端的最后一个 ACK 来确认关闭。
  2. 动作 :服务端启动一个定时器,等待 ACK 的到来。
  3. 应对丢失 :由于 ACK 报文丢失,服务端在定时器超时后仍未收到确认。它会认为自己的 FIN 报文可能丢失了,于是触发超时重传机制重新发送 FIN 报文
  4. 重传限制 :这个重传过程同样受到 tcp_orphan_retries 参数的控制。如果重传多次后依然收不到 ACK,服务端会单方面强制关闭连接,进入 CLOSED 状态。

客户端(主动关闭方)的视角:处理重传与 TIME_WAIT

  1. 状态 :客户端在发送完第四次挥手(ACK)后,会立即进入 TIME_WAIT 状态,并启动一个时长为 2MSL(报文段最大生存时间)的定时器。
  2. 动作 :客户端处于 TIME_WAIT 状态,等待可能出现的、迟到的报文。
  3. 应对重传
    • 如果客户端收到了服务端因超时而重传的 FIN 报文,它会认为之前的 ACK 丢失了。
    • 于是,客户端会再次发送 ACK 报文作为回应。
    • 同时,它会重置 TIME_WAIT 的 2MSL 定时器,从头开始计时。
  4. 最终结局 :客户端会一直停留在 TIME_WAIT 状态,直到 2MSL 的定时器超时。这段时间足以让服务端收到 ACK 并正常关闭。定时器超时后,客户端也进入 CLOSED 状态,连接彻底关闭。

双方状态对比

角色 当前状态 心理活动 动作
服务端 LAST_ACK "我发了 FIN,怎么没收到最后的 ACK?我再发一次 FIN 试试。" 重传 FIN,直到收到 ACK 或达到重传上限后强制关闭。
客户端 TIME_WAIT "我发了 ACK,但我不确定对方收到没。我得等一会儿,如果对方重传 FIN,我就再回一个 ACK。" 等待并重传 ACK,重置 2MSL 定时器,确保服务端能可靠关闭。

简单来说,第四次挥手丢失后,服务端会主动重试,而客户端则通过 TIME_WAIT 状态来"兜底",确保能响应服务端的重传,从而保证连接能够被可靠、优雅地关闭。

相关推荐
汤愈韬2 小时前
网络安全之网络基础知识
服务器·网络协议·网络安全·security
zzzsde2 小时前
【Linux】进程间通信(2)命名管道&&共享内存
linux·运维·服务器
菱玖2 小时前
Centos重连IP改变问题解决
linux·tcp/ip·centos
SpikeKing2 小时前
Server - 服务器 CentOS 安装与配置 Docker
服务器·docker·centos
一个行走的民2 小时前
Ceph PG 状态详解与线上故障处理
网络·ceph
RTC老炮2 小时前
WebRTC PCC (Performance-oriented Congestion Control) 算法精解
网络·算法·webrtc
idolao2 小时前
傲梅分区助手 使用教程:免安装硬盘分区管理工具
linux·运维·服务器
初遇见2 小时前
【DGX Spark v3.0:基于多智能体交互网络与 Alpaca 实盘集成的企业级量化交易系统】
大数据·网络·spark·nvidia