飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • IP协议

    • 认识 IP 协议与网络层定位
    • IPv4 编址体系与分类地址
    • CIDR 与子网划分实战
    • IPv6 编址体系
    • ARP 协议详解
    • NDP 协议详解
    • IGMP 与 MLD 组播侦听发现
    • VRRP 与网关冗余
    • IPv4 数据报首部解析
    • IPv6 数据报与扩展首部
    • IPsec 安全扩展
    • 分片、MTU 与路径发现
    • ICMP 与 Traceroute 原理
    • 路由基础与转发流水线
    • 动态路由协议
    • NAT 网络地址转换
    • DHCP 与自动配置
    • Wireshark 与命令行工具
    • IPv6 过渡技术
    • IP 协议栈排障与攻击防御
  • TCP协议

    • 认识 TCP 协议与传输层定位
    • TCP 报文段首部解析
    • 三次握手与连接建立
    • 四次挥手与连接释放
    • TCP 有限状态机
    • 序列号与确认机制
    • 超时重传与 RTO 计算
    • 滑动窗口与流量控制
    • 拥塞控制基础
    • 现代拥塞控制算法
    • TCP 选项与扩展
    • TCP 性能调优与内核参数
    • Nagle 算法与糊涂窗口综合征
    • TCP 定时器与 Keep-Alive 机制
    • TCP 安全与攻击防御
    • TCP 与上层/下层交互
    • TCP 综合实践与排障

四次挥手与连接释放

为什么需要四次挥手

TCP 连接是全双工的——双方可以同时发送和接收数据。关闭连接时,每个方向的关闭必须独立进行,因此需要四次报文交换。

生活例子:两人打电话,一方说"我说完了"(FIN),另一方说"我知道了"(ACK),但另一方可能还有话要说;等另一方也说"我说完了"(FIN),最初的一方回复"我知道了"(ACK),双方才都挂断。

四次挥手完整流程

第一步:FIN(主动关闭方)

客户端(假设主动关闭)发送:

FIN=1, Seq=u

客户端进入 FIN_WAIT_1 状态。FIN 消耗一个序列号。

第二步:ACK(被动关闭方)

服务端回复:

ACK=1, Ack=u+1

服务端进入 CLOSE_WAIT 状态。此时服务端仍可发送数据(半关闭状态)。

第三步:FIN(被动关闭方数据发完)

服务端数据发送完毕后:

FIN=1, Seq=w

服务端进入 LAST_ACK 状态。

第四步:ACK(主动关闭方)

客户端回复:

ACK=1, Ack=w+1

客户端进入 TIME_WAIT 状态;服务端收到后进入 CLOSED。

TIME_WAIT 状态:为什么等 2MSL

TIME_WAIT 状态持续 2MSL(Maximum Segment Lifetime,通常 2 分钟,故 TIME_WAIT 约 4 分钟)。设计目的:

  1. 确保最后一个 ACK 被服务端收到:如果 ACK 丢失,服务端会重发 FIN,客户端需能响应
  2. 防止旧连接报文干扰新连接:等待旧报文从网络中彻底消失,避免序列号混淆

MSL 是什么:报文在网络中存活的最大时间,RFC 793 建议 2 分钟。Linux 默认 60 秒,故 TIME_WAIT 约 120 秒。

TIME_WAIT 过多的问题与调优

高并发服务器(如 Web 服务器)主动关闭大量连接后,会产生大量 TIME_WAIT,导致:

  • 本地端口耗尽:同一 {目的IP:目的端口} 组合下,TIME_WAIT 连接占用本地端口
  • 内存占用:每个 TIME_WAIT 连接占用少量内核内存

调优参数(谨慎使用)

# 允许复用 TIME_WAIT 连接(仅出站连接,安全)
sysctl net.ipv4.tcp_tw_reuse=1

# 缩短 TIME_WAIT 时间(Linux 2.6 后默认 60 秒)
sysctl net.ipv4.tcp_fin_timeout=30

# 注意:tcp_tw_recycle 在 NAT 环境下有风险,Linux 4.12+ 已移除

根本解决方案:

  • HTTP Keep-Alive:复用 TCP 连接,减少新建连接
  • 连接池:数据库/缓存客户端维护长连接
  • 服务端避免主动关闭:让客户端主动关闭,服务端不进入 TIME_WAIT

同时关闭:CLOSING 状态

双方同时主动关闭的罕见场景:

双方同时发送 FIN,都进入 FIN_WAIT_1;收到对方 FIN 后发送 ACK 并进入 CLOSING;再收到对方 ACK 后进入 TIME_WAIT。

半关闭:shutdown 函数

Socket API 提供 shutdown() 实现半关闭:

shutdown(sockfd, SHUT_WR);  // 关闭写方向,仍可读
shutdown(sockfd, SHUT_RD);  // 关闭读方向,仍可写
shutdown(sockfd, SHUT_RDWR); // 双向关闭

shutdown(SHUT_WR) 发送 FIN,进入 FIN_WAIT_2 状态(只等对方发 FIN),而非完全关闭连接。

本篇小结

  • 四次挥手原因:TCP 全双工,每个方向独立关闭
  • 流程:FIN → ACK(被动方仍可发数据)→ FIN → ACK
  • TIME_WAIT 持续 2MSL:确保 ACK 送达 + 防止旧报文干扰
  • TIME_WAIT 过多:端口耗尽,用 Keep-Alive/连接池/服务端不主动关闭解决
  • CLOSING:双方同时主动关闭的罕见状态
  • 半关闭:shutdown(SHUT_WR) 只关闭写方向

动手实践

  1. Wireshark 抓包观察四次挥手:

    • 过滤 tcp.flags.fin==1 找 FIN 报文
    • 观察 TIME_WAIT 前的最后一个 ACK
  2. 查看本机 TIME_WAIT 连接数量:

    ss -tan | grep TIME-WAIT | wc -l
    
  3. 查看 TIME_WAIT 相关内核参数:

    sysctl net.ipv4.tcp_tw_reuse
    sysctl net.ipv4.tcp_fin_timeout
    
  4. 思考:为什么服务端收到 FIN 后不能立即回复 FIN+ACK,而是先回复 ACK,等数据发完再发 FIN?如果合并成三次挥手会有什么风险?

上一页
三次握手与连接建立
下一页
TCP 有限状态机