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 端口上。

小提示: 如果你用的是旧版 Nginx(1.9.1 之前),别慌。可以在 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 选项。说实话,我不推荐这么干,太折腾了。

注意: Apache 的 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 端口复用流程图

下面这张图展示了端口复用的完整流程,从配置到内核处理:

端口复用配置与运行流程图 配置阶段 启动阶段 运行阶段 Nginx: listen 80 reuseport Apache: Listen 80 reuseport 自定义: setsockopt(SO_REUSEPORT) ↓ 配置解析 进程1: socket → bind → listen 进程2: socket → bind → listen 进程N: socket → bind → listen ↓ 内核哈希表注册 新连接到达 内核负载均衡 选择最优进程 连接交付 内核 SO_REUSEPORT 处理 查找端口 → 获取复用组 → 负载均衡 → 交付连接 负载均衡算法 默认:哈希(源IP + 端口)→ 选定进程 可配置:随机、轮询(需内核补丁)

这张图把整个流程串起来了。你从配置阶段开始,经过启动阶段的多进程绑定,再到运行阶段的内核负载均衡,每一步都很清晰。说白了,端口复用就是让内核帮你做调度,你只管写业务逻辑。

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 上别想了。