簡體   English   中英

在 Windows 上使用綁定到同一端口的 UDP 套接字的問題

[英]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_REUSEPORTSO_REUSEADDR (取決於平台)來創建“監聽”套接字,然后調用bind() “接受”套接字可以使用SO_REUSEPORTbind() ,然后調用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 如何處理這種調度以及是否有setsockoptWSAIoctl或類似的解決方法?

#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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM