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 表示“我确认收到了”。
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 时客户端还能响应。
- 让网络中残留的旧数据包都消失,避免影响新连接。
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 就像突然着火,所有人扔下东西就跑,管你数据有没有发完。
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 可以看到大量堆积。
3.5 核心流程总结图
下面这张图把三次握手和四次挥手放在一起,方便你对照理解:
嗯,这张图把整个流程串起来了。你抓包的时候,对照着看,哪个阶段卡住了,问题就出在哪。记住一句话:三次握手看能不能连上,四次挥手看能不能断开,RST 看是不是异常。