{T}

提升篇答疑:TCP四次挥手与连接状态转换

本文为提升篇答疑部分,针对读者常见疑问进行集中解答,重点涵盖 TCP 四次挥手(Four-Way Handshake)过程、连接状态转换、MSL 含义、listen backlog 语义以及 UDP connect 相关问题。

TCP 四次挥手

TCP 建立连接需要三次握手(Three-Way Handshake),而终止连接需要四次挥手。四次挥手的完整过程如下:

图表渲染中…

详细过程

  1. 主动关闭方发送 FIN:一方应用程序调用 close(),该端 TCP 发送 FIN 包,进入 FIN_WAIT_1 状态;
  2. 被动关闭方确认 FIN:接收端 TCP 协议栈将 FIN 包插入一个 EOF(End of File)到接收缓冲区中。注意,EOF 会被放置在已排队等候的其他已接收数据之后,应用程序通过 read() 调用感知此 FIN 包。被动关闭方进入 CLOSE_WAIT 状态;
  3. 被动关闭方发送 FIN:被动关闭方读到 EOF 后,应用程序调用 close() 关闭套接字,TCP 发送 FIN 包,进入 LAST_ACK 状态;
  4. 主动关闭方确认 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() 将响应发送给客户端:

plaintext
// 服务器端程序
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() 操作,具有两个作用:

  1. 重新指定对端地址:将套接字关联到新的 IP 地址和端口号;
  2. 断开已连接的套接字:第二次调用 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。

总结

本文解答了提升篇中的核心疑问:

  1. TCP 四次挥手:每个方向各需一个 FIN 和 ACK,主动关闭方经历 TIME_WAIT 状态(2MSL);
  2. MSL 与 TTL:MSL 通过 TTL 机制实现,Linux 默认 30 秒;
  3. backlog 语义:Linux 2.2+ 定义为已完成连接队列长度,SYN 队列由 tcp_max_syn_backlog 控制;
  4. UDP connect:建立目的地址映射以接收 ICMP 错误,与源地址映射(正常收发)是两套独立机制;
  5. UDP 多次 connect:允许重新指定对端或通过 AF_UNSPEC 断开关联。

思考题

  1. TIME_WAIT 状态持续 2MSL 的设计目的是什么?若缩短此时间可能产生什么问题?
  2. 在高并发服务器中,如何有效管理大量 TIME_WAIT 状态的连接?

版本信息

  • 更新日期:2026-06-09
  • 目标内核:Linux 7.0