Проблемы с использованием сокетов UDP, привязанных к одному и тому же порту на Windows - PullRequest
0 голосов
/ 17 января 2020

Я думаю, что моя проблема похожа на этот вопрос , на который так и не получил ответа. В моем случае у меня большое многопоточное серверное приложение, которое обменивается данными по протоколу UDP. Каждый клиент, который подключается к серверу, получает свой собственный сокет UDP и поток. В одном конкретном случае очень полезно, чтобы все эти сокеты были связаны с одним и тем же локальным адресом и портом, но были связаны с разными адресами назначения. На macOS и Linux это прекрасно работает, но на Windows.

это не работает. Самый простой способ проиллюстрировать проблему - сравнить ее с TCP. Давайте предположим, что каждый сокет является 5-кортежным (protocol, src_addr, src_port, dst_addr, dst_port). Если я звоню listen() через порт 5000, а затем выполняю accept(), я получаю что-то вроде следующей ситуации:

  • прослушивающий сокет: (TCP, 0.0.0.0, 5000, 0.0.0.0, 0)
  • принимаем сокет: (TCP, 10.1.1.1, 5000, 10.2.2.2, 12345)

, который можно подтвердить с помощью netstat (этот на ма c, но Windows выглядит аналогично):

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)    
tcp4       0      0  192.168.1.220.5000     192.168.1.220.65282    ESTABLISHED
tcp4       0      0  *.5000                 *.*                    LISTEN     

Когда входящие пакеты принимаются, они передаются в «лучший» совпадающий сокет и возвращаются в прослушивающий сокет, если лучшего совпадения нет. Это происходит на всех платформах.

Чтобы сделать то же самое с UDP, я создаю сокет "listen", устанавливая SO_REUSEPORT или SO_REUSEADDR (в зависимости от платформы), затем вызывая bind(). Сокет «принять» можно сделать, используя SO_REUSEPORT, bind(), затем вызывая connect(), чтобы установить адрес получателя. Что дает аналогичную ситуацию:

  • сокет "listen": (UDP, 0.0.0.0, 5000, 0.0.0.0, 0)
  • сокет "accept": (UDP, 10.1.1.1, 5000, 10.2.2.2, 12345)

снова подтверждено netstat (на ма c):

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)    
udp4       0      0  10.211.55.2.5000       10.211.55.2.6666                  
udp4       0      0  *.5000                 *.* 

На macOS и Linux, входящие пакеты go в «лучший» сокет, как и в TCP. Но в Windows пакеты всегда отправляются в первый сокет, полностью игнорируя connect(). Вызов connect() действительно связывался как с локальным, так и с внешним адресом (проверяется с помощью getsockname() и getpeername()), но даже netstat считает, что 2 сокета одинаковы:

  Proto  Local Address          Foreign Address        State
  UDP    0.0.0.0:5000           *:*
  UDP    0.0.0.0:5000           *:*

Я нахожу это странным, поскольку он работает на других платформах, работает с TCP, а netstat не показывает реальные связанные адреса. Похоже, что connect() выполняет фильтрацию на более высоком уровне и никогда не переходит к сетевым драйверам.

Я понимаю, что могу просто открыть один сокет и обработать диспетчеризацию самостоятельно, но это боль, потому что есть много потоков, которые в настоящее время могут действовать независимо и которые добавили бы одну точку syn c, а также добавили дополнительный слой буферизации. В идеале был бы простой способ убедить Windows просто обработать UDP-диспетчеризацию так же, как и все остальное.

Вот минимальный рабочий пример, который компилируется и работает на macOS (clang / llvm), Linux (g cc) и Windows (Visual Studio). Обработка ошибок была опущена для краткости. Обратите внимание, что я явно использую внешний адрес, поскольку localhost является особенным и не относится к моему сценарию использования. Пример кода создает 2 сокета на локальном порту 9999, один из которых также подключен к жестко закодированному IP-адресу на порту 6666. Затем я отправляю пакет на каждый сокет, используя что-то вроде:

echo 1 | nc -w 1 -u 10.211.55.9 9999 && echo 2 | nc -w 1 -p 6666 -u 10.211.55.9 9999

Вкл Linux и macOS это дает:

Socket listen got packet [49] from [0ad33702:56651].
Socket accept got packet [50] from [0ad33702:6666].

но на Windows я получаю:

Socket listen got packet [49] from [0ad33702:58940].
Socket listen got packet [50] from [0ad33702:6666].

Вот программа C. Там немного подковы, но, надо надеяться, main() должно быть самоочевидным:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#ifdef _WIN32
#include <ws2tcpip.h>
typedef SOCKET SocketHandle;
static int poll(struct pollfd *fds, unsigned long nfds, int timeout) { return WSAPoll(fds, nfds, timeout); } // rename WSAPoll to poll
#pragma comment(lib, "Ws2_32.lib")
#else
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
typedef int SocketHandle;
#endif

// Explicitly send data to an external IP address, not localhost. In this case 10.211.55.2. Change as needed.
static uint32_t sIpAddr = 10u<<24 | 211u<<16 | 55u<<8 | 2u;

static void setReusePort(SocketHandle s)
{
    int trueval = 1;
    #ifdef SO_REUSEPORT
    setsockopt(s, SOL_SOCKET, SO_REUSEPORT, (const char*) &trueval, sizeof(trueval));
    #else
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char*) &trueval, sizeof(trueval));
    #endif
}

static void bindSocket(SocketHandle s, uint32_t addr, uint16_t port)
{
    struct sockaddr_in sourceAddr;
    memset(&sourceAddr, 0, sizeof(sourceAddr));
    sourceAddr.sin_family = AF_INET;
    sourceAddr.sin_addr.s_addr = htonl(addr);
    sourceAddr.sin_port = htons(port);
#ifdef __APPLE__
    sourceAddr.sin_len = sizeof(sourceAddr);
#endif
    bind(s, (const struct sockaddr*) &sourceAddr, sizeof(sourceAddr));
}

static void connectSocket(SocketHandle s, uint32_t addr, uint16_t port)
{
    struct sockaddr_in peerAddr;
    memset(&peerAddr, 0, sizeof(peerAddr));
    peerAddr.sin_family = AF_INET;
    peerAddr.sin_addr.s_addr = htonl(addr);
    peerAddr.sin_port = htons(port);
#ifdef __APPLE__
    peerAddr.sin_len = sizeof(addr);
#endif
    connect(s, (const struct sockaddr*) &peerAddr, sizeof(peerAddr));
}

static void receive(SocketHandle s, const char* name)
{
    struct sockaddr_in retAddr;
    memset(&retAddr, 0, sizeof(retAddr));
    retAddr.sin_family = AF_INET;
#ifdef __APPLE__
    retAddr.sin_len = sizeof(retAddr);
#endif
    socklen_t retAddrLen = sizeof(retAddr);

    // Recv up to 10 packets over 10 seconds.
    for (int i = 0; i < 10; ++i) {
        struct pollfd pollSet = {s, POLLRDNORM, 0};
        int r = poll(&pollSet, 1, 1000);
        if (1 == r && POLLRDNORM == (pollSet.revents & POLLRDNORM)) {
            char data[256];
            if (recvfrom(s, data, (int) sizeof(data), 0, (struct sockaddr*) &retAddr, &retAddrLen) > 0) {
                printf("Socket %s got packet [%u] from [%8.8x:%u].\n", name, (unsigned int)data[0], ntohl(retAddr.sin_addr.s_addr), ntohs(retAddr.sin_port));
            }
        }
    }
}

int main()
{
#ifdef _WIN32
    WSADATA data;
    WSAStartup(MAKEWORD(2, 2), &data);
#endif

    // Create a UDP socket on port 9999 (essentially the "listen" socket).
    SocketHandle s = socket(AF_INET, SOCK_DGRAM, 0);
    setReusePort(s);
    bindSocket(s, INADDR_ANY, 9999);

    // At this point, all incoming packets on port 9999 are delivered to `s`.

    // Create another UDP socket on port 9999 (will be the "accept" socket).
    SocketHandle s2 = socket(AF_INET, SOCK_DGRAM, 0);
    setReusePort(s2);
    bindSocket(s2, INADDR_ANY, 9999);

    // At this point, all incoming packets on port 9999 are still delivered to `s`.

    // Restrict `s2` to only communicate with a particular address ("accept").
    connectSocket(s2, sIpAddr, 6666);

    // At this point, things are different between platforms:
    // Linux/BSD: packets from 10.211.55.2:6666 are delivered to `s2`, everything else to `s`
    // Windows: all packets still delivered to `s`
    receive(s, "listen");
    receive(s2, "accept");

    return 0;
}

Кто-нибудь знает, как Windows справляется с этой диспетчеризацией и есть ли setsockopt или WSAIoctl или похоже обойти это?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...