MacOS SO_REUSEADDR/SO_REUSEPORT与Linux不一致?

azpvetkf  于 2023-05-16  发布在  Mac
关注(0)|答案(1)|浏览(126)

考虑以下代码:

#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

根据How do SO_REUSEADDR and SO_REUSEPORT differ?,这适用于BSD:
因此,如果您将相同协议的两个套接字绑定到相同的源地址和端口,并尝试将它们连接到相同的目的地址和端口,则connect()实际上会失败,并对您尝试连接的第二个套接字显示错误EADDRINUSE,这意味着具有相同五值元组的套接字已经连接。
这就解释了为什么它不起作用。如果这在Linux上实际上是可行的,那么什么是没有意义的呢?
我当然希望在MacOS上完成这项工作,但我目前觉得这可能是不可能的--然而,我仍然想了解Linux是如何做到这一点的。

fnvucqvd

fnvucqvd1#

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

相关问题