3. TCP三次握手与四次挥手:连接的生与死

TCP 的连接管理,说白了就两件事:怎么建立连接,怎么断开连接。我做了这么多年网络优化,见过太多因为握手挥手没搞明白而翻车的案例。今天咱们就把这两个过程彻底讲透。

3.1 三次握手:建立连接的三步舞

为什么是三次?不是两次,也不是四次?我刚开始学的时候也困惑过。其实核心就一句话:三次握手是为了确认双方的收发能力都正常。

咱们来看具体流程:

  1. 第一次握手(SYN):客户端发送 SYN 包,告诉服务器「我想跟你建立连接」。此时客户端进入 SYN_SENT 状态。
  2. 第二次握手(SYN+ACK):服务器收到后,回复 SYN+ACK,意思是「收到你的请求,我也准备好了」。服务器进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):客户端再发一个 ACK,表示「好的,我知道了」。双方进入 ESTABLISHED 状态。

嗯,这里有个细节:第三次握手是可以携带数据的。我在项目中就利用过这个特性,在握手阶段就把小数据包带过去,省了一次 RTT。不过要注意,前两次握手不能带数据,这是协议规定的。

核心要点:三次握手的本质是双方各自确认对方的接收能力和发送能力都正常。第一次确认了服务器的接收能力,第二次确认了客户端的收发能力,第三次确认了服务器的发送能力。

3.2 四次挥手:优雅地告别

断开连接比建立连接复杂一些。为什么?因为 TCP 是全双工的,双方都要关闭自己的通道。

挥手过程是这样的:

  1. 第一次挥手(FIN):主动关闭方发送 FIN,表示「我这边数据发完了」。进入 FIN_WAIT_1 状态。
  2. 第二次挥手(ACK):被动关闭方回复 ACK,表示「知道了」。进入 CLOSE_WAIT 状态。主动方收到后进入 FIN_WAIT_2。
  3. 第三次挥手(FIN):被动关闭方发送 FIN,表示「我这边也发完了」。进入 LAST_ACK 状态。
  4. 第四次挥手(ACK):主动关闭方回复 ACK,进入 TIME_WAIT 状态。等待 2MSL 后关闭。

你可能会问:为什么挥手要四次,不能像握手那样合并吗?

原因很简单:服务器收到 FIN 时,可能还有数据没发完。它得先把剩余数据发完,才能发 FIN。所以 ACK 和 FIN 是分开的。我遇到过一种情况,服务器端程序写得不好,收到 FIN 后一直不发送 FIN,导致客户端一直卡在 FIN_WAIT_2,这就是典型的 CLOSE_WAIT 泄漏问题。

3.3 状态转换图

下面这张图是我自己整理的 TCP 状态机,涵盖了握手和挥手的所有状态转换。建议你把它记下来,面试和排障都用得上。

CLOSED SYN_SENT ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT LISTEN SYN_RCVD ESTABLISHED CLOSE_WAIT LAST_ACK SYN SYN+ACK ACK FIN ACK FIN ACK 图例: 初始/关闭状态 连接建立 握手/挥手中间态 关闭相关状态 虚线箭头表示报文传输方向,实线箭头表示状态转换方向

3.4 常见问题:半连接与 TIME_WAIT

半连接(SYN Flood)

半连接指的是连接只完成了前两次握手,第三次握手迟迟不来。这种情况在服务器端表现为大量 SYN_RCVD 状态的连接。

我曾经处理过一个案例:某电商平台大促期间,服务器突然扛不住了。上去一看,ss -s 显示 SYN_RCVD 连接数飙到了几万。这就是典型的 SYN Flood 攻击。

应对方案我列一下:

  • 增大 SYN 队列:调整 net.ipv4.tcp_max_syn_backlog
  • 启用 SYN Cookie:net.ipv4.tcp_syncookies = 1,这是最常用的防御手段
  • 减少 SYN 超时时间:net.ipv4.tcp_synack_retries 默认 5 次,可以降到 2-3 次

我的建议:生产环境一定要开启 SYN Cookie。虽然会消耗一点 CPU,但能有效防止连接耗尽。我见过没开 SYN Cookie 的服务器,一个简单的 SYN Flood 就能让整个服务瘫痪。

TIME_WAIT 问题

TIME_WAIT 是四次挥手中主动关闭方最后停留的状态,持续 2MSL(默认约 60 秒)。

为什么需要 TIME_WAIT?两个原因:

  1. 保证最后一个 ACK 能到达对方。如果 ACK 丢了,对方会重发 FIN
  2. 防止旧连接的数据包干扰新连接

但 TIME_WAIT 过多也会带来问题。我记得有一次排查一个高并发短连接的服务,netstat 一看,TIME_WAIT 占了 80% 以上的连接。端口都被占满了,新连接建立不了。

解决方案:

方法 说明 适用场景
调整 MSL 减小 tcp_fin_timeout(实际是 MSL 的一半) 内网环境,延迟可控
开启端口复用 net.ipv4.tcp_tw_reuse = 1 客户端场景,需配合时间戳
快速回收 net.ipv4.tcp_tw_recycle = 1 已废弃,NAT 环境下有严重问题
长连接改造 复用连接,减少挥手次数 业务允许的情况下首选

避坑指南:我曾经在生产环境开启了 tcp_tw_recycle,结果 NAT 后面的用户大量反馈连接失败。后来查资料才知道,这个参数在 NAT 环境下会导致时间戳混乱。现在 Linux 4.12+ 已经移除了这个参数,大家千万别再用它了。

3.5 实战排查命令

最后分享几个我常用的排查命令:

# 查看连接状态统计
ss -s

# 查看具体连接,过滤 SYN_RCVD
ss -t state syn-recv

# 查看 TIME_WAIT 数量
ss -t state time-wait | wc -l

# 抓包分析握手挥手过程
tcpdump -i eth0 -nn 'tcp port 80' -w handshake.pcap

# 查看内核参数
sysctl net.ipv4.tcp_syncookies
sysctl net.ipv4.tcp_fin_timeout

嗯,TCP 的连接管理就讲到这里。记住一句话:三次握手保建立,四次挥手保关闭,TIME_WAIT 保安全。理解了这些,网络排障你就有了底气。

可可精品资料库