①为什么需要“挥手”:连接是全双工的
建立连接时,服务端的 SYN(我要连你)和 ACK(我收到你的请求)可以合并在同一个报文里发出,所以三次搞定。但关闭时,一方说“我没数据要发了(FIN)”,另一方只能先回一个 ACK——因为它可能还有数据没发完,不能立刻也发 FIN。于是 ACK 和 FIN 得分开两次发,总共四次。
②四次挥手全流程(时序图)
假设客户端主动关闭(实际上服务端也可以主动 close)。每一步的报文与状态如下:
| 步骤 | 方向 | 报文 | 发送方进入 | 接收方进入 |
|---|---|---|---|---|
| ① | C → S | FIN=1, seq=u | FIN_WAIT_1 | CLOSE_WAIT |
| ② | S → C | ACK=1, ack=u+1 | — | FIN_WAIT_2(接收方) |
| ③ | S → C | FIN=1, seq=v | LAST_ACK | — |
| ④ | C → S | ACK=1, ack=v+1 | TIME_WAIT | CLOSED |
③状态机变迁:关键状态都什么意思
FIN_WAIT_1 / FIN_WAIT_2
主动方发出 FIN 后等对方 ACK;收到后进入 FIN_WAIT_2,此时半关闭:还能收对方数据,但不能再发。
CLOSE_WAIT
被动方收到 FIN 后进入,含义是“对方要关了,等我自己的应用层 close()”。停留过久=程序忘了关 socket。
LAST_ACK
被动方发完自己的 FIN,等最后一个 ACK。收到即 CLOSED。
TIME_WAIT
主动方发出最后 ACK 后进入,要等 2MSL 才真正释放。见下一节。
④TIME_WAIT:为什么要等 2MSL
MSL(Maximum Segment Lifetime)是报文在网络中的最大生存时间,Linux 默认 30 秒,所以 TIME_WAIT 默认持续 60 秒(2MSL)。等这么久有两个硬核理由:
理由一:可靠地终止连接
最后那个 ACK 可能丢失。如果丢了,被动方会重发 FIN。主动方若已直接关闭,就只能回 RST,对方会认为出错。留着 TIME_WAIT 才能重发 ACK,让连接正常收尾。
理由二:消散旧连接的迷途报文
若不等待就复用同一四元组(源IP:源端口, 目的IP:目的端口)建新连接,上一次连接迟到的旧报文可能被新连接误收。等 2MSL 保证旧报文在网络中自然消亡。
TIME_WAIT 过多有什么危害
- 占用端口资源:每个 TIME_WAIT 占一个本地端口,客户端高并发短连接时可能端口耗尽(尤其压测机、爬虫、Nginx 反代上游)。
- 占用内存与连接表项:内核要维护该连接的控制块,量大了有开销。
怎么优化(面试答这几个)
| 手段 | 做法 | 说明 |
|---|---|---|
| 复用连接 | 开启 Keep-Alive / 连接池 | 首选:从源头减少主动关闭 |
| 让对方先关 | 业务设计上让客户端主动断开 | 把 TIME_WAIT 转移给客户端 |
| 端口复用 | net.ipv4.tcp_tw_reuse = 1 | 允许把 TIME_WAIT 连接用于新出站连接(需时间戳支持) |
| 放宽端口范围 | ip_local_port_range | 从默认约 2.8 万扩到 6 万 |
| 缩短等待 | 调小 tcp_fin_timeout | 治标不治本,需谨慎 |
tcp_tw_recycle 已在 Linux 4.12+ 被移除——它会导致 NAT 环境下大量连接被拒。别再把这个当答案说,会被认为知识过时。⑤CLOSE_WAIT 堆积:几乎一定是代码 bug
常见原因:
- HTTP 客户端拿到响应后没有关闭连接 / 没有放回连接池(如忘记
resp.Body.Close())。 - 异常处理分支里漏了 close,正常路径关了、异常路径没关。
- 使用了连接池但连接未归还,池子泄漏。
- 线程池 / 协程阻塞,导致 close 逻辑根本没执行到。
# 排查命令:统计各状态连接数
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
# 看哪些进程持有大量 CLOSE_WAIT
ss -tanp | grep CLOSE-WAIT
# 典型输出(危险信号)
# CLOSE_WAIT 8234
# ESTABLISHED 120
# TIME_WAIT 45
修复方向:在 finally / defer 里保证资源释放;使用带超时和最大空闲数的连接池;给 HTTP client 设置合理超时。