TCP 三次握手与四次挥手:连接的建立与终止
本文是《计算机网络学习笔记》系列的第二篇。TCP 是互联网最核心的传输协议,它的可靠性建立在一套精妙的连接管理机制之上——三次握手建立连接,四次挥手终止连接。看似简单的"多握几次手"背后,其实有很多值得深究的设计哲学。
前置知识
本文会频繁用到 TCP 报文头中的几个字段:序列号(seq)、确认号(ack),以及 SYN、FIN、ACK、RST 这几个标志位。如果还不熟悉,建议先阅读 《TCP 协议格式》,再来看本文会顺畅很多。
这里只补充一个最关键的细节,贯穿全文:
SYN 和 FIN 报文各自消耗 1 个序列号,纯 ACK 报文不消耗序列号。
这条规则是理解握手和挥手中序号变化的基础。
一、三次握手:建立连接
完整流程

三步详解
第一步 — 客户端发 SYN
客户端将 SYN 标志位置为 1,并随机生成一个初始序列号(ISN,Initial Sequence Number),记为 client_isn。这个数字是随机的,而非从 0 开始——原因在下面"选择随机 ISN"一节解释。
此报文不携带任何数据,但 SYN 本身消耗 1 个序列号。
第二步 — 服务端发 SYN+ACK
服务端收到 SYN 后,立即为这条连接分配接收缓冲区和各类变量(这意味着服务器资源的消耗从第二步就开始了)。回复的报文同时置位 SYN 和 ACK:
ack = client_isn + 1:确认收到客户端的 SYN,告知"下一个期望从 client_isn+1 开始"seq = server_isn:服务端自己随机生成的初始序列号
此报文同样不携带数据。
第三步 — 客户端发 ACK
客户端确认收到服务端的 SYN:
ack = server_isn + 1:确认服务端的 SYN 已收到
注意:第三个报文段允许携带数据,比如 HTTP 请求可以直接附在里面发出,节省一个完整的往返时间(RTT)。这也是 TCP Fast Open(TFO) 等优化技术的思路来源。
为什么是三次,而不是两次?
这是面试中出现频率极高的问题,答案的关键词是:防止历史连接的干扰。
想象这个场景:客户端因为网络抖动,发出去一个 SYN 包在网络里"游荡"。客户端等不到回复,超时后重发,双方完成了正常通信,连接也已经关闭了。
过了一会儿,那个旧的 SYN 包姗姗来迟,终于到达了服务端。
如果只要两次握手:服务端收到 SYN,直接回 SYN+ACK,然后认为连接建立完成,开始分配资源、傻等客户端发数据——但客户端根本不知道有这个连接,什么也不会发。服务端只能白白消耗资源直到超时。
如果是三次握手:服务端回 SYN+ACK 后,还需要等到客户端的第三个 ACK 才算正式建立。客户端收到这个莫名其妙的 SYN+ACK,发现自己没有对应的连接记录,直接回一个 RST 包,服务端收到后干净地释放资源。
三次握手的本质:给客户端一次"鉴别并拒绝历史连接"的机会。
附加收益:三次握手完成后,双方都确认了彼此的初始序列号(client_isn 和 server_isn),为后续的有序可靠传输奠定了基础。
为什么 ISN 要随机选取,而不从 0 开始?
主要有两个原因:
- 隔离历史连接的残留数据:如果每次连接都从 0 开始,上一条连接滞留在网络中的老数据包(序号空间与新连接一致)可能被错误地当作新数据接受,造成数据污染。随机 ISN 使新旧连接的序号空间大概率不重叠。
- 安全性:可预测的序列号(如总是从 0 开始)会让攻击者轻易伪造合法的 TCP 报文,注入恶意数据。随机 ISN 大大提高了伪造门槛。
握手中途丢包了怎么办?
TCP 的设计极其健壮——三次握手中任意一个包丢失,都可以自动恢复。
情况一:SYN 丢了(第一个包)
- 服务端不知道有人要连,静静监听,无事发生。
- 客户端:触发超时重传,采用指数退避策略依次等待(1s、2s、4s、8s、16s、32s……),重试次数达到上限(Linux 默认
tcp_syn_retries = 6,累计约 63 秒)后,返回Connection timed out错误。
情况二:SYN+ACK 丢了(第二个包)
- 客户端:没收到回复,以为自己的 SYN 丢了,触发超时,重发 SYN。
- 服务端:没收到第三个 ACK,以为自己的 SYN+ACK 丢了,也触发超时,重发 SYN+ACK。
两边同时在重试,只要有任意一次重传顺利到达,握手就可以继续。
情况三:ACK 丢了(第三个包)
这是最有趣的情况:
- 客户端已经认为连接建立完毕(进入
ESTABLISHED),可能紧接着就发送数据。 - 服务端还停留在
SYN_RCVD状态,等着 ACK。
此时有两种自愈路径:
- 客户端随即发送数据:服务端收到数据,发现自己虽然没收到 ACK,但数据已经出现了,说明客户端认为连接已通,于是推进到
ESTABLISHED状态,正常处理数据。 - 双方暂时沉默:服务端超时,重发 SYN+ACK,客户端收到后再次回 ACK,流程继续。
SYN Flood 攻击
三次握手有一个经典的安全漏洞利用——SYN Flood 攻击。
攻击原理:攻击者向服务端疯狂发送大量 SYN 包,但故意不发第三个 ACK(可以用 Raw Socket 或 DPDK 直接构造报文绕过操作系统的 TCP 协议栈),让服务端的 SYN_RCVD 半连接队列被大量连接占满。由于在第二步服务端就已经分配了资源,堆积的半连接会耗尽服务器内存,导致无法响应正常请求。
常见的防御手段:
- SYN Cookie:服务端在第二步不立即分配资源,而是将连接信息编码进 SYN+ACK 的序列号中(即 Cookie),只有收到合法的第三个 ACK 后才真正建立连接,分配资源。
- 限制单 IP 的 SYN 速率,配合防火墙过滤。
二、四次挥手:终止连接
TCP 是全双工通信——连接的两个方向(A→B 和 B→A)是相互独立的数据流。关闭连接时,每个方向需要独立地"说再见",因此一共需要四次报文交换。
完整流程

四步详解
第一步 — 主动方发 FIN
主动关闭方(客户端或服务端均可率先发起)发送 FIN 报文,表示"我这边没有数据要发了"。FIN 消耗 1 个序列号,进入 FIN_WAIT_1 状态。
第二步 — 被动方发 ACK
被动方收到 FIN,立即回复 ACK(ack = m+1),进入 CLOSE_WAIT 状态。此时连接进入半关闭(Half-Close)状态:
- 主动方 → 被动方:已关闭,主动方不再发送数据;
- 被动方 → 主动方:仍然开放,被动方可以继续把剩余数据发完。
主动方收到 ACK 后进入 FIN_WAIT_2,等待被动方的 FIN。
第三步 — 被动方发 FIN
被动方将剩余数据全部发送完毕后,发送自己的 FIN 报文,表示"我这边也没数据了",进入 LAST_ACK 状态。
第四步 — 主动方发最后一个 ACK
主动方回复 ACK(ack = n+1),然后进入 TIME_WAIT 状态,等待 2MSL 时间后,正式关闭连接。
为什么挥手需要四次,而握手只需要三次?
握手时,服务端可以把 SYN 和 ACK 合并为一个报文(第二步),所以是三次。
挥手不行。被动方收到 FIN 后,必须立刻回 ACK(否则主动方会超时重传),但此时被动方的应用层可能还有数据没发完,FIN 不能立刻跟上。ACK 和 FIN 在时间上是分离的,因此必须分两个独立的报文发出——这是挥手从三步变为四步的根本原因。
一句话总结:握手时两件事(同步序列号 + 确认)可以同时做;挥手时两件事(确认对方关闭 + 自己关闭)必须分开做,因为中间可能还有数据在传。
TIME_WAIT:为什么要等 2MSL?
主动方发出最后一个 ACK 之后,不会立即关闭,而是进入 TIME_WAIT 状态,等待 2MSL(Maximum Segment Lifetime,报文最大存活时间) 后才真正关闭。
Linux 默认 MSL = 60 秒,因此 TIME_WAIT 约持续 2 分钟。
这个"看似多余"的等待,解决了两个真实问题:
问题一:最后一个 ACK 可能丢失
如果第四步的 ACK 在网络中丢失,被动方会超时,重新发送第三步的 FIN。
- 如果主动方已经彻底关闭:当被动方重发的 FIN 到达时,主动方的端口已经不存在,操作系统会回复 RST,被动方被迫异常关闭(而非干净地结束)。
- 如果主动方处于 TIME_WAIT:收到重发的 FIN 后,主动方重新发送 ACK,等待时间重置,直到确认对方已经收到。
问题二:旧连接的游荡报文干扰新连接
网络中可能存在本次连接的滞留报文(被网络延迟卡住的老数据包)。等待 2MSL,确保这些报文全部过期失效,才允许用相同四元组建立新连接,避免新连接收到"来自过去"的脏数据。
工程实践:TIME_WAIT 是高并发服务器的常见陷阱。如果服务端主动关闭大量短连接(如 HTTP/1.0 短连接场景),会产生海量 TIME_WAIT 状态的连接,迅速占满本地端口资源(ephemeral port 范围约 28000 个端口),导致后续连接请求因"无可用端口"而失败。
常见解法:
SO_REUSEADDR:允许处于 TIME_WAIT 的端口被新连接复用;tcp_tw_reuse = 1:在安全的条件下复用 TIME_WAIT 连接;- 使用长连接(Keep-Alive)或连接池,从根本上减少连接的频繁创建与关闭。
能不能三次挥手?
理论上,如果被动方的应用层收到 FIN 后,立即也确认没有数据要发了,可以把第二步的 ACK 和第三步的 FIN 合并成一个 FIN+ACK 报文,这样就变成了三次挥手。
某些场景下这确实会发生,但这只是实现上的优化,不改变状态机的本质——两个方向的关闭仍然需要分别确认。
三、TCP 状态机全景
三次握手和四次挥手的过程,本质上是 TCP 连接在不同状态之间的迁移。

四、连接保活与心跳包
连接建立后,如果长时间没有数据传输,连接是否还"活着"?
不要依赖 TCP KeepAlive
TCP 协议层内置了 KeepAlive 机制,但在实际工程中不推荐作为主要的连接保活手段。
Linux 默认的 KeepAlive 探测间隔为 7200 秒(2 小时)。然而,客户端和服务端之间的 NAT 网关通常会维护一张连接状态表,一条连接超过 5 分钟(甚至更短)没有任何数据通过,就会被从表中删除——这叫做 NAT 超时。
结果:KeepAlive 2 小时才探测一次,而 NAT 早就把连接条目删掉了。此后再发包,路由器找不到对应条目,直接返回 RST,连接被强制断开。
虽然可以在代码中把 KeepAlive 间隔调短,但由于这是系统级内核参数,在不同环境、不同操作系统上行为各异,不建议作为业务层可靠性的保障。
自己实现应用层心跳包
正确的做法是在应用协议层实现心跳:
struct HeartbeatPacket {
uint8_t type; // 消息类型:PING 或 PONG
uint32_t seq; // 序列号:防止乱序,用于匹配请求和响应
uint64_t timestamp; // 发送时间戳:PONG 时原样回填,发送方算出 RTT
uint32_t load; // 可选:当前负载(如 CPU 使用率),顺带汇报状态
};这样做的好处:
- 完全掌控探测频率,不受操作系统参数限制;
- 可以精确测量端到端 RTT;
- 可以扩展携带业务信息(如对端负载、版本号),一举两得。
总结
| 三次握手 | 四次挥手 | |
|---|---|---|
| 目的 | 建立连接,同步双方初始序列号 | 安全终止连接,等双方数据都发完 |
| 次数 | 3 次 | 4 次(全双工,两个方向独立关闭) |
| 核心状态 | SYN_SENT → SYN_RCVD → ESTABLISHED | FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED |
| 特殊等待 | 无 | TIME_WAIT,等待 2MSL(Linux 约 2 分钟) |
| 丢包处理 | 超时重传 + 指数退避 | 超时重传(被动方重发 FIN) |
| 安全威胁 | SYN Flood,SYN Cookie 防御 | 无特殊威胁 |
TCP 的连接管理设计精妙之处在于:每一个"多余"的步骤,背后都有充分的工程理由。三次握手不是"多此一举",TIME_WAIT 也不是"浪费时间"——它们都是在不可靠的网络环境下,构建可靠通信所必须付出的代价。
*参考资料:《计算机网络:自顶向下方法》