Я думаю, что моя проблема похожа на этот вопрос , на который так и не получил ответа. В моем случае у меня большое многопоточное серверное приложение, которое обменивается данными по протоколу 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
или похоже обойти это?