4、端口复用实战:Nginx 端口复用配置、Apache 端口复用配置、自定义程序实现端口复用
理论讲完了,咱们得动真格的了。这一章我带你亲手配一遍端口复用。说实话,光看原理不落地,等于白学。我当年刚接触这个技术时,也是对着文档折腾了一下午才跑通。今天我把踩过的坑都给你标出来,你照着做就行。
4.1 Nginx 端口复用配置
Nginx 做端口复用,核心思路就是利用 SO_REUSEPORT 让多个 worker 进程共享同一个监听端口。嗯,这里要注意,Nginx 从 1.9.1 版本开始才原生支持这个特性。
4.1.1 配置示例
打开你的 nginx.conf,在 http 块或 server 块里加上这么一段:
events {
worker_connections 1024;
use epoll;
multi_accept on;
reuse_port on; # 开启端口复用
}
http {
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80 reuseport; # 关键:reuseport 参数
server_name example.com;
location / {
proxy_pass http://backend;
}
}
}
我个人习惯在 listen 指令后面直接加 reuseport,这样更直观。你启动 Nginx 后,用 ss -tlnp 看一下,会发现多个 worker 进程都绑在同一个 80 端口上。
events 块里设置 reuse_port on,效果一样。不过我还是建议你升级到新版,省心。
4.1.2 验证配置
配置完重启 Nginx:
nginx -t # 检查配置语法
nginx -s reload # 热加载配置
然后看进程状态:
ss -tlnp | grep :80
# 输出示例:
# LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6),("nginx",pid=1235,fd=6),("nginx",pid=1236,fd=6))
看到多个 pid 共享同一个 fd 6 了吧?这就是端口复用生效了。我曾经遇到过一个问题:配置完重启后只有一个 worker 在监听。查了半天才发现是内核版本太低(3.9 以下),SO_REUSEPORT 压根不支持。所以,先确认内核版本:uname -r。
4.2 Apache 端口复用配置
Apache 的端口复用配置稍微绕一点。它不像 Nginx 那样直接在 listen 指令上加参数,而是通过 mpm_event 或 mpm_worker 模块配合 SO_REUSEPORT 实现。
4.2.1 配置步骤
第一步,确认你用的是 event 或 worker MPM:
httpd -V | grep "Server MPM"
# 输出:Server MPM: event
第二步,编辑 httpd.conf,在 Listen 指令后加上 reuseport:
Listen 80 reuseport
<VirtualHost *:80>
DocumentRoot "/var/www/html"
ServerName example.com
</VirtualHost>
等等,这里有个坑。Apache 的 reuseport 参数在 2.4.31 版本才引入。如果你用的是更早的版本,得用另一种方式——通过 mod_so 加载一个自定义模块来设置 socket 选项。说实话,我不推荐这么干,太折腾了。
reuseport 在 Windows 上不支持。如果你在 Windows 上开发,别浪费时间了,直接上 Linux 或 macOS。
4.2.2 多进程验证
重启 Apache 后,用同样的命令检查:
ss -tlnp | grep :80
# 输出示例:
# LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("httpd",pid=2345,fd=4),("httpd",pid=2346,fd=4),("httpd",pid=2347,fd=4))
你想想看,如果没有端口复用,Apache 的每个子进程会各自监听不同的端口,或者通过父进程转发。有了 SO_REUSEPORT,内核直接帮你做负载均衡,性能提升很明显。我在一次压测中看到,开启端口复用后,Apache 的吞吐量提升了约 30%。
4.3 自定义程序实现端口复用
有时候,现成的服务器软件满足不了你的需求。比如你想写一个高性能的代理服务器,或者一个自定义协议的服务。这时候就得自己动手了。别怕,代码其实很简单。
4.3.1 C 语言实现
核心就三步:创建 socket、设置 SO_REUSEPORT、绑定端口。我给你一个完整的示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd;
int optval = 1;
struct sockaddr_in addr;
// 1. 创建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置 SO_REUSEPORT(关键!)
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT,
&optval, sizeof(optval)) < 0) {
perror("setsockopt");
close(sockfd);
exit(1);
}
// 3. 绑定端口
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
close(sockfd);
exit(1);
}
// 4. 监听
if (listen(sockfd, 10) < 0) {
perror("listen");
close(sockfd);
exit(1);
}
printf("进程 %d 已绑定端口 8888(端口复用已开启)\n", getpid());
// 5. 接受连接
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(sockfd,
(struct sockaddr*)&client_addr,
&client_len);
if (client_fd > 0) {
printf("进程 %d 处理新连接\n", getpid());
close(client_fd);
}
}
close(sockfd);
return 0;
}
编译运行:
gcc -o reuse_server reuse_server.c
./reuse_server & # 启动第一个实例
./reuse_server & # 启动第二个实例(同一个端口!)
你会发现两个进程都绑在 8888 端口上。这就是端口复用的魔力。
setsockopt 的 SO_REUSEPORT 选项必须在 bind 之前设置。顺序错了,复用就失效了。我刚开始写的时候就把顺序搞反了,查了半天才反应过来。
4.3.2 Python 实现
如果你不想用 C,Python 也能搞定。不过 Python 的 socket 模块默认不暴露 SO_REUSEPORT,得用 setsockopt 手动设置:
import socket
import os
import time
def create_reuse_server(port=8888):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置 SO_REUSEPORT
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('0.0.0.0', port))
sock.listen(10)
sock.setblocking(False)
print(f"进程 {os.getpid()} 已绑定端口 {port}")
while True:
try:
conn, addr = sock.accept()
print(f"进程 {os.getpid()} 处理来自 {addr} 的连接")
conn.close()
except BlockingIOError:
time.sleep(0.1)
if __name__ == '__main__':
create_reuse_server()
运行方式一样:
python3 reuse_server.py &
python3 reuse_server.py &
说白了,不管用什么语言,核心就是那一个 socket 选项。你掌握了这个,就能在任何地方实现端口复用。
4.4 三种实现方式对比
我把这三种方式放在一起对比一下,方便你选型:
| 实现方式 | 配置复杂度 | 性能 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| Nginx | 低(一行配置) | 高(原生支持) | 中(受限于配置) | Web 服务器、反向代理 |
| Apache | 中(需注意版本) | 中(依赖 MPM) | 低(配置较死板) | 传统 Web 服务 |
| 自定义程序 | 高(需编码) | 最高(可优化) | 高(完全可控) | 高性能代理、自定义协议 |
我个人建议:如果是常规 Web 服务,直接用 Nginx,省心又高效。如果你在做一些底层网络开发,比如写网关或负载均衡器,那就自己写代码,灵活性更高。
4.5 端口复用流程图
下面这张图展示了端口复用的完整流程,从配置到内核处理:
这张图把整个流程串起来了。你从配置阶段开始,经过启动阶段的多进程绑定,再到运行阶段的内核负载均衡,每一步都很清晰。说白了,端口复用就是让内核帮你做调度,你只管写业务逻辑。
4.6 避坑指南
最后,我把自己踩过的坑总结一下,你遇到类似问题可以直接翻到这里:
- 内核版本太低: Linux 3.9 以下不支持
SO_REUSEPORT。先uname -r确认。 - 顺序搞反:
setsockopt必须在bind之前。我刚开始写 C 程序时就犯过这个错。 - 防火墙干扰: 有些 iptables 规则会拦截端口复用。检查
iptables -L -n。 - SELinux 限制: 如果配置了 SELinux,可能需要调整策略。查看
audit.log确认。 - Windows 不支持:
SO_REUSEPORT是 Linux 特有的。Windows 上别想了。