UDP多端口负载均衡实战

cpp 复制代码
// udp_reuseaddr_multi.cpp
// 编译: g++ -std=c++11 -pthread udp_reuseaddr_multi.cpp -o udp_multi
// 运行: ./udp_multi <num_sockets> [reuseport]
// 参数:
//   num_sockets : 要创建的 UDP socket 数量
//   reuseport   : 可选,若为 1 则启用 SO_REUSEPORT,默认 0(不启用)
// 示例:
//   ./udp_multi 5        # 只启用 SO_REUSEADDR,数据全流向最后一个 socket
//   ./udp_multi 5 1      # 同时启用 SO_REUSEPORT,数据按四元组哈希分发,每个包不同源端口

#include <iostream>
#include <thread>
#include <vector>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int PORT = 8888;
const int BUFFER_SIZE = 1024;

// 创建一个UDP socket,绑定到INADDR_ANY:PORT,根据参数启用选项
int create_and_bind(bool reuseport_flag) {
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        perror("socket");
        return -1;
    }

    int opt = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt SO_REUSEADDR");
        close(sock);
        return -1;
    }

    if (reuseport_flag) {
        if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
            perror("setsockopt SO_REUSEPORT");
            close(sock);
            return -1;
        }
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sock);
        return -1;
    }

    return sock;
}

// 接收线程:不断打印收到的数据
void receive_loop(int sock, const std::string& name) {
    char buffer[BUFFER_SIZE];
    struct sockaddr_in src_addr;
    socklen_t addr_len = sizeof(src_addr);

    while (true) {
        ssize_t n = recvfrom(sock, buffer, BUFFER_SIZE - 1, 0,
                             (struct sockaddr*)&src_addr, &addr_len);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << "[" << name << "] received from "
                      << inet_ntoa(src_addr.sin_addr) << ":"
                      << ntohs(src_addr.sin_port) << " -> " << buffer << std::endl;
        } else if (n == 0) {
            std::cout << "[" << name << "] connection closed?" << std::endl;
            break;
        } else {
            perror("recvfrom");
            break;
        }
    }
}

// 发送线程:每次发送都创建一个新的临时 socket(自动获得不同的源端口)
void sender_loop() {
    struct sockaddr_in dest;
    memset(&dest, 0, sizeof(dest));
    dest.sin_family = AF_INET;
    dest.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr);

    for (int i = 0; ; ++i) {
        int send_sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (send_sock < 0) {
            perror("sender socket");
            return;
        }

        std::string msg = "self_msg_" + std::to_string(i);
        ssize_t sent = sendto(send_sock, msg.c_str(), msg.size(), 0,
                              (struct sockaddr*)&dest, sizeof(dest));
        if (sent < 0) {
            perror("sendto");
        }
        close(send_sock);  // 关闭后下次循环会创建新socket,新源端口
        sleep(1);          // 每秒发送一条
    }
}

int main(int argc, char* argv[]) {
    if (argc < 2 || argc > 3) {
        std::cerr << "Usage: " << argv[0] << " <num_sockets> [reuseport]" << std::endl;
        std::cerr << "  reuseport: 0 (default) or 1" << std::endl;
        return 1;
    }
    int num_sockets = atoi(argv[1]);
    if (num_sockets <= 0 || num_sockets > 100) {
        std::cerr << "Error: num_sockets must be between 1 and 100." << std::endl;
        return 1;
    }

    bool reuseport_enabled = false;
    if (argc == 3) {
        reuseport_enabled = (atoi(argv[2]) != 0);
    }

    std::cout << "=== UDP Socket Test ===" << std::endl;
    std::cout << "Number of sockets: " << num_sockets << std::endl;
    std::cout << "SO_REUSEPORT: " << (reuseport_enabled ? "ENABLED" : "DISABLED") << std::endl;
    std::cout << "=========================" << std::endl;

    std::vector<int> sockets;
    sockets.reserve(num_sockets);
    for (int i = 0; i < num_sockets; ++i) {
        int sock = create_and_bind(reuseport_enabled);
        if (sock < 0) {
            for (int fd : sockets) close(fd);
            return 1;
        }
        sockets.push_back(sock);
        std::cout << "Socket " << i << " created, fd=" << sock << std::endl;
    }
    std::cout << "=========================" << std::endl;
    std::cout << "Sending test messages every second with different source ports..." << std::endl;
    std::cout << "Observe which socket(s) receive the data." << std::endl;
    std::cout << "=========================" << std::endl;

    std::vector<std::thread> threads;
    for (int i = 0; i < num_sockets; ++i) {
        std::string name = "Socket" + std::to_string(i);
        threads.emplace_back(receive_loop, sockets[i], name);
    }

    std::thread sender(sender_loop);
    sender.detach();

    for (auto& t : threads) {
        t.join();
    }

    for (int fd : sockets) close(fd);
    return 0;
}

bash

复制代码
g++ -std=c++11 -pthread udp_reuseaddr_multi.cpp -o udp_multi

1️⃣ 仅 SO_REUSEADDR(默认)

bash

复制代码
./udp_multi 5

结果:所有数据包都被最后一个创建的 socket(Socket4)接收。

2️⃣ 启用 SO_REUSEPORT

bash

复制代码
./udp_multi 5 1

结果 :因为发送端每次使用不同的源端口 ,内核根据四元组哈希将数据包分发到不同的接收 socket。你会看到输出类似:

text

复制代码
[Socket2] received from 127.0.0.1:45678 -> self_msg_0
[Socket0] received from 127.0.0.1:45679 -> self_msg_1
[Socket4] received from 127.0.0.1:45680 -> self_msg_2
[Socket1] received from 127.0.0.1:45681 -> self_msg_3
...

(具体哪个 socket 收到由哈希决定,但多个 socket 都会出现)

🔍 原理说明

  • SO_REUSEPORT 会创建一个接收组 ,内核为每个到达的 UDP 数据包计算 (源IP, 源端口, 目标IP, 目标端口) 的哈希值,然后选择组内的一个 socket 递送。

  • 发送端每次新建 socket → 自动获得不同的临时源端口 → 每个数据包的四元组都不同 → 哈希结果分散到各个接收 socket,从而实现负载均衡

通过这个示例,你可以亲手验证 SO_REUSEPORT 的真正作用。

cpp 复制代码
// udp_reuseaddr_multi.cpp
// 编译: g++ -std=c++11 -pthread udp_reuseaddr_multi.cpp -o udp_multi
// 运行: ./udp_multi <num_sockets> [reuseport]
// 参数:
//   num_sockets : 要创建的 UDP socket 数量
//   reuseport   : 可选,若为 1 则启用 SO_REUSEPORT,默认 0(不启用)
// 示例:
//   ./udp_multi 5        # 只启用 SO_REUSEADDR,数据全流向最后一个 socket
//   ./udp_multi 5 1      # 同时启用 SO_REUSEPORT,数据按四元组哈希分发

#include <iostream>
#include <thread>
#include <vector>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int PORT = 8888;
const int BUFFER_SIZE = 1024;

// 创建一个UDP socket,绑定到INADDR_ANY:PORT,根据参数启用选项
// reuseport_flag: 1表示启用SO_REUSEPORT,0表示不启用
int create_and_bind(bool reuseport_flag) {
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        perror("socket");
        return -1;
    }

    // 设置 SO_REUSEADDR(总是启用)
    int opt = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt SO_REUSEADDR");
        close(sock);
        return -1;
    }

    // 根据参数决定是否启用 SO_REUSEPORT
    if (reuseport_flag) {
        if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
            perror("setsockopt SO_REUSEPORT");
            close(sock);
            return -1;
        }
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sock);
        return -1;
    }

    return sock;
}

// 接收线程:不断打印收到的数据
void receive_loop(int sock, const std::string& name) {
    char buffer[BUFFER_SIZE];
    struct sockaddr_in src_addr;
    socklen_t addr_len = sizeof(src_addr);

    while (true) {
        ssize_t n = recvfrom(sock, buffer, BUFFER_SIZE - 1, 0,
                             (struct sockaddr*)&src_addr, &addr_len);
        if (n > 0) {
            buffer[n] = '\0';
            std::cout << "[" << name << "] received from "
                      << inet_ntoa(src_addr.sin_addr) << ":"
                      << ntohs(src_addr.sin_port) << " -> " << buffer << std::endl;
        } else if (n == 0) {
            std::cout << "[" << name << "] connection closed?" << std::endl;
            break;
        } else {
            perror("recvfrom");
            break;
        }
    }
}

// 发送线程:向本机端口持续发送数据
void sender_loop() {
    int send_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (send_sock < 0) {
        perror("sender socket");
        return;
    }

    struct sockaddr_in dest;
    memset(&dest, 0, sizeof(dest));
    dest.sin_family = AF_INET;
    dest.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr);

    for (int i = 0; ; ++i) {
        std::string msg = "self_msg_" + std::to_string(i);
        ssize_t sent = sendto(send_sock, msg.c_str(), msg.size(), 0,
                              (struct sockaddr*)&dest, sizeof(dest));
        if (sent < 0) {
            perror("sendto");
        }
        sleep(1);  // 每秒发送一条
    }
    close(send_sock);
}

int main(int argc, char* argv[]) {
    // 解析命令行参数
    if (argc < 2 || argc > 3) {
        std::cerr << "Usage: " << argv[0] << " <num_sockets> [reuseport]" << std::endl;
        std::cerr << "  reuseport: 0 (default) or 1" << std::endl;
        return 1;
    }
    int num_sockets = atoi(argv[1]);
    if (num_sockets <= 0) {
        std::cerr << "Error: num_sockets must be a positive integer." << std::endl;
        return 1;
    }
    if (num_sockets > 100) {
        std::cerr << "Error: num_sockets too large (max 100)." << std::endl;
        return 1;
    }

    bool reuseport_enabled = false;
    if (argc == 3) {
        reuseport_enabled = (atoi(argv[2]) != 0);
    }

    std::cout << "=== UDP Socket Test ===" << std::endl;
    std::cout << "Number of sockets: " << num_sockets << std::endl;
    std::cout << "SO_REUSEPORT: " << (reuseport_enabled ? "ENABLED" : "DISABLED") << std::endl;
    std::cout << "=========================" << std::endl;

    // 动态创建socket
    std::vector<int> sockets;
    sockets.reserve(num_sockets);
    for (int i = 0; i < num_sockets; ++i) {
        int sock = create_and_bind(reuseport_enabled);
        if (sock < 0) {
            for (int fd : sockets) close(fd);
            return 1;
        }
        sockets.push_back(sock);
        std::cout << "Socket " << i << " created, fd=" << sock << std::endl;
    }
    std::cout << "=========================" << std::endl;
    std::cout << "Sending test messages every second..." << std::endl;
    std::cout << "Observe which socket(s) receive the data." << std::endl;
    std::cout << "=========================" << std::endl;

    // 启动接收线程
    std::vector<std::thread> threads;
    for (int i = 0; i < num_sockets; ++i) {
        std::string name = "Socket" + std::to_string(i);
        threads.emplace_back(receive_loop, sockets[i], name);
    }

    // 启动发送线程(分离)
    std::thread sender(sender_loop);
    sender.detach();

    // 等待接收线程结束(实际不会结束)
    for (auto& t : threads) {
        t.join();
    }

    // 清理(不会执行到这里)
    for (int fd : sockets) close(fd);
    return 0;
}
相关推荐
叼烟扛炮2 小时前
C++ 知识点08 类与对象
开发语言·c++·算法·类和对象
楼田莉子2 小时前
仿Muduo的高并发服务器:Http协议模块
linux·服务器·c++·后端·学习
tjl521314_219 小时前
04C++ 名称空间(Namespace)
开发语言·c++
ximu_polaris9 小时前
设计模式(C++)-行为型模式-备忘录模式
c++·设计模式·备忘录模式
tankeven14 小时前
C++ 智能指针
c++
handler0116 小时前
【算法模板】最小生成树:稠密图选 Prim,稀疏图选 Kruskal
c语言·数据结构·c++·算法
许长安16 小时前
RPC 异步调用基本使用方法:基于官方helloworld-async 示例
c++·经验分享·笔记·rpc
sparEE17 小时前
c++面向对象:对象的赋值
开发语言·c++
此生决int17 小时前
快速复习之数据结构篇——栈和队列
数据结构·c++