TCP 四次挥手:TIME_WAIT 为什么等 2MSL?
一句话结论(30s)
TCP 挥手要四次而不是三次,是因为 TCP 是全双工的:客户端发 FIN 只表示”我不再发数据了”,但服务端可能还有数据要发。所以服务端先由内核立即回一个 ACK(确认收到你的 FIN),等自己的数据发完、应用调用 close() 后再发自己的 FIN。而主动关闭方最后要等 2MSL,是为了① 确保最后一个 ACK 被对端收到(丢了能重传),② 让旧连接的所有残留报文在网络中彻底死透,避免污染相同四元组的新连接。
核心原理(2min)
- 四次挥手 = 分别关闭两个方向:主动方 FIN + 对端 ACK(关”自己→对端”),对端 FIN + 主动方 ACK(关”对端→自己”),每个方向各一对”请求-确认”。
- 中间两次不能合并:ACK 是内核收到 FIN 后立即回复的(协议栈行为),FIN 却是应用层调用 close() 才触发的,二者触发者与时间点不同,之间可能隔着服务端继续发数据的漫长间隙。
- TIME_WAIT 等 2MSL 的两个目的:① 保证最后的 ACK 到达——若 ACK 丢失,对端会重传 FIN,TIME_WAIT 期间可再回 ACK;② 让旧连接的所有报文(含其响应)超时消亡,防止相同四元组的新连接收到”前世”数据。
先停一秒,问自己:为什么建连要三次,断开却要四次?两者不都是”双方确认”吗? 区别在于”ACK 和 SYN/FIN 是不是同一个人、同一个时刻发出来的”。建连时服务端的 SYN 和 ACK 由内核同时产生,可以合并;断开时 ACK 是内核收到 FIN 立刻回的,FIN 却是应用层调 close() 才发的,中间可能隔着服务端继续发数据的漫长间隙。这个”时间差”就是四次挥手多出来那一脚的根因。带着这个直觉往下读。
底层深入(5-10min)
完整的挥手过程
客户端 (主动关闭方) 服务端 (被动关闭方)
| |
|──── FIN (seq=u) ────────────→| ① 客户端: 我发完了,准备关
| FIN_WAIT_1 | CLOSE_WAIT
|←─── ACK (ack=u+1) ──────────┤ ② 服务端: 知道了(内核自动回复)
| FIN_WAIT_2 |
|←─── FIN (seq=v) ────────────┤ ③ 服务端: 我也发完了
| (收到) TIME_WAIT | LAST_ACK
|──── ACK (ack=v+1) ──────────→| ④ 客户端: 收到
| TIME_WAIT (等 2MSL) | CLOSED
| CLOSED |
为什么服务端 ACK 和 FIN 不能合并?
客户端发 FIN 表示”我不发了”。但服务端可能还有数据要发——TCP 是全双工的,客户端关了自己的发送通道不代表服务端的发送通道也要关。
ACK 是内核立刻回复的(确认收到客户端 FIN),但 FIN 是服务端应用层决定发送的(close() 或 shutdown() 调用)。 两者时间可能相差很远——如果服务端应用正在处理一个慢查询,FIN 可能要等几十秒。
这就是为什么挥手需要四次:ACK 和 FIN 之间有个时间间隙——那是服务端应用在发完剩余数据。
如果服务端也没数据可发(比如 HTTP Body 已经发完,就是个 GET 请求),可以开启 TCP 延迟确认机制(Delayed ACK),将 ACK 和 FIN 合并,变成”三次挥手”。
内核视角:close() 是如何一步步走到 FIN 的
主动关闭方调 close() 后,内核的 tcp_close_state() 用一个状态机表决定”当前状态该怎么转、要不要发 FIN”。注意表里 TCP_ESTABLISHED → TCP_FIN_WAIT1 | TCP_ACTION_FIN,TCP_ACTION_FIN 这个标志位就是”发 FIN”的指令:
/*
* State processing on a close. This implements the state shift for
* sending our FIN frame. Note that we only send a FIN for some
* states. A shutdown() may have already sent the FIN, or we may be
* closed.
*/
static const unsigned char new_state[16] = {
/* current state: new state: action: */
[0 /* (Invalid) */] = TCP_CLOSE,
[TCP_ESTABLISHED] = TCP_FIN_WAIT1 | TCP_ACTION_FIN,
[TCP_SYN_SENT] = TCP_CLOSE,
[TCP_SYN_RECV] = TCP_FIN_WAIT1 | TCP_ACTION_FIN,
[TCP_FIN_WAIT1] = TCP_FIN_WAIT1,
[TCP_FIN_WAIT2] = TCP_FIN_WAIT2,
[TCP_TIME_WAIT] = TCP_CLOSE,
[TCP_CLOSE] = TCP_CLOSE,
[TCP_CLOSE_WAIT] = TCP_LAST_ACK | TCP_ACTION_FIN,
[TCP_LAST_ACK] = TCP_LAST_ACK,
[TCP_LISTEN] = TCP_CLOSE,
[TCP_CLOSING] = TCP_CLOSING,
[TCP_NEW_SYN_RECV] = TCP_CLOSE, /* should not happen ! */
};
static int tcp_close_state(struct sock *sk)
{
int next = (int)new_state[sk->sk_state];
int ns = next & TCP_STATE_MASK;
tcp_set_state(sk, ns);
return next & TCP_ACTION_FIN;
}
分析:这张表就是四次挥手”主动关闭方”那一半的状态机。返回值 next & TCP_ACTION_FIN 非零时,调用方才会真正去发 FIN。注意 TCP_CLOSE_WAIT → TCP_LAST_ACK | TCP_ACTION_FIN:这正是被动关闭方收到 FIN、回完 ACK 之后,应用调 close() 时的跃迁——它也要发自己的 FIN,印证了”两个方向各自关闭”。
💭 思考:为什么 close() 要靠一张状态机表来推进,而不是“直接发 FIN 就完事”?——因为同一个 close() 在不同状态下动作完全不同:ESTABLISHED 里要发 FIN,SYN_SENT 里却是“还没建连就放弃”,CLOSE_WAIT 里则要转 LAST_ACK 再发 FIN。把“当前状态 → 下一步状态 + 要不要发 FIN”编成一张表,内核就不用写一堆 if-else,任何状态下 close() 也都有确定归宿。这张表本质就是“主动关闭方那一半”挥手流程的完整拼图。
内核视角:FIN 到底怎么构造和发送
tcp_send_fin() 是发送 FIN 的真正实现。有意思的是它的优化路径:如果写队列里正好有一个还没发出去的 skb,就直接把 FIN 标志”搭”在那个 skb 上(tcp_flags |= TCPHDR_FIN),而不是单独发一个空包:
void tcp_send_fin(struct sock *sk)
{
struct sk_buff *skb, *tskb, *tail = tcp_write_queue_tail(sk);
struct tcp_sock *tp = tcp_sk(sk);
/* Optimization, tack on the FIN if we have one skb in write queue and
* this skb was not yet sent, or we are under memory pressure.
* Note: in the latter case, FIN packet will be sent after a timeout,
* as TCP stack thinks it has already been transmitted.
*/
tskb = tail;
if (!tskb && tcp_under_memory_pressure(sk))
tskb = skb_rb_last(&sk->tcp_rtx_queue);
if (tskb) {
TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN;
TCP_SKB_CB(tskb)->end_seq++;
tp->write_seq++;
if (!tail) {
/* This means tskb was already sent.
* Pretend we included the FIN on previous transmit.
* We need to set tp->snd_nxt to the value it would have
* if FIN had been sent. This is because retransmit path
* does not change tp->snd_nxt.
*/
WRITE_ONCE(tp->snd_nxt, tp->snd_nxt + 1);
return;
}
} else {
skb = alloc_skb_fclone(MAX_TCP_HEADER,
sk_gfp_mask(sk, GFP_ATOMIC |
__GFP_NOWARN));
if (unlikely(!skb))
return;
INIT_LIST_HEAD(&skb->tcp_tsorted_anchor);
skb_reserve(skb, MAX_TCP_HEADER);
sk_forced_mem_schedule(sk, skb->truesize);
/* FIN eats a sequence byte, write_seq advanced by tcp_queue_skb(). */
tcp_init_nondata_skb(skb, sk, tp->write_seq,
TCPHDR_ACK | TCPHDR_FIN);
tcp_queue_skb(sk, skb);
}
__tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);
}
分析:注意 end_seq++ 和注释 “FIN eats a sequence byte”——FIN 和 SYN 一样占用一个序号,所以对端必须用 ack = seq + 1 确认它,这也是四次挥手里每一步 ack=u+1、ack=v+1 的来历。最后 __tcp_push_pending_frames 把 FIN 真正推上链路。
内核视角:close() 的完整决策链
__tcp_close() 是应用 close() 的入口,它先检查接收队列里还有没有没读完的数据,再决定走 RST 还是走正常的 FIN 挥手:
/* We need to flush the recv. buffs. We do this only on the
* descriptor close, not protocol-sourced closes, because the
* reader process may not have drained the data yet!
*/
while ((skb = skb_peek(&sk->sk_receive_queue)) != NULL) {
u32 end_seq = TCP_SKB_CB(skb)->end_seq;
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
end_seq--;
if (after(end_seq, tcp_sk(sk)->copied_seq))
data_was_unread = true;
tcp_eat_recv_skb(sk, skb);
}
/* If socket has been already reset (e.g. in tcp_reset()) - kill it. */
if (sk->sk_state == TCP_CLOSE)
goto adjudge_to_death;
...
if (unlikely(tcp_sk(sk)->repair)) {
sk->sk_prot->disconnect(sk, 0);
} else if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation,
SK_RST_REASON_TCP_ABORT_ON_CLOSE);
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
/* Check zero linger _after_ checking for unread data. */
sk->sk_prot->disconnect(sk, 0);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
} else if (tcp_close_state(sk)) {
/* We FIN if the application ate all the data before
* zapping the connection.
*/
...
tcp_send_fin(sk);
}
sk_stream_wait_close(sk, timeout);
分析:这里藏着一个容易忽略的知识点——不是所有 close() 都走四次挥手。如果接收队列还有没读的数据(data_was_unread),内核会直接发 RST 而不是 FIN,因为”你都没读完整数据,我不可能优雅挥手”。只有数据读干净了,tcp_close_state() 返回 FIN 指令,才会走到 tcp_send_fin() 开始四次挥手。
💭 思考:为什么“没读完数据就 close”会直接发 RST,而不是走四次挥手?——因为四次挥手的前提是“数据已经完整、干净地收发完”,它要靠 FIN/ACK 一步步确认。如果接收队列里还躺着没读的数据就关闭,等于告诉对端“你的数据我不要了”,此时再优雅地 FIN 挥手已经没有意义,对端只会误以为数据都被正常接收。所以内核宁可发 RST 明确告知“连接异常终止”,也不让对端产生“数据都送达了”的错觉。
TIME_WAIT 为什么等 2MSL?
MSL(Maximum Segment Lifetime)= 报文在网络中的最大存活时间。Linux 默认 30 秒,所以 TIME_WAIT = 60 秒。
先设问:既然最后一条 ACK 都发出去了,直接 CLOSED 不香吗?多等 60 秒图什么? 图的是”绝对安全”。ACK 发出去不代表对方一定收到——它也可能丢。而且这条连接上所有还在路上飘的旧报文,要等它们全部”死透”才敢把同一个四元组让给新连接。这两个担忧,正好对应下面两个目的。
两个目的:
1. 确保最后的 ACK 被对方收到
如果最后 ACK 丢失,服务端会重发 FIN。TIME_WAIT 期间客户端可以重新回复 ACK。2MSL = “最后一个 ACK 的最大存活时间 + 服务端重发 FIN 的最大存活时间”。
如果 ACK 丢失:
客户端: 发 ACK → TIME_WAIT 等待
服务端: 没收到 ACK → 超时重发 FIN
客户端: 收到 FIN → 重发 ACK → 重置 2MSL 计时器
2. 确保旧连接的所有报文在网络中”死透”
一个 TCP 连接由四元组(src_ip, src_port, dst_ip, dst_port)唯一标识。如果客户端立即从 TIME_WAIT 切换到 CLOSED 并重建相同四元组的连接,网络中还没到达的旧连接数据报文会被当成新连接的合法数据接收——造成数据错乱。2MSL 等待确保所有旧报文都已超时被丢弃。
内核视角:2MSL 是一个写死的 60 秒常量
TIME_WAIT 的时长在内核里不是临时算出来的,而是一个编译期常量 TCP_TIMEWAIT_LEN = 60*HZ(HZ 是每秒的时钟中断数,60*HZ 即 60 秒):
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
* state, about 60 seconds */
#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN
分析:60*HZ 里的 60 秒,就是 2 个 MSL(Linux 里 MSL 取 30 秒)。TIME_WAIT 状态的实际定时器在 tcp_minisocks.c 里反复用 inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN) 排期——也就是说,“等 2MSL”不是等待一个真实的丢包,而是保守地给旧报文留足消亡时间。
💭 思考:为什么 2MSL 要写死成 60 秒常量,而不是像 RTO 那样动态算?——因为 RTO 关心的是“这条连接当前的往返时间”,会随网络波动;而 TIME_WAIT 要防的是“全网任意迷路报文的最长寿命”,这是一个与具体连接无关的全局上限,取 MSL 的两倍就够了。写死反而简单可靠:不需要采样、不需要调参,任何主动关闭方都统一等满 60 秒,保证最坏情况下的安全。
内核视角:对端 LAST_ACK 收到最后 ACK 后的收尾
服务端(被动关闭方)发完 FIN 进入 LAST_ACK,当收到客户端最后的 ACK 时,tcp_rcv_state_process() 的 TCP_LAST_ACK 分支判定 snd_una == write_seq(即 FIN 已被确认)后直接销毁连接:
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
tcp_update_metrics(sk);
tcp_done(sk);
goto consume;
}
break;
分析:snd_una == write_seq 是”我发出去的所有字节(含 FIN 占的那个序号)都已被确认”的判据。一旦成立,tcp_done() 直接把 socket 清掉——被动关闭方这边没有 TIME_WAIT,它确认完就干净退场。而 TIME_WAIT 只落在主动关闭方身上,这也是”大量 TIME_WAIT 往往出现在主动 close 的服务端”的原因。
服务端大量 TIME_WAIT 怎么办?
服务端主动关闭连接才会产生 TIME_WAIT。常见的主动关闭方是客户端(浏览器),但以下场景服务端会成为主动关闭方:
- HTTP 长连接超时:
keepalive_timeout到期,Nginx 主动断开 - 连接数达上限:
keepalive_requests到上限,Nginx 主动断开 - HTTP 短连接:每次请求后 Nginx 关闭
优化措施:
# 1. 开启 TIME_WAIT 复用(让新连接复用 TIME_WAIT 状态的端口)
net.ipv4.tcp_tw_reuse = 1
# 2. 调整 Nginx 减少主动关闭
keepalive_timeout 120; # 长连接保持更久
keepalive_requests 10000; # 单连接处理更多请求
# 3. 扩大 TIME_WAIT 可追踪的连接数
net.ipv4.tcp_max_tw_buckets = 262144
总结
| 状态 | 持有方 | 含义 |
|---|---|---|
| FIN_WAIT_1 | 主动关闭方 | 已发 FIN,等对方 ACK |
| FIN_WAIT_2 | 主动关闭方 | 已收对方 ACK,等对方 FIN |
| CLOSE_WAIT | 被动关闭方 | 已收 FIN 并回了 ACK,等应用调 close() |
| LAST_ACK | 被动关闭方 | 已发 FIN,等对方最后 ACK |
| TIME_WAIT | 主动关闭方 | 等 2MSL,确保连接彻底消亡 |
章末提问
追问 1:为什么挥手是四次,不能像握手那样合并成三次? 结论先行——因为中间那两次的触发者不同、时间点也可能差很远。第二次 ACK 是内核收到 FIN 后立刻自动回复的(协议栈行为);第三次 FIN 却是服务端应用调 close()/shutdown() 才触发的(应用行为)。两者之间可能隔着服务端继续发数据的漫长间隙,所以无法合并。只有当服务端确实没有数据要发时,Delayed ACK 才会把 ACK 和 FIN 合到一起,变成”三次挥手”。
追问 2:为什么 TIME_WAIT 只出现在主动关闭方,被动关闭方没有?
结论先行——因为只有主动关闭方需要”为这条连接收尾兜底”。主动方发了最后一条 ACK,它要负责确认这条 ACK 真的被对方收到(否则对方会重发 FIN);被动方在 LAST_ACK 收到最后 ACK 后,tcp_done() 直接退场,不需要再为任何人兜底。所以内核源码里 TIME_WAIT 的定时器只在主动关闭方排期。
追问 3:2MSL 具体是哪两个 MSL 相加?为什么是 2 而不是 1 或 3? 结论先行——一个 MSL 是”最后一条 ACK 从主动方到被动方的最大存活时间”,另一个 MSL 是”被动方超时重发的 FIN 回到主动方的最大存活时间”,两者相加正好 2 个 MSL。等于 1 个 MSL 时,可能 ACK 还在路上、对方重发的 FIN 还没到,主动方就关了;等于 3 个 MSL 则纯属多余,因为最长的一来一回也就 2 个 MSL。
追问 4:如果服务端在 CLOSE_WAIT 一直不调 close(),会怎样?
结论先行——连接会一直僵在 CLOSE_WAIT,服务端资源(文件描述符、socket)无法释放,最终耗尽连接数。因为 CLOSE_WAIT 是”被动关闭方收到了 FIN、回了 ACK,但应用还没调 close()“的中间态,内核只能等应用,自己不会主动推进。这是典型的应用层 bug(比如代码里没在合适的时机 close,或进程异常但没关 socket),排查时看 netstat 里大量 CLOSE_WAIT 就能定位。
追问 5:TIME_WAIT 过多和服务端有什么关系?怎么优化?
结论先行——TIME_WAIT 只落在主动关闭方,服务端一旦成为主动关闭方(比如 Nginx 的 keepalive 超时、短连接),就会堆积大量 TIME_WAIT。优化思路有三:一是让服务端尽量别当主动关闭方(调大 keepalive_timeout/keepalive_requests);二是复用端口(tcp_tw_reuse);三是扩大可追踪的 TIME_WAIT 数量上限(tcp_max_tw_buckets),避免溢出后被内核强制回收。