1981 年 9 月,Jon Postel 发布了 RFC 793,定义了传输控制协议(TCP)的核心规范。这份文档中有一张著名的 TCP 状态转换图,包含 11 个状态和错综复杂的箭头指向。四十多年后的今天,这张图依然是网络工程师面试的必考题,也是无数系统故障排查的起点。

很多人能背出"三次握手建立连接,四次挥手断开连接",但很少有人真正理解:为什么是三次不是两次?为什么是四次不是三次?TIME_WAIT 到底在等什么? 这些问题背后,是 TCP 设计者对网络不可靠性的深刻洞察,以及一系列精妙的工程权衡。

从一个基本问题开始:网络是不可靠的

TCP 的设计目标是"在不可靠的互联网络上提供可靠的端到端字节流"。这个目标看似简单,实则暗藏玄机。

网络不可靠意味着什么?

  • 数据包可能丢失:路由器缓冲区溢出、链路故障、拥塞控制丢包
  • 数据包可能重复:超时重传导致同一数据包在网络中存在多份
  • 数据包可能乱序:不同路径、不同延迟导致后发的包先到
  • 数据包可能延迟:网络拥塞可能导致数据包在网络中滞留很长时间

TCP 必须在这样恶劣的环境中保证:数据不丢、不重、不乱。三次握手和四次挥手,正是这套可靠性机制的入口和出口。

三次握手:双方都要确认"我准备好了"

标准流程

客户端                                              服务端
  |                                                   |
  |  SYN, seq=x                                      |
  |-------------------------------------------------->|  第一次握手
  |                                SYN_RCVD 状态      |
  |                                                   |
  |  SYN, seq=y, ACK, ack=x+1                         |
  |<--------------------------------------------------|  第二次握手
  |  SYN_SENT 状态                                    |
  |                                                   |
  |  ACK, ack=y+1                                     |
  |-------------------------------------------------->|  第三次握手
  |  ESTABLISHED 状态              ESTABLISHED 状态   |
  |                                                   |

每一步都在完成特定任务:

第一次握手(SYN):客户端告诉服务端"我想建立连接,我的初始序列号是 x"。此时客户端进入 SYN_SENT 状态。

第二次握手(SYN+ACK):服务端确认收到客户端的 SYN(ack=x+1),同时发送自己的 SYN(seq=y)。此时服务端进入 SYN_RCVD 状态。

第三次握手(ACK):客户端确认收到服务端的 SYN(ack=y+1)。双方都进入 ESTABLISHED 状态。

序列号:TCP 可靠性的基石

每次握手都携带序列号,这不是装饰。序列号的作用包括:

  1. 确认机制:接收方通过 ACK 告诉发送方"下一个我期望收到的序列号"
  2. 去重:如果收到重复的包,序列号可以识别并丢弃
  3. 排序:乱序到达的包可以按序列号重新排列

初始序列号(ISN)的选择非常重要。RFC 793 规定 ISN 应该基于时钟,每隔 4 微秒加 1。这样设计是为了:

  • 避免新连接使用与旧连接相同的序列号
  • 防止网络中滞留的旧数据包被误认为是新连接的数据

现代操作系统更进一步,ISN 采用加密随机数生成,这是为了防止序列号预测攻击——攻击者如果能猜到 ISN,就可以伪造 TCP 包劫持连接。

为什么不能是两次握手?

假设改成两次:

客户端                                              服务端
  |                                                   |
  |  SYN, seq=x                                       |
  |-------------------------------------------------->|  
  |                                                   |
  |  SYN, seq=y, ACK, ack=x+1                         |
  |<--------------------------------------------------|  
  |  ESTABLISHED                                      |  ESTABLISHED

看起来也能建立连接,但存在致命问题:

场景一:延迟的 SYN

假设客户端发送的 SYN 在网络中滞留,客户端超时重传后建立了新连接并完成了通信。之后,那个滞留的旧 SYN 终于到达服务端。

如果是两次握手,服务端会立即认为连接建立,开始发送数据。但客户端早已关闭了这个连接,收到数据后只会丢弃或回复 RST。

场景二:无法确认客户端的接收能力

两次握手中,服务端不知道客户端是否收到了自己的 SYN+ACK。如果这个包丢失:

  • 服务端认为连接已建立,开始发送数据
  • 客户端还在 SYN_SENT 状态,不知道服务端已经确认
  • 所有数据都会被客户端拒绝

三次握手的第三次 ACK,正是让服务端确认"客户端确实收到了我的回复"。

同时打开:边缘情况的处理

RFC 793 定义了一个有趣的场景:双方同时发起连接。

客户端 A                                            客户端 B
  |                                                   |
  |  SYN, seq=x                                       |
  |-------------------------------------------------->|  SYN_SENT
  |                                SYN, seq=y         |
  |<--------------------------------------------------|  SYN_SENT
  |                                                   |
  |  ACK, seq=x+1, ack=y+1                            |
  |-------------------------------------------------->|  SYN_RCVD
  |  SYN_RCVD       ACK, seq=y+1, ack=x+1             |
  |<--------------------------------------------------|
  |  ESTABLISHED                   ESTABLISHED        |

双方从 SYN_SENT 状态经过 SYN_RCVD 最终都到达 ESTABLISHED。这展示了 TCP 状态机的完备性——即使不常见的场景也有明确的处理路径。

四次挥手:优雅的告别

标准流程

主动关闭方(客户端)                                 被动关闭方(服务端)
  |                                                   |
  |  FIN, seq=u                                       |
  |-------------------------------------------------->|  第一次挥手
  |  FIN_WAIT_1 状态                                  |
  |                                CLOSE_WAIT 状态    |
  |  ACK, ack=u+1                                     |
  |<--------------------------------------------------|  第二次挥手
  |  FIN_WAIT_2 状态                                  |
  |                                                   |
  |  ... 服务端可能还有数据要发送 ...                   |
  |                                                   |
  |  FIN, seq=w                                       |
  |<--------------------------------------------------|  第三次挥手
  |                                LAST_ACK 状态      |
  |  ACK, ack=w+1                                     |
  |-------------------------------------------------->|  第四次挥手
  |  TIME_WAIT 状态                CLOSED 状态        |
  |                                                   |
  |  等待 2MSL                                        |
  |                                                   |
  |  CLOSED 状态                                      |

为什么是四次而不是三次?

对比三次握手:第二次握手时服务端同时发送 SYN 和 ACK(SYN+ACK)。为什么四次挥手不能合并?

关键区别:连接是全双工的

三次握手时,双方都还没开始发送数据,可以同时完成"确认对方"和"声明自己"两件事。

但四次挥手时,被动关闭方可能还有数据要发送。收到 FIN 只意味着"对方发完了",不代表"我也发完了"。

客户端:我没有数据要发了(FIN)
服务端:收到了(ACK)

... 服务端继续发送剩余数据 ...

服务端:我也没有数据要发了(FIN)
客户端:收到了(ACK)

这就是"半关闭"状态——一方关闭了发送通道,但接收通道仍然开放。

TCP 状态机全景

TCP 共有 11 个状态:

状态 含义 谁会进入
CLOSED 初始状态 双方
LISTEN 等待连接 服务端
SYN_SENT 已发送 SYN,等待 ACK 客户端
SYN_RCVD 已收到 SYN,已发送 SYN+ACK 服务端
ESTABLISHED 连接建立 双方
FIN_WAIT_1 已发送 FIN,等待 ACK 主动关闭方
FIN_WAIT_2 已收到 ACK,等待对方 FIN 主动关闭方
CLOSE_WAIT 已收到 FIN,等待应用关闭 被动关闭方
CLOSING 同时关闭场景 双方
LAST_ACK 已发送 FIN,等待最终 ACK 被动关闭方
TIME_WAIT 等待 2MSL 主动关闭方

状态机的设计原则:每个状态都有明确的进入条件和退出条件,不会出现"卡住"的情况(除非应用程序本身有问题)。

TIME_WAIT:最被误解的状态

为什么需要 TIME_WAIT?

主动关闭方发送最后一个 ACK 后,不直接进入 CLOSED,而是进入 TIME_WAIT 状态等待 2MSL。为什么?

原因一:确保对方收到最后的 ACK

如果最后的 ACK 丢失,被动关闭方会重发 FIN。如果主动关闭方已经进入 CLOSED,收到重发的 FIN 会回复 RST,导致被动关闭方异常关闭。

TIME_WAIT 状态下,收到重发的 FIN 会重新发送 ACK,重置 2MSL 计时器。

原因二:让旧连接的数据包消失

网络中可能存在延迟的数据包。如果立即复用相同的四元组(源 IP、源端口、目的 IP、目的端口)建立新连接,旧数据包可能被新连接误收。

MSL(Maximum Segment Lifetime)是数据包在网络中的最大生存时间。RFC 793 建议 MSL 为 2 分钟,但 Linux 默认使用 30 秒,实际 TIME_WAIT 等待时间是 60 秒。

等待 2MSL 的逻辑:

  • 1 个 MSL 让旧数据包消失
  • 1 个 MSL 给最后的 ACK 足够时间到达对方

TIME_WAIT 过多的问题

高并发短连接场景下,TIME_WAIT 会快速积累。Linux 默认可用端口范围是 32768-60999,约 28000 个端口。如果每秒建立 500 个短连接,60 秒后就会有 30000 个连接处于 TIME_WAIT,端口耗尽。

解决方案:

方案一:设置 SO_REUSEADDR

允许新连接复用 TIME_WAIT 状态的端口。前提是序列号不冲突——现代 TCP 使用时间戳选项来保证这一点。

方案二:调整 tcp_tw_reuse

Linux 的 net.ipv4.tcp_tw_reuse = 1 允许在 TIME_WAIT 超过 1 秒后复用连接。比 SO_REUSEADDR 更安全,因为有额外的时间戳验证。

方案三:修改端口范围

扩大 net.ipv4.ip_local_port_range 范围,增加可用端口数量。

不推荐的方案:tcp_tw_recycle

这个选项曾在 Linux 中存在,用于快速回收 TIME_WAIT 连接。但因为会导致 NAT 环境下的连接问题,已在 Linux 4.12 中移除。

Cloudflare 发现的 CLOSE_WAIT 陷阱

2016 年,Cloudflare 工程师发现一个奇怪的问题:偶尔连接 localhost 会超时。调试后发现,原因竟然是大量的 CLOSE_WAIT 状态 socket。

问题链条:

  1. 服务端程序忘记 close(),导致 socket 停留在 CLOSE_WAIT
  2. 客户端关闭连接后,socket 进入 FIN_WAIT_2,然后超时消失
  3. 当新的连接恰好复用这个端口时,内核发现四元组冲突
  4. 服务端的 CLOSE_WAIT socket 还占着,无法响应新的 SYN
  5. 连接超时

这揭示了一个违背 TCP 规范的事实:Linux 内核会自动清理 FIN_WAIT_2 状态,而不是像 RFC 793 规定的那样无限等待对方关闭

Linux 文档中明确写道:

“This is strictly a violation of the TCP specification, but required to prevent denial-of-service attacks.”

这是工程实践对理论规范的妥协。

SYN Flood:利用握手机制的攻击

攻击原理

三次握手的第二次,服务端收到 SYN 后会进入 SYN_RCVD 状态,等待客户端的 ACK。此时服务端已经分配了 TCB(Transmission Control Block),占用了资源。

如果攻击者伪造大量不存在的 IP 地址发送 SYN 包:

  1. 服务端回复 SYN+ACK,进入 SYN_RCVD
  2. 因为 IP 不存在,永远不会收到 ACK
  3. 服务端不断重发 SYN+ACK 直到超时
  4. 半连接队列被占满,正常请求被拒绝

防御措施

SYN Cookies

不立即分配资源,而是把连接信息编码在 ISN 中。收到 ACK 时,验证序列号来重建连接信息。

ISN = hash(sip, sport, dip, dport, secret) + MSS_index

这样,只有收到正确的 ACK 才会分配资源,攻击者无法完成这一步。

调整内核参数

  • net.ipv4.tcp_syncookies = 1:启用 SYN Cookies
  • net.ipv4.tcp_synack_retries:减少 SYN+ACK 重试次数
  • net.ipv4.tcp_max_syn_backlog:增加半连接队列长度

同时关闭:完整的四次挥手

当双方同时发起关闭时:

客户端 A                                            客户端 B
  |                                                   |
  |  FIN, seq=x                                       |
  |-------------------------------------------------->|  FIN_WAIT_1
  |                                FIN, seq=y         |
  |<--------------------------------------------------|  FIN_WAIT_1
  |  CLOSING                                          |
  |                                                   |
  |  ACK, ack=y+1                                     |
  |-------------------------------------------------->|  CLOSING
  |  ACK, ack=x+1                                     |
  |<--------------------------------------------------|
  |  TIME_WAIT                     TIME_WAIT          |

双方都经历 FIN_WAIT_1 -> CLOSING -> TIME_WAIT -> CLOSED。每个状态都有明确的处理逻辑,不会出现混乱。

为什么 TCP 如此复杂?

回顾 RFC 793 的设计哲学:

“TCP is designed to work in a very general environment of interconnected networks.”

网络不可靠性是 TCP 复杂的根源:

  • 丢包 → 需要重传机制
  • 延迟 → 需要超时和 TIME_WAIT
  • 乱序 → 需要序列号
  • 重复 → 需要去重逻辑
  • 网络变化 → 需要连接迁移(后来的改进)

三次握手和四次挥手,本质上是双方在不可靠网络上进行的"可靠协商"。每一次交互都在确认对方的状态,每一个状态都是为了应对某种异常情况。

理解这一点,TCP 状态机就不再是需要死记硬背的面试题,而是一套精妙的工程解决方案。


参考

  1. Postel, J. (1981). RFC 793: Transmission Control Protocol. IETF.
  2. Eddy, W. (2022). RFC 9293: Transmission Control Protocol (TCP). IETF.
  3. Stevens, W. R. (1994). TCP/IP Illustrated, Volume 1: The Protocols. Addison-Wesley.
  4. Kurose, J. F., & Ross, K. W. (2017). Computer Networking: A Top-Down Approach. Pearson.
  5. GeeksforGeeks. “TCP 3-Way Handshake Process.” https://www.geeksforgeeks.org/computer-networks/tcp-3-way-handshake-process/
  6. SoByte. “Why does the TCP protocol have a TIME_WAIT state?” https://www.sobyte.net/post/2021-12/whys-the-design-tcp-time-wait/
  7. Cloudflare Blog. “This is strictly a violation of the TCP specification.” https://blog.cloudflare.com/this-is-strictly-a-violation-of-the-tcp-specification/
  8. Wikipedia. “TCP sequence prediction attack.” https://en.wikipedia.org/wiki/TCP_sequence_prediction_attack
  9. Northwestern University. “TCP/IP State Transition Diagram (RFC793).” https://users.cs.northwestern.edu/~agupta/cs340/project2/TCPIP_State_Transition_Diagram.pdf
  10. Red Hat Documentation. “TCP States - Explained.” https://docs.redhat.com/
  11. 知乎. “TCP三次握手、四次挥手以及TIME_WAIT详解.” https://zhuanlan.zhihu.com/p/499127830
  12. 博客园. “TCP 三次握手和四次挥手图解(有限状态机).” https://www.cnblogs.com/huansky/p/13951567.html
  13. IETF. “TCP SYN Flooding Attacks and Common Mitigations.” https://datatracker.ietf.org/doc/draft-ietf-tcpm-syn-flood/
  14. Radware. “What Are TCP SYN Flood DDOS Attacks & 6 Ways to Stop Them.” https://www.radware.com/security/ddos-knowledge-center/ddospedia/tcp-flood/
  15. Server Fault. “Why are connections in FIN_WAIT2 state not closed by the Linux kernel?” https://serverfault.com/questions/738300/
  16. Stack Overflow. “Why TIME_WAIT state need to be 2MSL long?” https://stackoverflow.com/questions/25338862/
  17. Medium. “TCP 3-Way & 4-Way Handshake.” https://medium.com/@iclalaca.ai/tcp-3-way-4-way-handshake-the-logic-of-establishing-and-terminating-a-connection-bed2a74a678d
  18. Networkwalks. “TCP 3-way Handshake Process.” https://networkwalks.com/tcp-3-way-handshake-process/
  19. The Hacker Spotlight. “TCP Three-Way Handshake Attacks.” https://www.thehackersspotlight.com/post/tcp-three-way-handshake-attacks-how-hackers-exploit-the-foundation-of-internet-communication
  20. SegmentFault. “TCP’s three-way handshake and four waved hands.” https://segmentfault.com/a/1190000040054239/en