端口复用

一、端口复用解决了什么问题?

在讲解具体用法前,先明确端口复用的核心价值 —— 它主要解决两类高频的 “端口占用” 问题:

问题 1:服务重启时提示 “Address already in use”

默认情况下,当你关闭一个 TCP 服务端(比如 epoll 服务)后,端口不会立即释放,而是会进入 TIME_WAIT 状态(通常持续 2-4 分钟)。此时如果立即重启服务,内核会提示:

1
bind failed: Address already in use

这是因为内核认为该端口还被 “旧连接” 占用,不允许新进程绑定。

问题 2:多进程 / 多线程绑定同一端口(如负载均衡)

某些场景下(比如 Nginx 多进程、多线程服务),需要让多个进程 / 线程同时监听同一个端口,默认情况下内核会拒绝,而端口复用可以允许这种操作。

二、端口复用的核心原理

端口复用的核心是设置套接字选项 SO_REUSEADDR,它会修改内核对 TCP 端口的绑定规则:

  1. 允许绑定处于 TIME_WAIT 状态的端口:跳过 “端口被 TIME_WAIT 连接占用” 的检查;
  2. 允许多个套接字绑定同一端口(需满足条件):多个进程 / 线程的套接字都设置 SO_REUSEADDR 后,可绑定同一端口(最终由内核做连接分发)。

补充:TIME_WAIT 状态的背景

TCP 连接关闭时,主动关闭的一方会进入 TIME_WAIT 状态,目的是:

  • 确保对方收到最后的 FIN 包(避免丢包导致连接残留);

  • 防止旧连接的延迟数据包被新连接接收。

    默认超时时间是 2*MSL(MSL 是报文最大生存时间,Linux 下默认 60 秒,所以 TIME_WAIT 是 120 秒)。

三、端口复用的代码实现(核心步骤)

设置端口复用的代码通常在 创建套接字后、绑定端口前 执行,核心函数是 setsockopt(),以下是完整步骤:

1. 核心 API:setsockopt ()

1
2
3
4
#include <sys/socket.h>

// 设置套接字选项
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数拆解(针对端口复用)
参数 取值(端口复用) 含义
sockfd 套接字描述符 刚创建的监听套接字(lfd)
level SOL_SOCKET 套接字层级(通用选项,而非 TCP/UDP 层级)
optname SO_REUSEADDR 要设置的选项名(端口复用核心)
optval int* 类型 指向整数的指针:1 表示开启,0 表示关闭
optlen sizeof(int) optval 的长度

2. 完整实现代码(结合 epoll 服务端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <string.h>

#define PORT 9999

int main() {
// 1. 创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 2. 核心:设置端口复用(创建套接字后,绑定前执行)
int opt = 1; // 1=开启,0=关闭
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt SO_REUSEADDR failed");
exit(EXIT_FAILURE);
}
printf("端口复用已开启\n");

// 3. 绑定IP和端口(此时即使端口在TIME_WAIT,也能绑定成功)
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡

if (bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("绑定端口 %d 成功\n", PORT);

// 4. 监听
if (listen(lfd, 128) == -1) {
perror("listen failed");
exit(EXIT_FAILURE);
}

// 后续epoll相关代码(省略,和之前示例一致)
// ...

close(lfd);
return 0;
}

四、端口复用的扩展:SO_REUSEPORT(Linux 3.9+)

除了 SO_REUSEADDR,Linux 3.9 后还引入了 SO_REUSEPORT,它是更强大的端口复用选项,两者的核心区别:

特性 SO_REUSEADDR SO_REUSEPORT(Linux 3.9+)
核心作用 允许绑定 TIME_WAIT 端口;多进程绑定同一端口(需条件) 允许多个进程 / 线程独立绑定同一端口,内核均匀分发连接
连接分发 由第一个绑定的进程接收所有连接 内核将连接均匀分发给所有绑定的进程(负载均衡)
适用场景 服务重启、简单多进程绑定 高性能服务(如 Nginx、Redis 集群)
使用复杂度 低(只需设置 1 个选项) 高(需所有进程都设置该选项)

SO_REUSEPORT 的使用示例

1
2
3
// 开启 SO_REUSEPORT(需 Linux 3.9+)
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

五、端口复用的常见使用场景

  1. 服务快速重启:开发 / 测试阶段频繁重启服务,避免 TIME_WAIT 导致的端口占用;
  2. 生产环境服务重启:线上服务升级重启时,无需等待 2 分钟 TIME_WAIT 超时;
  3. 多进程 / 多线程服务:如 Nginx 多 worker 进程、多线程服务器,多个进程监听同一端口;
  4. 容器化部署:容器重启时,快速复用原有端口,避免端口占用导致容器启动失败。

六、避坑指南(关键注意事项)

坑 1:设置时机错误

  • 现象:设置了 SO_REUSEADDR 但仍提示 “Address already in use”;
  • 原因:在 bind() 之后才调用 setsockopt()
  • 解决:必须在 创建套接字后、绑定端口前 设置。

坑 2:混淆 SO_REUSEADDR 和 SO_REUSEPORT

  • 现象:多进程绑定同一端口,但只有一个进程接收连接;
  • 原因:用了 SO_REUSEADDR 而非 SO_REUSEPORT
  • 解决:高性能多进程场景用 SO_REUSEPORT(需 Linux 3.9+)。

坑 3:忽略 TIME_WAIT 的其他影响

  • 现象:端口复用开启后,新连接收到旧连接的延迟数据包;
  • 原因:TIME_WAIT 的核心目的是避免旧数据包干扰,端口复用跳过了这个检查;
  • 解决:生产环境可适当缩短 TIME_WAIT 超时(不推荐),或依赖 TCP 序列号过滤旧数据包(内核默认行为)。

坑 4:权限问题

  • 现象:普通用户设置端口复用后,绑定 1-1024 端口失败;
  • 原因:1-1024 是特权端口,普通用户无绑定权限;
  • 解决:用 sudo 运行程序,或修改内核参数 net.ipv4.ip_unprivileged_port_start

七、验证端口复用是否生效

方法 1:查看 TIME_WAIT 连接

1
2
3
4
# 查看端口 9999 的连接状态
netstat -anp | grep 9999
# 或用 ss(更高效)
ss -an | grep 9999

如果看到 TIME_WAIT 状态的连接,但服务仍能重启绑定该端口,说明端口复用生效。

方法 2:快速重启服务

  1. 启动服务 → 用 telnet 建立连接 → 关闭服务(此时端口进入 TIME_WAIT);
  2. 立即重启服务,如果能成功绑定端口,说明端口复用生效。

总结

  1. 核心作用:端口复用(SO_REUSEADDR)解决 TCP 端口因 TIME_WAIT 导致的占用问题,允许服务快速重启、多进程绑定同一端口;
  2. 使用方法:在 socket() 后、bind() 前调用 setsockopt() 设置 SO_REUSEADDR 为 1;
  3. 关键区别:SO_REUSEADDR 用于基础复用,SO_REUSEPORT(Linux 3.9+)用于高性能多进程负载均衡;
  4. 避坑关键:设置时机要正确(bind 前),区分两个复用选项的场景。