2、SO_REUSEADDR 原理:SO_REUSEADDR 的作用、TIME_WAIT 状态详解、SO_REUSEADDR 与 TIME_WAIT 的关系
聊到端口复用,绕不开的就是 SO_REUSEADDR 这个套接字选项。很多刚入行的朋友觉得它就是个“防绑定失败”的开关,打开就完事了。但说实话,如果不懂它背后的 TIME_WAIT 状态,你迟早会在线上踩坑。我当年就吃过这个亏,一个高并发的短连接服务,上线不到半小时端口全占满,业务直接挂掉。
这一节,咱们就把这两个东西彻底讲透。
2.1 SO_REUSEADDR 到底在干什么?
先看定义。SO_REUSEADDR 是一个 socket 选项,告诉内核:允许重用本地地址和端口。默认情况下,一个端口被占用后,其他进程(甚至同一个进程的其他 socket)就不能再绑定它了。但打开这个选项后,规则就变了。
它的核心作用有三个:
- 允许重用处于 TIME_WAIT 状态的端口 —— 这是最常用的场景
- 允许多个 socket 绑定到同一个地址和端口 —— 但前提是 IP 不同(比如多播场景)
- 允许在同一个端口上启动多个监听实例 —— 比如热升级时新老进程共存
我个人习惯,在写 TCP 服务端代码时,bind() 之前一定会先设置这个选项。代码很简单:
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt SO_REUSEADDR failed");
exit(1);
}
嗯,就这么几行,能省掉你后面 90% 的端口冲突问题。
SO_REUSEPORT,它和 SO_REUSEADDR 不是一回事。SO_REUSEPORT 允许完全相同的地址+端口被多个 socket 绑定,内核会做负载均衡。但 SO_REUSEADDR 只管 TIME_WAIT 和地址冲突。别搞混了。
2.2 TIME_WAIT 状态详解
要理解 SO_REUSEADDR,必须先搞懂 TIME_WAIT。这是 TCP 连接关闭流程中最“磨人”的一个状态。
为什么会有 TIME_WAIT?
TCP 是可靠的。当主动关闭方发送最后一个 ACK 后,它不能确定这个 ACK 是否被对方收到。如果 ACK 丢了,对方会重发 FIN。所以主动关闭方必须保留足够的信息,来重发这个 ACK。这个“保留信息”的阶段,就是 TIME_WAIT。
TIME_WAIT 的持续时间是 2MSL(Maximum Segment Lifetime)。MSL 在 Linux 上默认是 30 秒,所以 TIME_WAIT 通常持续 60 秒。
在这 60 秒内,这个四元组(源 IP、源端口、目标 IP、目标端口)是不能被重用的。你想想看,如果你的服务每秒处理几千个短连接,每个连接关闭后都要等 60 秒才能释放端口……那端口号很快就用光了。
我整理了一个 TCP 关闭状态流转表,方便你对照:
| 状态 | 触发条件 | 持续时间 | 说明 |
|---|---|---|---|
| FIN_WAIT_1 | 主动关闭方发送 FIN | 瞬时或等待 ACK | 等待对方的 ACK 确认 |
| FIN_WAIT_2 | 收到对方 ACK | 等待对方 FIN | 半关闭状态,只能收不能发 |
| CLOSE_WAIT | 被动关闭方收到 FIN | 等待应用层调用 close() | 应用层不关 socket,这个状态会一直挂着 |
| LAST_ACK | 被动关闭方发送 FIN | 等待最后一个 ACK | 被动关闭方的“最后确认”阶段 |
| TIME_WAIT | 主动关闭方收到 FIN 并发送最后一个 ACK | 2MSL(60秒) | 等待可能重传的 FIN,确保连接可靠关闭 |
| CLOSED | TIME_WAIT 超时 | — | 连接彻底结束 |
CLOSE_WAIT,那说明应用层没有正确关闭 socket。这不是 TIME_WAIT 的问题,是代码 bug。我曾经排查过一个线上故障,发现 80% 的连接卡在 CLOSE_WAIT,最后定位到是某个第三方库忘了调用 close()。
2.3 SO_REUSEADDR 与 TIME_WAIT 的关系
好,现在把两个概念串起来。
默认情况下,一个端口处于 TIME_WAIT 状态时,你不能用同样的地址和端口去 bind。内核会告诉你 Address already in use。但如果你设置了 SO_REUSEADDR,内核就会放行。
说白了,SO_REUSEADDR 就是用来绕过 TIME_WAIT 限制的。它告诉内核:我知道这个端口还在 TIME_WAIT,但我就是要用,你让我绑吧。
但这里有个关键点:SO_REUSEADDR 只对 主动关闭方 的 TIME_WAIT 有效。什么意思?
- 如果你的服务端是主动关闭连接的一方(比如短连接服务中,服务端主动 close),那么服务端会进入 TIME_WAIT。此时设置
SO_REUSEADDR可以立即重用端口。 - 如果你的服务端是被动关闭的一方(比如客户端主动断连),服务端不会进入 TIME_WAIT,而是直接进入 CLOSED 或 CLOSE_WAIT。这时候
SO_REUSEADDR其实用不上。
我画了一张图,帮你理清这个逻辑:
所以,SO_REUSEADDR 和 TIME_WAIT 的关系可以总结为:
- TIME_WAIT 是问题 —— 它占着端口不放,导致新连接无法绑定
- SO_REUSEADDR 是解药 —— 它允许你在 TIME_WAIT 期间重用端口
- 但解药只对主动关闭方有效 —— 如果你的服务是被动关闭的,TIME_WAIT 根本不会出现在服务端,你也不用操心
2.4 实战中的避坑指南
讲完了原理,说几个我踩过的坑。
坑一:只设了服务端,没设客户端。
有一次我写一个压测工具,客户端频繁创建短连接。结果跑了几万次后,客户端报 Address already in use。我检查了半天,发现服务端确实设了 SO_REUSEADDR,但客户端没设。客户端主动关闭连接后进入 TIME_WAIT,端口被占满。解决方案很简单:客户端也加上 SO_REUSEADDR。
坑二:以为 SO_REUSEADDR 能解决所有 TIME_WAIT 问题。
它只能解决 bind() 阶段的冲突。如果你的服务已经 bind 成功,但 connect() 时发现四元组冲突(同一个源 IP、源端口、目标 IP、目标端口已经存在),那 SO_REUSEADDR 也帮不了你。这种情况需要调整连接池策略,或者缩短 TIME_WAIT 时间。
坑三:在 Windows 上行为不一样。
嗯,这个要特别提一下。Windows 上的 SO_REUSEADDR 语义和 Linux 不完全一样。在 Windows 上,它允许完全相同的地址+端口被多个 socket 绑定,行为更像 Linux 的 SO_REUSEPORT。跨平台开发时一定要留意。
SO_REUSEADDR 应该作为标配。每次 bind 之前都设一下,成本几乎为零,但能避免很多线上故障。我现在的项目模板里,socket 创建后的第一件事就是 setsockopt 设这个选项。
最后留个思考题:如果你的服务端是短连接模型,每秒处理 5000 个请求,每个请求都是服务端主动关闭。60 秒的 TIME_WAIT 会占用多少端口?算一算你就知道为什么端口复用这么重要了。