[英]Problems using UDP sockets bound to the same port on Windows
我認為我的問題類似於這個從未得到回答的問題。 就我而言,我有一個使用 UDP 進行通信的大型多線程服務器應用程序。 每個連接到服務器的客戶端都有自己的 UDP 套接字和線程。 在一個特定的用例中,將所有這些套接字綁定到相同的本地地址和端口,但連接到不同的目標地址是非常有利的。 在 macOS 和 Linux 上,這很好用,但在 Windows 上不起作用。
說明問題的最簡單方法是將其與 TCP 進行比較。 讓我們假設每個套接字都是一個 5 元組(protocol, src_addr, src_port, dst_addr, dst_port)
。 如果我在端口5000
上調用listen()
然后執行accept()
,我最終會遇到以下情況:
(TCP, 0.0.0.0, 5000, 0.0.0.0, 0)
(TCP, 10.1.1.1, 5000, 10.2.2.2, 12345)
這可以通過netstat
確認(這個在 mac 上,但 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 做同樣的事情,我通過設置SO_REUSEPORT
或SO_REUSEADDR
(取決於平台)來創建“監聽”套接字,然后調用bind()
。 “接受”套接字可以使用SO_REUSEPORT
、 bind()
,然后調用connect()
來設置其目標地址。 這給出了類似的情況:
(UDP, 0.0.0.0, 5000, 0.0.0.0, 0)
(UDP, 10.1.1.1, 5000, 10.2.2.2, 12345)
netstat
再次確認(在 Mac 上):
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 上,傳入的數據包會像使用 TCP 一樣進入“最佳”套接字。 但是在 Windows 上,數據包總是被分派到第一個套接字,完全忽略connect()
。 對connect()
的調用確實綁定到本地和外部地址(使用getsockname()
和getpeername()
),但即使是netstat
認為這兩個套接字是相同的:
Proto Local Address Foreign Address State
UDP 0.0.0.0:5000 *:*
UDP 0.0.0.0:5000 *:*
我覺得這很奇怪,因為它適用於其他平台,它適用於 TCP,並且netstat
不顯示真正的綁定地址。 就好像connect()
在更高級別上進行過濾一樣,它永遠不會深入到網絡驅動程序。
我意識到我可以只打開一個套接字並自己處理調度,但這很痛苦,因為目前有很多線程可以獨立運行,這會添加一個同步點,以及添加額外的緩沖層。 理想情況下,有一種簡單的方法可以說服 Windows 將 UDP 分派與其他所有內容一樣對待。
這是在 macOS (clang/llvm)、Linux (gcc) 和 Windows (Visual Studio) 上編譯和運行的最小工作示例。 為簡潔起見,省略了錯誤處理。 請注意,我明確使用了外部地址,因為localhost
是特殊的並且與我的用例無關。 示例代碼在本地端口 9999 上創建了 2 個套接字,其中一個還連接到端口 6666 上的硬編碼 IP 地址。然后我使用以下內容向每個套接字發送一個數據包:
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
或類似的解決方法?
#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 receiveFrom(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));
}
}
}
}
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 (recv(s, data, (int) sizeof(data), 0) > 0) {
printf("Socket %s got packet [%u].\n", name, (unsigned int)data[0]);
}
}
}
}
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);
receiveFrom(s, "listen");
connectSocket(s, sIpAddr, 6666);
// 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").
// 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, "accept");
receiveFrom(s2, "listen");
return 0;
}
測試命令行
echo 2 | nc -w 1 -p 6666 -u 10.211.55.9 9999 && echo 1 | nc -w 1 -u 10.211.55.9 9999 && echo 3 | nc -w 1 -p 6666 -u 10.211.55.9 9999
我遇到過這個問題。 MSDN 有關於connect()
說法:
對於無連接套接字(例如,類型 SOCK_DGRAM),connect 執行的操作只是建立一個默認目標地址,該地址可用於后續的 send/WSASend 和 recv/WSARecv 調用。 從指定的目標地址以外的地址接收的任何數據報都將被丟棄。 如果 name 指定的結構的地址成員填充為零,則套接字將斷開連接。 然后,默認遠程地址將是不確定的,因此 send/WSASend 和 recv/WSARecv 調用將返回錯誤代碼 WSAENOTCONN。 但是,sendto/WSASendTo 和 recvfrom/WSARecvFrom 仍然可以使用。 即使套接字已經連接,也可以通過再次調用 connect 來更改默認目標。 如果名稱與之前的連接不同,則任何排隊等待接收的數據報都將被丟棄。
所以我認為 udp 五元組會在第一次收到套接字時優先,所以我使用 udp 服務器套接字連接遠程地址,並創建另一個 udp 套接字作為 udp 服務器,這個工作正常
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.