MacOS的SO_REUSEADDR/SO_REUSEPORT与Linux不一致?

6
考虑以下代码:
#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);
}

非常简单:

  • 创建套接字
  • 设置SO_REUSEADDR
  • 设置SO_REUSEPORT
  • 绑定到本地端口23456
  • 连接到端口12345上的::1

在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

根据SO_REUSEADDR和SO_REUSEPORT有何不同?,这适用于BSD:

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

所以这就解释了为什么这不起作用。但是,在Linux上为什么会起作用就不太合理了?

理想情况下,我当然希望这在MacOS上能够工作,但我目前感觉可能不可能 - 但我仍然希望了解Linux是如何做到的。

1个回答

5
是的,Linux实现与大多数其他操作系统不同。您可以在这里找到详尽的解释。引用其中的特定部分:
Linux 3.9还将选项SO_REUSEPORT添加到Linux中。此选项的行为与BSD中的选项完全相同,只要所有套接字在绑定之前都设置了此选项,就可以绑定到完全相同的地址和端口号。
然而,在其他系统上仍有两个与SO_REUSEPORT不同的地方:
1.为防止"端口劫持",有一个特殊的限制:想要共享相同地址和端口组合的所有套接字必须属于共享相同有效用户ID的进程!因此,一个用户不能"窃取"另一个用户的端口。这是一些特殊魔法,以在缺少SO_EXCLBIND/SO_EXCLUSIVEADDRUSE标志的情况下进行补偿。
2.另外,内核对SO_REUSEPORT套接字执行一些在其他操作系统中找不到的"特殊魔法":对于UDP套接字,它尝试均匀地分布数据报;对于TCP监听套接字,它会尝试将传入的连接请求(通过调用accept()接受的请求)均匀地分布在共享相同地址和端口组合的所有套接字上。因此,一个应用程序可以轻松地在多个子进程中打开相同的端口,然后使用SO_REUSEPORT来获得非常廉价的负载平衡。

1
我仔细阅读了整篇文章,非常有启发性。它解释了为什么在BSD上connect()会失败(我认为MacOS可能也因为同样的原因而失败)。然而,我仍然不明白Linux是如何做到的,因为BSD已经清楚地说明这是不可能的。我已经更新了帖子以反映这一点。 - lukash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接