TCP 三次握手和四次挥手(传输层)
TCP 三次握手和四次挥手很容易被背成一张流程图:客户端发 `SYN`,服务端回 `SYN+ACK`,最后再来一个 `ACK`;关闭连接时,再按 `FIN`、`ACK`、`FIN`、`ACK` 走一遍
TCP 三次握手和四次挥手很容易被背成一张流程图:客户端发 ,服务端回 ,最后再来一个 ;关闭连接时,再按 、 、 、 走一遍。 但真正排查网络问题、看抓包或者聊面试题时,只记顺序往往不够。比如:为什么建立连接不是两次握手?服务端收到第三次握手之后,连接到底放在哪个队列?四次挥手中的 ACK 和 FIN 为什么通常分开发?又在什么条件下能合并成三次挥手? 这篇文章就围绕 TCP 连接的建立和释放,把这些问题串起来讲清楚: 1. TCP 三次握手每一步分别做了什么? 2. 为什么建立连接需要三次握手,而不是两次或四次? 3. 半连接队列和全连接队列分别保存什么? 4. TCP 四次挥手每一步分别做了什么? 5. 、 、三次挥手这些细节该怎么理解? 术语约定 :本文正文统一使用 、 这类下划线写法;RFC 中常写作 、 ,Linux 命令中常显示为 、 。它们指向的是同一类 TCP 状态,只是不同语境下的写法不同。 建立连接:TCP 三次握手 在最常见的“一端主动发起连接、一端被动监听”的场景下,TCP 连接通常通过三次握手建立: 1. 第一次握手(SYN) :客户端向服务端发送一个 SYN(Synchronize Sequence Numbers)报文段,其中包含客户端生成的初始序列号(Initial Sequence Number,ISN),例如 。发送后,客户端进入 状态,等待服务端确认。 2. 第二次握手(SYN+ACK) :服务端收到 SYN 后,如果同意建立连接,会回复一个 SYN+ACK 报文段。这个报文段包含两个关键信息: SYN :服务端也需要同步自己的初始序列号,因此会携带服务端生成的 ISN,例如 。 ACK :用于确认收到客户端的 SYN,确认号设置为客户端初始序列号加一,即 。 发送该报文段后,服务端进入 状态。 3. 第三次握手(ACK) :客户端收到服务端的 SYN+ACK 后,会向服务端发送最终确认报文段。由于客户端的 SYN 会消耗一个序列号,因此这个 ACK 报文段的序列号通常为 ;它用于确认服务端的 SYN,确认号为 。发送后,客户端进入 状态。服务端收到这个 ACK 后,也进入 状态。 至此,双方完成初始序列号同步,并确认这条连接可以开始双向传输数据。 什么是半连接队列和全连接队列? 图表加载中 在 TCP 三次握手过程中,服务端内核通常会用两个队列来管理连接请求。下面以常见 Linux 行为为例,不同操作系统、内核版本、socket 选项和部署环境可能会有细节差异。 1. 半连接队列(SYN Queue) : 保存“握手未完成”的请求。服务端收到 SYN 并回复 SYN+ACK 后,连接进入 ,等待客户端最终 ACK。 如果一直收不到 ACK,内核会按重传策略重发 SYN+ACK,最终超时清理。 常见相关参数包括 。在 SYN Flood 场景下,还会涉及 。 2. 全连接队列(Accept Queue) : 保存“握手已完成但应用还没有 accept”的连接。服务端收到最终 ACK 后,连接变为 ,并进入全连接队列,等待应用层 取走。 队列容量受 和系统上限 共同影响。实践中常见有效上限可以近似理解为 ,具体行为仍要看内核版本和应用配置。 总结一下: | 队列 | 作用 | 状态 | 移出条件 | | | | | | | 半连接队列(SYN Queue) | 保存未完成握手的连接 | | 收到 ACK / 超时重传失败 | | 全连接队列(Accept Queue) | 保存已完成握手、等待应用 accept 的连接 | | 被应用层 取出 | 当全连接队列满时, 会影响处理策略: (默认):Linux 通常不会立即返回 RST,而可能丢弃第三次握手 ACK,使服务端继续停留在握手未完全完成的状态,并重传 SYN+ACK。客户端发出第三次 ACK 后,通常已经认为 成功;但服务端并没有把这个连接放进全连接队列,所以客户端后续发送数据时可能迟迟得不到正常响应,最终表现为首包阻塞、读超时或重试。 :直接对客户端回复 ,让连接快速失败。 排查时可以用 看监听 socket。对于 状态, 通常表示当前 backlog 中等待应用 accept 的连接数, 表示 socket backlog 上限。如果 长时间接近 ,就要重点怀疑应用 accept 不及时、backlog 偏小、线程池卡住、GC 抖动或者短时间连接突刺。 当半连接队列满时,如果 ,Linux 会在 SYN backlog 溢出时启用 SYN Cookie:服务端把必要信息编码进返回的 SYN+ACK 中,而不是为每个请求都保留完整的半连接状态。也就是说,SYN Cookie 生效时,服务端不会为这个 SYN 在半连接队列中分配常规状态;只有收到合法的最终 ACK 后,内核才会校验 cookie,并重建连接所需的信息。 但 SYN Cookie 是防护手段,不是扩容手段。它能缓解 SYN Flood 对半连接队列的冲击,但仍会消耗 CPU;如果攻击流量已经打满带宽,SYN Cookie 也无法从根本上恢复可用性。另外,SYN Cookie 模式下部分 TCP 扩展能力可能受限,在高延迟、高带宽链路下可能出现性能退化。 更偏测试用途,不建议作为生产环境默认配置。 为什么要三次握手? TCP 三次握手主要做两件事: 同步双方的初始序列号 ,并且 确认双方的收发路径是可用的 。真正的数据可靠交付,还要依赖后续传输过程中的确认、重传、窗口控制和拥塞控制。 1. 确认双方收发能力,并同步初始序列号 图表加载中 TCP 依赖序列号(SEQ)和确认号(ACK)来保证数据有序、去重和重传。三次握手通过交换并确认双方的 ISN,让两端对“从哪个序号开始收发数据”达成一致,同时避免只凭单向信息就进入已建立状态。 可以用下面这张表来记: | 步骤 | 报文 | 能确认什么 | | | | | | 1 | C→S:SYN | 服务端知道:客户端能发,服务端能收,C→S 方向可达 | | 2 | S→C:SYN+ACK | 客户端知道:服务端能发,客户端能收;同时确认服务端收到了自己的 SYN | | 3 | C→S:ACK | 服务端知道:客户端收到了 SYN+ACK,S→C 方向也被服务端确认;至此握手闭环 | 注意:第 2 步完成时,只有客户端确认了双向可达;服务端此时还不知道自己发出的 SYN+ACK 是否被客户端收到。服务端只有收到第 3 次握手的 ACK 后,才真正确认这个闭环,这也是两次握手不够的核心原因。 2. 防止已失效的连接请求被错误建立 图表加载中 设想一个场景:客户端发送的第一个连接请求 SYN1 因网络延迟而滞留。客户端超时后,重新发送 SYN2,并成功建立连接,数据传输完毕后连接也释放了。此时,延迟的 SYN1 才到达服务端。 如果是两次握手 :服务端收到这个失效的 SYN1 后,可能误认为这是一个新的连接请求,并立即分配资源、建立连接。但客户端已经没有这个连接意图,不会继续配合传输,服务端就会单方面维持一个无效连接。 有了第三次握手 :服务端收到失效的 SYN1 并回复 SYN+ACK 后,还要等待客户端最终 ACK。由于客户端当前没有这个连接状态,它可能直接丢弃,也可能发送 RST。服务端收不到合法 ACK,最终就会清理这个错误连接。 所以,三次握手不是“多发一次包而已”,它让连接建立过程形成闭环,避免网络中的延迟、重复历史请求干扰新的连接。 第 2 次握手已经传回 ACK,为什么还要传回 SYN? 第二次握手里的 ACK 是为了确认“服务端收到了客户端的 SYN”,也就是确认 C→S 方向的请求已经到达。 同时携带 SYN,是因为服务端也需要把自己的 ISN 同步给客户端,并要求客户端确认。只有双方的 ISN 都完成同步,后续可靠传输才有共同的序列号起点。 简言之:ACK 表示“我收到了你的 SYN”,SYN 表示“我也要同步我的初始序列号,请你确认”。 SYN(Synchronize Sequence Numbers)是 TCP 建立连接时使用的同步信号。客户端先发送 SYN,服务端使用 SYN+ACK 应答,最后客户端再用 ACK 确认。这样双方才能完成初始序列号同步,建立一条可用于可靠数据传输的 TCP 连接。 三次握手过程中可以携带数据吗? 普通 TCP 中,第三次握手的 ACK 可以携带数据。RFC 9293 也允许连接同步阶段出现携带数据的报文,但接收端在确认数据有效前,不能把这部分数据交付给应用;通常需要等连接进入 后,应用层才能读到这些数据。 如果第三次握手的 ACK 丢失,但客户端随后发送了一个携带数据且带 ACK 标志的报文,服务端收到后可以把它视为有效的第三次握手确认。连接被认为建立后,服务端再继续处理该数据。 需要注意,这和 TCP Fast Open(TFO)不是一回事。TFO 讨论的是第一次 SYN 就携带应用数据,需要客户端、服务端和系统配置共同支持,不是普通 TCP 默认行为。 断开连接:TCP 四次挥手 TCP 是全双工通信,两端的发送方向彼此独立。关闭连接时,通常需要两个方向分别完成“我不发了”和“我确认你不发了”的过程,所以逻辑上常被讲成“四次挥手”。 不过要注意:四次挥手说的是逻辑动作,不一定意味着抓包时总能看到 4 个独立报文段。在某些场景下,ACK 和 FIN 可以合并在同一个报文段里。 典型流程如下: 1. 第一次挥手(FIN) :客户端,或者任意一方,决定关闭自己的发送方向时,会发送一个 FIN 报文段,表示自己已经没有数据要发送了。该报文段包含一个序列号,例如 。发送后,主动关闭方进入 状态。 2. 第二次挥手(ACK) :服务端收到 FIN 后,会回复 ACK,确认号为 。发送后,服务端进入 状态。客户端收到 ACK 后,进入 状态。此时连接处于 半关闭(Half Close) 状态:客户端到服务端的发送方向已关闭,但服务端仍然可以继续向客户端发送剩余数据。 3. 第三次挥手(FIN) :当服务端确认剩余数据都发送完毕后,也会发送 FIN,表示自己也准备关闭发送方向。该报文段同样包含一个序列号,例如 ;通常也会继续携带当前确认号,例如 。发送后,服务端进入 状态,等待客户端最终确认。 4. 第四次挥手(ACK) :客户端收到服务端的 FIN 后,回复最终 ACK,确认号为 。发送后,客户端进入 状态。服务端收到这个 ACK 后进入 。客户端则在 状态等待 2MSL 后,最终进入 。 这里为了方便理解,用客户端发起关闭作为例子。实际中谁主动关闭连接,谁就会进入 ,这和“客户端 / 服务端”的角色没有必然关系。 注意区分: 半关闭(Half Close) 指一个方向已经发送 FIN,另一个方向仍可继续发送数据; 半开连接(Half Open Connection) 通常指一端崩溃、重启或状态丢失后,另一端仍以为连接存在。两者不是同一个概念。 TCP 连接建立与关闭的常见状态迁移路径如下。图中省略了同时打开、同时关闭、RST、CLOSING 等少见或异常分支。 为什么要四次挥手? 因为 TCP 是全双工的。A 不想发了,不代表 B 也立刻没有数据要发。 举个例子,A 和 B 打电话,通话即将结束: 1. A 说:“我没什么要说的了。”(A 发 FIN) 2. B 回答:“我知道了。”但 B 可能还有话要说。(B 回 ACK) 3. B 继续说完剩下的话,最后说:“我也说完了。”(B 发 FIN) 4. A 回答:“知道了。”(A 回 ACK) 这对应到 TCP 中,就是两个方向分别关闭、分别确认。 为什么通常不能把服务端发送的 ACK 和 FIN 合并起来,变成三次挥手? 图表加载中 关键原因是: 回复 ACK 和 发送 FIN 的触发时机通常不同。 当服务端收到客户端 FIN 时,内核协议栈需要回复 ACK,确认“我收到了你要关闭发送方向的请求”。此时服务端进入 ,等待本端应用处理剩余数据。 只有当服务端应用处理完毕,并调用 或 后,内核才会发送本端 FIN。 因此,“内核自动回 ACK”和“应用决定发 FIN”在时间上是解耦的,通常无法合并。只有在服务端恰好也准备立即关闭时,才可能出现 FIN+ACK 合并在一个报文段中的情况。 CLOSE\ WAIT 为什么会堆积? 是被动关闭方收到 FIN、并回复 ACK 之后进入的状态。正常情况下,它只是一个过渡状态:应用读到对端关闭发送方向的信号后,处理完剩余数据,再调用 或 ,连接就会继续进入 。 如果机器上出现大量 ,通常不是内核参数没调好,而是应用层没有及时关闭连接。常见原因包括:异常分支漏掉 、连接池归还和真实关闭逻辑不一致、业务线程被慢查询或外部调用卡住,导致代码迟迟走不到关闭 socket 的位置。 排查时可以用 先看哪些连接停在 ,再结合应用日志、线程栈和连接池监控定位具体代码路径。 的重点在“本端应用还没关闭”,所以单纯调 TCP 参数通常解决不了根因。 什么情况下会出现三次挥手? 四次挥手变成三次挥手,本质上不是少了关闭步骤,而是 第二次挥手的 ACK 和第三次挥手的 FIN 被合并到同一个报文段里 。 比较典型的条件是:被动关闭方收到 FIN 后,本端已经没有待发送的数据,应用也立刻决定关闭连接。 这里还要结合 TCP 延迟确认(Delayed ACK)来理解。延迟确认的目的,是让 ACK 有机会和窗口更新、应用响应或其他出站报文合并,减少纯 ACK 报文数量。RFC 1122 要求 ACK 不能被过度延迟,具体等待多久则由实现决定。在 Linux 等实现中,如果“确认对端 FIN”的 ACK 还在等待合并,本端应用又很快调用了 或 ,内核就可以发出一个 FIN+ACK:既确认对端的 FIN,也表达“我这边也不再发送数据了”。 抓包时看到的流程就会变成: 1. 主动关闭方发送 FIN; 2. 被动关闭方发送 FIN+ACK; 3. 主动关闭方回复 ACK,并进入 。 这里有两个细节容易混淆: 三次挥手并不违背 TCP 全双工关闭语义。两个方向仍然都要关闭,只是被动关闭方的“确认”和“关闭发送方向”刚好放进了同一个 TCP 报文段。 能不能合并,还和具体 TCP 实现、延迟确认策略、应用关闭时机有关。如果 ACK 已经被内核单独发出,后面再发送 FIN 时就无法“倒回去”合并;如果开启了类似 的快速确认策略,使 ACK 尽快独立发出,也更容易看到完整的四次挥手。 如果第二次挥手时服务端的 ACK 没有送达客户端,会怎样? 客户端发送第一次 FIN 后进入 ,并启动重传计时器。如果在超时时间内没有收到对端对 FIN 的确认 ACK,客户端会重传 FIN。 服务端如果收到重复 FIN,通常会再次发送 ACK。如果由于网络问题 ACK 一直无法送达,客户端在达到一定重试或超时阈值后,可能报错或放弃。具体行为受实现和参数影响:在 Linux 中,如果 socket 已经被应用关闭、成为 orphaned socket,后续重试更直接受 影响;普通存活连接上的 RTO 重传超时则和 有关。 为什么第四次挥手后要等待 2MSL? 第四次挥手时,主动关闭方发送给被动关闭方的最后一个 ACK 可能丢失。如果被动关闭方没有收到 ACK,就会重传 FIN。主动关闭方还在 里,就能再次回复 ACK。 如果主动关闭方发完最后一个 ACK 后立刻进入 ,当对端重传 FIN 到达时,本端可能已经没有对应连接状态,只能回复 RST,导致对端看到异常关闭或连接被重置。 图表加载中 MSL(Maximum Segment Lifetime) 是报文段在网络中的最大生存时间。2MSL 不是一次请求 响应的最大 RTT,而是一个保守等待窗口:既给最后 ACK 丢失后的 FIN 重传留出处理机会,也尽量保证旧连接中的延迟报文从网络中消失。 需要注意,RFC 里的 MSL 是协议层概念,具体系统实现可能不同。Linux 常见实现中, 保留时间通常是 60 秒,对应内核中的 常量,并不是根据实时网络环境动态计算出来的“2 倍 MSL”。还有一个常见误区: 控制的是 orphaned connection 的 超时,不是 。想缓解 带来的端口压力,优先看连接复用、端口范围、主动关闭方和 条件,而不是试图用 缩短 。 TIME\ WAIT 常见问题:为什么要等、会不会出问题、能不能复用? 这部分内容已单独成文,详见 TCP TIME\ WAIT 详解:为什么要等、会不会出问题、能不能复用?。 总结 TCP 三次握手的核心,不是“刚好发了三次包”,而是通过 、 和初始序列号同步,让客户端和服务端都确认连接具备双向通信能力。少一次握手,服务端就可能无法确认客户端是否收到了自己的 ,也更容易被网络中的旧连接请求干扰。 服务端在握手过程中会涉及半连接队列和全连接队列:前者保存还没完成握手的连接,后者保存已经建立、等待应用 的连接。排查连接建立慢、偶发超时、SYN Flood 或 accept 不及时等问题时,这两个队列是很重要的观察点。 TCP 四次挥手的核心,是全双工连接的两个发送方向要分别关闭。主动关闭方发 FIN,只表示“我不再发送数据了”,并不代表对端也立刻没有数据要发。因此,ACK 和 FIN 通常分开发送;只有被动关闭方没有待发数据、应用立刻关闭连接,并且 ACK 还可以借助延迟确认等机制等待合并时,ACK 和 FIN 才可能合并成一个 FIN+ACK,抓包上看起来就是三次挥手。 则通常提醒我们:被动关闭方的应用还没有真正关闭连接。 最后, 不是多余等待。它既给最后一个 ACK 丢失后的 FIN 重传留出处理机会,也尽量避免旧连接中的延迟报文影响后续新连接。理解这些状态和报文的触发时机,比单纯记住“几次握手、几次挥手”更有用。 参考 《计算机网络(第 7 版)》 《图解 HTTP》 TCP and UDP Tutorial:<https://www.9tut.com/tcp and udp tutorial 从一次线上问题说起,详解 TCP 半连接队列、全连接队列:<https://mp.weixin.qq.com/s/YpSlU1yaowTs pF6R43hMw RFC 9293: Transmission Control Protocol(TCP):<https://www.rfc editor.org/rfc/rfc9293 RFC 1122: Requirements for Internet Hosts Communication Layers:<https://www.rfc editor.org/rfc/rfc1122 RFC 1337: TIME WAIT Assassination Hazards in TCP:<https://www.rfc editor.org/rfc/rfc1337 tcp(7) Linux manual page:<https://www.man7.org/linux/man pages/man7/tcp.7.html Linux 内核 ip sysctl 文档:<https://www.kernel.org/doc/Documentation/networking/ip sysctl.txt Linux 内核 :<https://codebrowser.dev/linux/linux/include/net/tcp.h.html SoByte 为什么 TCP 需要 TIME\ WAIT 状态:<https://www.sobyte.net/post/2022 10/tcp time wait/ 写在最后 如果内容对你有帮助的话,欢迎顺手给 JavaGuide 点一个免费的 Star 支持一下:GitHub | Gitee。 JavaGuide 已持续维护近七年,累计 6100+ 次提交,来自 620+ 位贡献者共同完善。你的 Star、反馈和 PR,都是这个项目继续更新的动力。 如果你正在准备后端/AI 应用开发面试,也可以了解一下我的知识星球,里面包括后端和 AI 实战项目、简历优化、一对一提问和高频考点资料,已经持续维护六年。