1、理解TIME_WAIT状态
TCP 协议规定,主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个 MSL (maximum segment lifetime) 的时间后才能回到 CLOSED 状态. 我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方,在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口; MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同,在 Centos7/Ubuntu 上默认配置的值是 60s; 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值;
1. TIME_WAIT 是什么?(“主动关闭方”的专利)
TCP 连接终止时,主动发起关闭(发送第一个 FIN)的一方,在收到对端最后一次确认(ACK)后,并不会直接进入 CLOSED 状态,而是进入TIME_WAIT状态。
2. 为什么要等 2MSL?(核心设计的两个使命)
TIME_WAIT 的持续时间是2MSL(MSL:Maximum Segment Lifetime,报文最大生存时间)。在 Linux 系统中,MSL 通常为 30秒,因此 TIME_WAIT 通常持续60秒。这个等待期肩负着两大不可替代的使命:
2MSL=MSL(等待可能延迟的ACK)+MSL(等待可能重传的FIN)
这确保了主动关闭方发送的最后一个ACK能够可靠到达
网络中所有属于这个连接的报文都彻底消失
等待2MSL的主要原因是:
1、确保双方4次挥手都尽可能正确完成
2、让陈旧报文,在网络中尽可能消散
使命一:确保最后一个 ACK 能被对方收到(“兜底”机制)
主动关闭方发出的最后一个 ACK 可能会丢失。如果被动关闭方没收到 ACK,它会超时重传 FIN。
如果主动关闭方直接关闭(进入 CLOSED),就收不到重传的 FIN,导致被动方永远停留在 LAST_ACK 状态,浪费资源。
处于 TIME_WAIT 的机器可以响应这个重传的 FIN,并重新发送 ACK,从而保证双方都能正常关闭。
使命二:让网络中“迷路的”旧数据包自然消亡(“防止串线”)
由于网络延迟,连接中可能还有迟到的数据包(延迟段)正在网络中游荡。
如果旧连接关闭后,马上用相同的 IP 和端口建立新连接,这些“幽灵”数据包突然到达,就会被新连接误认为是合法数据,导致数据错乱。
等待 2MSL 的时间,足以保证网络中该连接的所有残余报文(包括重传的)全部消失。这样,新连接就不会受到旧数据的干扰。
3. 为什么 TIME_WAIT 集中在“服务器”上?
很多人误以为 TIME_WAIT 是服务器的噩梦,其实这取决于谁是“主动关闭方”:
短连接 + 高并发(如 HTTP/1.0 服务):服务器处理完请求后主动关闭连接,大量 TIME_WAIT 堆积在服务器上。
客户端主动关闭(如调用方):TIME_WAIT 堆积在客户端。
这里有一个反直觉的冷知识:TIME_WAIT 占用的不是内存,而是端口号(五元组中的源端口)。由于一个端口在 TIME_WAIT 期间不能被复用,如果服务器短时间爆发百万级连接,端口就会被耗光,导致无法建立新连接(报错Cannot assign requested address)。
4. 如何应对 TIME_WAIT 过多?
既然无法绕过,就需要策略性地处理:
最佳实践(长连接):在微服务内部调用中,尽量使用HTTP Keep-Alive或长连接池,避免频繁重复建立和关闭连接,从根源上减少 TIME_WAIT 的产生。
开启端口重用(
tcp_tw_reuse):Linux 内核允许在 TIME_WAIT 状态下复用端口,前提是新连接的时间戳大于旧连接的时间戳(需开启tcp_timestamps)。这非常适用于客户端或短连接场景。注意:严禁开启
tcp_tw_recycle(Linux 4.12 后已移除),它在 NAT 环境下会导致严重的丢包问题。
调整
net.ipv4.ip_local_port_range:扩大本地端口的可用范围(如改为1024 65535),增加端口池容量。使用
SO_REUSEADDR套接字选项:允许服务器在 TIME_WAIT 状态下立即重启并绑定同一个端口(常用于开发调试或需要快速重启的服务)。
2、解决TIME_WAIT状态引起的bind失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求). 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接. 由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现问题. 使用 setsockopt ()设置 socket 描述符的 选项 SO_REUSEADDR 为 1 , 表示允许创建端口号相同但IP地址不同的多个 socket 描述符
四次挥手之后主动断开连接的一方,进入time_wait状态,被动断开立即释放链接。
3、滑动窗口
这种一次发一个太低效了
我们可以如上图一样,一次性发送多组数据。可以大大的提高性能。
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是 4000 个字节 (四个段). ・发送前四个段的时候,不需要等待任何 ACK, 直接发送; ・收到第一个 ACK 后,滑动窗口向后移动,继续发送第五个段的数据;依次类推; ・操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉; ・窗口越大,则网络的吞吐率就越高;
情况1:数据包已经到达,但是ACK丢失了
可以通过后续的ACK进行确认。
情况2:
数据报直接丢失了
当某一段报文段丢失之后,发送端会一直收到 1001 这样的 ACK, 就像是在提醒发送端 "我想要的是 1001" 一样; 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送; 这个时候接收端收到了 1001 之后,再次返回的 ACK 就是 7001 了 (因为 2001‑7000) 接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中; 这种机制被称为 "高速重发控制"(也叫 "快重传").
以下这段为AI生成,辅助记忆
快重传,一步一步完整拆解
先铺垫最基础规则
接收方的 ACK 含义:
ACK = X→X 之前所有字节我收到了,我现在最想要 X 号字节举例子: 字节:1‑1000,1001‑2000,2001‑3000,3001‑4000,4001‑5000
场景开始(丢包发生)
- 发送方一口气发出 5 段数据
- 第一段:1‑1000 ✅顺利到达接收方
- 第二段:1001‑2000 ❌在路上丢了!没送到接收方
- 第三段:2001‑3000 ✅送到了
- 第四段:3001‑4000 ✅送到了
- 第五段:4001‑5000 ✅送到了
重点!第三、四、五段虽然到了,但是顺序乱了(失序)。接收方想要的 1001 还没来,后面的数据就算收到了,也不能向上交给应用。
接收方怎么做?(最关键)
TCP 规定:每当收到一个失序报文,立刻重复发送一次 ACK,告诉发送方我想要 1001
- 收到 1‑1000 → 返回
ACK=1001(正常的第一次)- 1001‑2000 丢了,收不到
- 收到 2001‑3000(失序)→ 再次发
ACK=1001(重复 ACK #1)- 收到 3001‑4000(失序)→ 再次发
ACK=1001(重复 ACK #2)- 收到 4001‑5000(失序)→ 再次发
ACK=1001(重复 ACK #3)👉 发送方现在收到了:3 个一模一样的重复 ACK=1001
发送方触发快重传
规则:收到 3 次重复 ACK,不等超时定时器到期,立刻重传丢失的报文 1001‑2000这一步就是快重传。
对比老方案【超时重传】:发送方只能干等着超时计时器走完漫长的时间,才能重传。速度很慢。 快重传不需要等!靠重复 ACK 快速发现丢包。
重传之后发生什么?
发送方重新发出:1001‑2000 这一次成功到达接收方。 现在接收方手里:1‑5000 全部集齐,顺序完整! 于是接收方返回一个新的、大的 ACK:
ACK=5001意思:1‑5000 字节全部收到,我接下来想要 5001
数字流程一览
表格
事件 ACK 返回值 收到 1‑1000 ACK=1001(原始) 收到 2001‑3000 ACK=1001(重复 1) 收到 3001‑4000 ACK=1001(重复 2) 收到 4001‑5000 ACK=1001(重复 3) 发送方收到 3 次重复 ACK →重传 1001‑2000 收到重传的 1001‑2000 ACK=5001(全新确认) 高频坑点(90% 同学这里出错)
- 3 次重复 ACK,不算最开始正常的那一个 ACK
- 总共收到4 个 ACK=1001,其中后面 3 个是重复冗余 ACK
- 只重传丢失的那一段!!不会把窗口里所有数据全部重发,本例只重传 1001‑2000,不是重发后面 2001‑5000。
- 快重传 ≠ 快恢复
- 快重传:负责快速把丢了的数据重新发一遍(解决丢包)
- 快恢复:紧跟着快重传,调整拥塞窗口 cwnd 大小(拥塞控制)
超时重传 VS 快重传对比
- 超时重传丢包→没人发重复 ACK→定时器倒计时结束→重传。等待时间很长,并且
cwnd=1,窗口暴跌。- 快重传丢包→收到 3 次重复 ACK→立刻重传,不等超时。配套快恢复,窗口不会降到 1,
ssthresh减半大白话口诀背诵
一段报文路上丢,后面报文抢先到。 接收不停发旧 ACK,一连三遍把信捎。 发送收到三个复,不等超时立刻抛。
4、流量控制
接 收 端 处 理 数据 的 速 度 是 有 限 的. 如 果 发 送 端 发 的 太 快 , 导 致 接 收 端 的 缓 冲 区 被 打 满 , 这 个 时 候 如 果 发 送 端 继 续 发 送 , 就 会 造 成 丢 包 , 继 而 引 起 丢 包 重 传 等 等 一 系 列 连 锁 反 应 . 因 此 T C P 支 持 根 据 接 收 端 的 处 理 能 力 , 来 决 定 发 送 端 的 发 送 速 度 . 这 个 机 制 就 叫 做 流 量 控 制 ( F l o w C o n t r o l ) ; • 接 收 端 将 自 己 可 以 接 收 的 缓 冲 区 剩 余 空 间 大 小 放 入 T C P 首 部 中 的 " 窗 口 大 小 " 字 段 , 通 过 A C K 端 通 知 发 送 端 ; • 窗 口 大 小 字 段 越 大 , 说 明 网 络 的 吞 吐 量 越 高 ; • 接 收 端 一 旦 发 现 自 己 的 缓 冲 区 快 满 了 , 就 会 将 窗 口 大 小 设 置 成 一 个 更 小 的 值 通 知 给 发 送 端 ; • 发 送 端 接 受 到 这 个窗 口 之 后 , 就 会 减 慢 自 己 的 发 送 速 度 ;