首页 > 解决方案 > MacOS SO_REUSEADDR/SO_REUSEPORT 与 Linux 不一致?

问题描述

考虑这段代码:

#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

#define SERVADDR "::1"
#define PORT 12345

int main() {
    int sd = -1;

    if ((sd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
        fprintf(stderr, "socket() failed: %d", errno);
        exit(1);
    }

    int flag = 1;
    if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) == -1) {
        fprintf(stderr, "Setsockopt %d, SO_REUSEADDR failed with errno %d\n", sd, errno);
        exit(2);
    }
    if(setsockopt(sd, SOL_SOCKET, SO_REUSEPORT, &flag, sizeof(flag)) == -1) {
        fprintf(stderr, "Setsockopt %d, SO_REUSEPORT failed with errno %d\n", sd, errno);
        exit(3);
    }

    struct sockaddr_in6 addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(23456);

    if(bind(sd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        fprintf(stderr, "Bind %d failed with errno %d: %s\n", sd, errno, strerror(errno));
        exit(4);
    }

    struct sockaddr_in6 server_addr;
    memset(&server_addr, 0, sizeof(server_addr));

    server_addr.sin6_family = AF_INET6;
    inet_pton(AF_INET6, SERVADDR, &server_addr.sin6_addr);
    server_addr.sin6_port = htons(PORT);

    if (connect(sd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        fprintf(stderr, "Connect %d failed with errno %d: %s\n", sd, errno, strerror(errno));
        exit(5);
    }

    printf("Seems like it worked this time!\n");
    close(sd);
}

很简单:

奇怪的是,在 MacOS 上连续运行会导致:

$ for i in {1..5}; do ./ipv6; done
Seems like it worked this time!
Connect 3 failed with errno 48: Address already in use
Connect 3 failed with errno 48: Address already in use
Connect 3 failed with errno 48: Address already in use
Connect 3 failed with errno 48: Address already in use
$

在 Linux 上运行它似乎工作得很好:

$ for i in {1..5}; do ./ipv6; done
Seems like it worked this time!
Seems like it worked this time!
Seems like it worked this time!
Seems like it worked this time!
Seems like it worked this time!
$

我在端口上有一个监听器12345

$ nc -6 -l -v -p12345 -k

这不仅限于 IPv6,尝试了与 IPv4 相同的事情 - 相同的行为。

谁能解释一下?

我以前认为它失败了,bind()但它在connect().

编辑#1

据此- 适用于 BSD

因此,如果您将相同协议的两个套接字绑定到相同的源地址和端口,并尝试将它们都连接到相同的目标地址和端口,connect()实际上将失败并出现EADDRINUSE您尝试连接的第二个套接字的错误,这意味着一个具有五个值的相同元组的套接字已经连接。

所以这是有道理的,为什么这不起作用。如果这怎么可能在 Linux 上实际运行,那有什么没有意义的呢?

理想情况下,我当然会在 MacOS 上完成这项工作,但我目前觉得这可能是不可能的——但我仍然想了解 Linux 是如何做到的。

标签: cmacossocketsraw-sockets

解决方案


是的,Linux 实现与大多数其他操作系统不同。您可以在此处找到详尽的解释。引用特定部分:

Linux 3.9 也向 Linux 添加了选项 SO_REUSEPORT。此选项的行为与 BSD 中的选项完全相同,只要所有套接字在绑定之前都设置了此选项,就允许绑定到完全相同的地址和端口号。

然而,在其他系统上与 SO_REUSEPORT 仍有两个不同之处:

  1. 为了防止“端口劫持”,有一个特殊的限制:所有想要共享相同地址和端口组合的套接字必须属于共享相同有效用户 ID 的进程!所以一个用户不能“窃取”另一个用户的端口。这是一些特殊的魔法,可以在一定程度上弥补丢失的 SO_EXCLBIND/SO_EXCLUSIVEADDRUSE 标志。
  2. 此外,内核对 SO_REUSEPORT 套接字执行了一些在其他操作系统中没有的“特殊魔法”:对于 UDP 套接字,它尝试均匀地分发数据报,对于 TCP 侦听套接字,它尝试分发传入的连接请求(那些通过调用accept()) 均匀地分布在所有共享相同地址和端口组合的套接字上。因此,一个应用程序可以很容易地在多个子进程中打开同一个端口,然后使用 SO_REUSEPORT 来获得非常便宜的负载平衡。

推荐阅读