提升篇答疑:TCP四次挥手与连接状态转换
本文为提升篇答疑部分,针对读者常见疑问进行集中解答,重点涵盖 TCP 四次挥手(Four-Way Handshake)过程、连接状态转换、MSL 含义、listen backlog 语义以及 UDP connect 相关问题。
TCP 四次挥手
TCP 建立连接需要三次握手(Three-Way Handshake),而终止连接需要四次挥手。四次挥手的完整过程如下:
详细过程
- 主动关闭方发送 FIN:一方应用程序调用
close(),该端 TCP 发送 FIN 包,进入 FIN_WAIT_1 状态; - 被动关闭方确认 FIN:接收端 TCP 协议栈将 FIN 包插入一个 EOF(End of File)到接收缓冲区中。注意,EOF 会被放置在已排队等候的其他已接收数据之后,应用程序通过
read()调用感知此 FIN 包。被动关闭方进入 CLOSE_WAIT 状态; - 被动关闭方发送 FIN:被动关闭方读到 EOF 后,应用程序调用
close()关闭套接字,TCP 发送 FIN 包,进入 LAST_ACK 状态; - 主动关闭方确认 FIN:主动关闭方收到 FIN 包并回复 ACK,进入 TIME_WAIT 状态。被动关闭方收到 ACK 后进入 CLOSED 状态。经过 2MSL 时间后,主动关闭方也进入 CLOSED 状态。
每个方向都需要一个 FIN 和一个 ACK,因此称为四次挥手。
TCP 连接状态转换图
关键说明
- 半关闭(Half-Close):使用
shutdown()可执行一端到另一端的半关闭,只关闭写端或读端; - 进程退出与 FIN:进程无论是正常退出(
exit()或main()返回)还是非正常退出(如收到 SIGKILL 信号,即kill -9),所有打开的描述符都会被系统关闭,TCP 连接上会发出 FIN 包; - 主动关闭方:客户端或服务器均可发起主动关闭。大多数实际场景中由客户端执行主动关闭,但 HTTP/1.0 由服务器发起主动关闭。
MSL 与 TTL
MSL(Maximum Segment Lifetime) 是任何 IP 数据报在互联网中存活的最长时间。其实现机制并非计时器,而是通过 IP 数据报头部的 TTL(Time to Live) 字段:
- TTL 为 8 位字段,最大值 255;
- 由源主机设置初始值,表示 IP 数据报可经过的最大路由器跳数;
- 每经过一个路由器,TTL 减 1,减至 0 时路由器丢弃该数据报并发送 ICMP 报文通知源主机;
- RFC 793 规定 MSL 为 2 分钟,Linux 实际设置为 30 秒。
内核版本注记:Linux 7.0 中,MSL 值仍为 30 秒,可通过
/proc/sys/net/ipv4/tcp_fin_timeout调整 TIME_WAIT 超时时间。注意tcp_fin_timeout的单位为秒,默认 60 秒,但实际 TIME_WAIT 持续时间受 MSL 约束。
listen 函数中 backlog 参数的语义
listen() 函数的 backlog 参数含义经历了历史演变:
| 内核版本 | backlog 语义 | 相关配置 |
|---|---|---|
| Linux < 2.2 | 未完成连接队列(SYN 队列)最大长度 | 无 |
| Linux >= 2.2 | 已完成连接队列(Accept 队列)最大长度 | 未完成队列由 tcp_max_syn_backlog 控制 |
- 未完成连接队列(SYN 队列):存放尚未完成三次握手的连接,最大长度由
/proc/sys/net/ipv4/tcp_max_syn_backlog控制,默认值 128; - 已完成连接队列(Accept 队列):存放已完成三次握手、等待
accept()返回的连接,最大长度为backlog参数与/proc/sys/net/core/somaxconn的较小值,默认 128。
内核版本注记:Linux 2.4.25 之前,
somaxconn为不可修改的固定值 128。Linux 7.0 中,somaxconn默认值已提升至 4096,适应高并发场景。设计良好的程序在 128 的限制下也可支持大量并发连接,关键在于 I/O 分发效率和多线程设计。
UDP connect 与断开
UDP connect 的本质
UDP connect() 不是发起连接请求,而是记录目的地址和端口到套接字的映射关系。断开则删除该映射关系。
未 connect 时为何能收到数据
此问题涉及两种不同的 API 场景:
场景一:ICMP 报文定位
ICMP 不可达报文通过目的地址和端口区分。未调用 connect() 时,目的地址和端口无法与套接字对应,内核无法将 ICMP 报文通知给应用程序。
场景二:正常数据收发
服务器端通过 recvfrom() 获取客户端地址和端口信息(UDP 报文包含此信息),再通过 sendto() 将响应发送给客户端:
// 服务器端程序
int n = recvfrom(socket_fd, message, MAXLINE, 0, (struct sockaddr *) &client_addr, &client_len);
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
sendto(socket_fd, send_line, strlen(send_line), 0, (struct sockaddr *) &client_addr, client_len);
核心区别:
connect()建立的是客户端目的地址和端口 → 套接字的映射(用于 ICMP 错误定位);- 正常收发依赖的是客户端源地址和端口 → 套接字的映射(由
bind()或内核自动分配建立)。
UDP 套接字的多次 connect
TCP 套接字的 connect() 只能调用一次,但 UDP 套接字允许多次 connect() 操作,具有两个作用:
- 重新指定对端地址:将套接字关联到新的 IP 地址和端口号;
- 断开已连接的套接字:第二次调用
connect()时,将套接字地址结构的地址族成员设置为AF_UNSPEC,即可解除关联。
第 11 讲程序与时序图答疑
针对关闭连接相关程序的常见疑问:
问题 1:代码运行结果是先显示 "Hi, data1",之后才接收到标准输入的 close,为何时序图中画的是先 close 再接收到 "Hi, data1"?
解答:时序图中的 close 表示客户端发起的 close() 调用,与代码执行顺序的显示时序不同。
问题 2:当一方主动 close 后,另一方发送数据时收到 RST,主动方缓冲区是否会丢弃该数据?
解答:"Hi, data1" 确实不应被接收到。该数据报即使发送出去也会收到 RST 回执,应用层无法读取。
问题 3:代码中 SIGPIPE 的作用不是忽略吗,为何服务器端会退出?
解答:默认的 SIGPIPE 行为是终止程序。signal(SIGPIPE, SIG_IGN) 将其设为忽略,但实际程序仍需做清理工作。
问题 4:主动关闭方关闭写端但未关闭读端,此时再读到数据是否就是 RST?然后 SIGPIPE?
解答:此理解有误。若主动关闭方调用 shutdown() 关闭写端但保留读端,主动关闭方可以正常读取对端数据。此时使用的是 read() 操作而非 write(),不会产生 RST,也不会触发 SIGPIPE。
总结
本文解答了提升篇中的核心疑问:
- TCP 四次挥手:每个方向各需一个 FIN 和 ACK,主动关闭方经历 TIME_WAIT 状态(2MSL);
- MSL 与 TTL:MSL 通过 TTL 机制实现,Linux 默认 30 秒;
- backlog 语义:Linux 2.2+ 定义为已完成连接队列长度,SYN 队列由
tcp_max_syn_backlog控制; - UDP connect:建立目的地址映射以接收 ICMP 错误,与源地址映射(正常收发)是两套独立机制;
- UDP 多次 connect:允许重新指定对端或通过
AF_UNSPEC断开关联。
思考题
- TIME_WAIT 状态持续 2MSL 的设计目的是什么?若缩短此时间可能产生什么问题?
- 在高并发服务器中,如何有效管理大量 TIME_WAIT 状态的连接?
版本信息
- 更新日期:2026-06-09
- 目标内核:Linux 7.0