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 知识体系总览

下面这张图帮你理清本章的核心脉络:

端口复用知识体系 端口复用知识体系 端口复用 SO_REUSEADDR SO_REUSEPORT TIME_WAIT 重用 多播/广播场景 多进程共享端口 内核负载均衡 SO_REUSEPORT 需要 Linux 3.9+ 生产环境建议使用 4.6+ 内核

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 模式。