3、TCP三次握手与四次挥手:SYN、SYN-ACK、ACK的交互过程,FIN与RST的区别,常见异常场景分析

TCP 的连接管理,说白了就是两台机器商量好怎么收发数据,以及怎么体面地结束。我刚开始抓包那会儿,看着一堆 SYN、ACK 的包,感觉跟看天书似的。后来踩的坑多了,才明白这些标志位背后藏着多少信息。

今天咱们就把三次握手和四次挥手彻底聊透。你想想看,一个连接从生到死,中间会经历什么?

3.1 三次握手:建立连接的三次对话

三次握手的本质,是双方确认彼此的收发能力都正常。我习惯把它比作打电话:

  • 第一次(SYN):客户端说“喂,能听到吗?”
  • 第二次(SYN-ACK):服务端回“听到了,你能听到我吗?”
  • 第三次(ACK):客户端说“我也听到了,咱们开始聊吧”

具体到报文上,流程是这样的:

客户端                             服务端
  |                                   |
  |  -------- SYN (seq=x) -------->   |
  |                                   |
  |  <---- SYN-ACK (seq=y, ack=x+1) - |
  |                                   |
  |  -------- ACK (seq=x+1, ack=y+1) -> |
  |                                   |
  |  ========= 连接建立 ===========   |

这里有几个关键点,我在项目中经常遇到有人搞混:

  • seq(序列号):每个字节的编号,用来保证数据有序。第一次握手客户端随机生成一个初始序列号 x。
  • ack(确认号):告诉对方“我期待你下一个字节的序列号是多少”。比如 ack=x+1,意思就是“我已经收到 x 了,下一个请发 x+1”。
  • SYN 和 ACK 标志位:SYN 表示“我要开始同步了”,ACK 表示“我确认收到了”。
我的小技巧:抓包时如果看到三次握手不完整,比如只有 SYN 没有 SYN-ACK,或者只有前两次没有第三次 ACK,那基本就是网络不通或者服务端没起来。我曾经排查过一个线上问题,客户端一直发 SYN,服务端就是不回,最后发现是防火墙把端口给拦了。

3.2 四次挥手:体面地结束连接

四次挥手比握手复杂一点,因为 TCP 是双工的,每一路都要单独关闭。说白了就是:

  • 第一次(FIN):客户端说“我这边没数据要发了,准备关了”。
  • 第二次(ACK):服务端回“知道了,但我这边可能还有数据要发”。
  • 第三次(FIN):服务端说“我这边也发完了,可以关了”。
  • 第四次(ACK):客户端回“收到,关闭吧”。

报文交互长这样:

客户端                             服务端
  |                                   |
  |  -------- FIN (seq=u) -------->   |
  |                                   |
  |  <------- ACK (ack=u+1) --------- |
  |                                   |
  |  <------- FIN (seq=v) ----------- |
  |                                   |
  |  -------- ACK (ack=v+1) --------> |
  |                                   |
  |  ========= 连接关闭 ===========   |

这里有个容易踩坑的地方:TIME_WAIT 状态。客户端发送最后一次 ACK 后,不会立即关闭,而是进入 TIME_WAIT 状态,等待 2MSL(最大报文生存时间)后才真正关闭。为什么要等?

  • 防止最后一次 ACK 丢失,服务端重发 FIN 时客户端还能响应。
  • 让网络中残留的旧数据包都消失,避免影响新连接。
我曾经踩过的坑:高并发场景下,服务器上 TIME_WAIT 连接堆积太多,导致端口被占满,新连接无法建立。后来通过调整内核参数 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle 才解决。不过要注意,tcp_tw_recycle 在 NAT 环境下会出问题,别乱开。

3.3 FIN 与 RST 的区别

FIN 和 RST 都是用来关闭连接的,但方式完全不同:

特性 FIN(正常关闭) RST(异常复位)
含义 优雅地结束,把缓冲区数据发完再关 立即终止,丢弃所有未处理数据
触发场景 正常调用 close() 或 shutdown() 程序崩溃、端口不可达、收到非法报文
对端反应 进入 CLOSE_WAIT,等待本端关闭 直接断开,可能收到 ECONNRESET 错误
抓包特征 FIN 标志位置 1,序列号正常递增 RST 标志位置 1,序列号可能重置

我举个例子你就明白了。FIN 就像你跟同事说“我先下班了,明天见”,大家把手里的事收尾完再走。RST 就像突然着火,所有人扔下东西就跑,管你数据有没有发完。

抓包实战经验:如果你在抓包中看到大量 RST 包,别慌。先看是谁发的:客户端发 RST,通常是程序主动关闭或超时;服务端发 RST,可能是端口没监听或者防火墙拦截。我曾经遇到一个案例,客户端一直发 RST,查了半天发现是 keepalive 超时设置太短,连接被内核强行复位了。

3.4 常见异常场景分析

实际抓包中,握手和挥手阶段最容易出问题。我总结了几个高频异常:

3.4.1 半连接队列溢出(SYN Flood)

攻击者伪造大量 SYN 包,但不完成第三次握手。服务端维护的半连接队列很快被占满,导致正常用户无法建立连接。

抓包特征:服务端收到大量 SYN,但看不到对应的 ACK。服务端会重传 SYN-ACK,直到超时。

应对方法:

  • 开启 SYN Cookie(net.ipv4.tcp_syncookies = 1)
  • 增大半连接队列大小(net.ipv4.tcp_max_syn_backlog)
  • 减少 SYN-ACK 重传次数(net.ipv4.tcp_synack_retries)

3.4.2 连接重置(RST 风暴)

客户端或服务端突然发送 RST,导致连接中断。常见原因:

  • 程序崩溃或主动调用 close() 时设置了 SO_LINGER
  • 收到不属于当前连接的报文(序列号不对)
  • 防火墙或中间设备主动拦截

抓包特征:在正常数据交互中,突然出现一个 RST 包,后续所有包都断了。

3.4.3 TIME_WAIT 堆积

高并发短连接场景下,客户端或服务端出现大量 TIME_WAIT 状态的连接。每个 TIME_WAIT 会占用一个本地端口,端口耗尽后新连接无法建立。

抓包特征:客户端尝试建立新连接时,系统返回 EADDRNOTAVAIL 错误。用 netstat -an | grep TIME_WAIT | wc -l 可以看到数量惊人。

优化思路:

  • 开启 net.ipv4.tcp_tw_reuse(允许重用 TIME_WAIT 连接)
  • 调整 net.ipv4.tcp_fin_timeout(减少 TIME_WAIT 等待时间)
  • 改用长连接,减少短连接数量

3.4.4 挥手阶段卡在 CLOSE_WAIT

服务端收到客户端的 FIN 后,进入 CLOSE_WAIT 状态。正常情况下服务端应该尽快发送自己的 FIN,但如果程序忘记调用 close(),就会一直卡在这里。

抓包特征:客户端已经发完 FIN 并收到 ACK,但服务端迟迟不发 FIN。用 netstat -an | grep CLOSE_WAIT 可以看到大量堆积。

排查思路:CLOSE_WAIT 堆积通常是代码 bug,比如 socket 没有正确关闭,或者业务逻辑阻塞导致 close() 没执行。我曾经帮一个团队排查过,发现是某个 HTTP 请求处理完后,response 流没有关闭,导致连接一直挂着。

3.5 核心流程总结图

下面这张图把三次握手和四次挥手放在一起,方便你对照理解:

TCP 三次握手与四次挥手流程图 TCP 连接建立与关闭流程 客户端 服务端 ① SYN (seq=x) ② SYN-ACK (seq=y, ack=x+1) ③ ACK (seq=x+1, ack=y+1) 连接建立 ④ FIN (seq=u) ⑤ ACK (ack=u+1) ⑥ FIN (seq=v) ⑦ ACK (ack=v+1) 连接关闭 蓝色箭头:客户端发起 | 绿色箭头:服务端发起 | 虚线区域:网络传输

嗯,这张图把整个流程串起来了。你抓包的时候,对照着看,哪个阶段卡住了,问题就出在哪。记住一句话:三次握手看能不能连上,四次挥手看能不能断开,RST 看是不是异常。