1. 端口复用基础
端口复用,说白了就是让多个 socket 能绑到同一个端口上。你可能会问:「这不是乱套了吗?」嗯,刚开始我也有这个疑问。后来在线上环境踩过坑才明白——这玩意儿用好了是神器,用不好就是灾难。
咱们先搞清楚两个核心概念:SO_REUSEADDR 和 SO_REUSEPORT。名字很像,但行为完全不同。
1.1 SO_REUSEADDR:老牌选手
这个选项出现得最早,几乎所有操作系统都支持。它的核心作用是:允许重用处于 TIME_WAIT 状态的端口。
我记得刚入行那会儿,写了个测试服务器,每次 Ctrl+C 杀掉进程再重启,就报错 "Address already in use"。当时我一脸懵,后来才知道是 TIME_WAIT 在作怪。加上 SO_REUSEADDR 就解决了。
SO_REUSEADDR 的核心场景:
- 服务端重启时,避免 "Address already in use" 错误
- 多进程监听同一个端口(但需要所有进程都设置该选项)
- 解决 TIME_WAIT 状态下的端口占用问题
代码示例很简单,几行就搞定:
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt SO_REUSEADDR failed");
exit(1);
}
但要注意——SO_REUSEADDR 不是万能的。它只对 TIME_WAIT 状态的端口有效。如果端口被其他进程正常占用(ESTABLISHED 或 LISTEN 状态),照样绑不上。
我曾经踩过的坑:
在某个高并发项目中,我天真地以为 SO_REUSEADDR 能解决所有端口冲突问题。结果线上出现了两个进程同时监听同一个端口,导致请求被随机分发到错误的进程。排查了一整天才发现——原来 SO_REUSEADDR 在 Linux 上对 UDP 多播场景的行为和 TCP 完全不同。所以,一定要看内核版本和协议类型。
1.2 SO_REUSEPORT:新时代的解决方案
SO_REUSEPORT 是后来加入的,Linux 3.9 才开始支持。它的设计目标更明确:允许多个进程/线程绑定到完全相同的 IP 和端口,并且内核负责负载均衡。
你想想看,以前要实现多进程监听同一个端口,得用 fork + 共享 listen fd,或者用 SO_REUSEADDR 配合多播。现在有了 SO_REUSEPORT,每个进程自己 bind 就行,内核自动把新连接分发给各个进程。
SO_REUSEPORT 的核心优势:
- 真正的端口共享——多个进程可以同时 bind 同一个 IP:PORT
- 内核级负载均衡——新连接会被均匀分发到各进程
- 平滑扩缩容——新进程启动后自动参与分发,旧进程退出后自动摘除
- 每个进程独立 accept——避免了惊群效应
代码示例:
int opt = 1;
// 注意:需要同时设置 SO_REUSEADDR 和 SO_REUSEPORT
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, SOMAXCONN);
个人建议:在实际项目中,我习惯把 SO_REUSEADDR 和 SO_REUSEPORT 一起设置。虽然 SO_REUSEPORT 已经包含了部分 SO_REUSEADDR 的功能,但加上 SO_REUSEADDR 可以兼容旧版本内核,也能处理 TIME_WAIT 问题。多一行代码,少一个坑。
1.3 两者的核心区别
我整理了一张对比表,方便你快速查阅:
| 特性 | SO_REUSEADDR | SO_REUSEPORT |
|---|---|---|
| 主要作用 | 重用 TIME_WAIT 状态的端口 | 多进程共享同一端口 |
| 多进程绑定 | 仅限多播/广播场景 | 支持任意场景 |
| 负载均衡 | 无 | 内核自动分发 |
| 惊群效应 | 存在 | 基本消除 |
| 安全性 | 低(其他进程可劫持端口) | 高(需相同 UID) |
| Linux 支持 | 所有版本 | 3.9+ |
1.4 内核版本支持情况
这部分我直接给你列个清单,省得你去翻内核文档:
- SO_REUSEADDR:从 Linux 1.0 开始就支持了,所有版本都有
- SO_REUSEPORT:
- Linux 3.9:首次引入(TCP + UDP)
- Linux 4.5:修复了 TCP 下的负载均衡问题
- Linux 4.6:增加了 SO_ATTACH_REUSEPORT_CBPF 和 SO_ATTACH_REUSEPORT_EBPF
- Linux 5.0+:稳定可用,推荐使用
注意:如果你还在用 CentOS 6(内核 2.6.x),那 SO_REUSEPORT 就别想了。升级到 CentOS 7(内核 3.10+)才能用。我去年还碰到一个客户,生产环境是 CentOS 6.5,代码里用了 SO_REUSEPORT,编译没问题,运行时 setsockopt 直接返回 EINVAL。查了半天才发现是内核版本太老。
1.5 知识体系总览
下面这张图帮你理清本章的核心脉络:
1.6 选型建议
说了这么多,到底该用哪个?我根据实际经验给你几个判断标准:
- 单进程应用:只用 SO_REUSEADDR 就够了,解决重启时的端口占用问题
- 多进程/多线程高并发服务:优先考虑 SO_REUSEPORT,配合 SO_REUSEADDR 一起用
- 旧内核(3.9 以下):只能用 SO_REUSEADDR,多进程场景考虑 fork + 共享 listen fd
- UDP 多播场景:SO_REUSEADDR 是必须的,SO_REUSEPORT 可选
一个小技巧:我在写网络框架时,会先检查内核版本,然后动态决定是否启用 SO_REUSEPORT。这样既能享受新内核的性能优势,又能兼容旧环境。具体做法就是调用 uname() 获取版本号,或者直接尝试 setsockopt,如果返回 EINVAL 就降级到 SO_REUSEADDR 模式。