【问题标题】:UDP connection responses not received by client客户端未收到 UDP 连接响应
【发布时间】:2019-07-18 03:51:56
【问题描述】:

我在实现 UDP 连接时遇到了麻烦,因为当我在 LAN 中尝试它时它可以工作,但是当 NAT 内部的某人尝试连接到公共服务器地址时,它会失败,因为从服务器发送的数据包作为响应,永远不会到达客户。

我的协议如下:

  1. 客户端 A 向服务器发送一个字节作为连接请求。
  2. 服务器 B 为客户端创建一个新套接字,并从那里响应一个字节到 recvfrom() 调用中报告的客户端端口。永远不会到达客户那里。

我也试过了:

为步骤 1 进行多次调用,每个调用发送一个字节。

为第 2 步进行多次调用,每个调用发送一个字节。

客户端代码:

#define GPK_CONSOLE_LOG_ENABLED
#include "gpk_stdsocket.h"
#include "gpk_sync.h"

int main() {
    ::gpk::tcpipInitialize();
    sockaddr_in         sa_server           = {AF_INET};
    while(true) {
//#define MAKE_IT_WORK
#if defined MAKE_IT_WORK
        ::gpk::tcpipAddressToSockaddr({{192,168,0,2}, 9898}, sa_server);
#else
        ::gpk::tcpipAddressToSockaddr({{201,235,131,233}, 9898}, sa_server);
#endif
        ::gpk::SIPv4        addrRemote          = {};
        SOCKET              handle              = socket(AF_INET, SOCK_DGRAM, 0);
        ree_if(INVALID_SOCKET == handle, "Failed to create socket.");
        sockaddr_in         sa_client           = {AF_INET};
        gpk_necall(::bind(handle, (sockaddr *)&sa_client, sizeof(sockaddr_in)), "Failed to bind listener to address");
        char                commandToSend       = '1';
        int                 sa_length           = sizeof(sa_server);
        gpk_necall(sendto(handle, (const char*)&commandToSend, (int)sizeof(char), 0, (sockaddr *)&sa_server, sa_length), "Failed to send connect request to server.");
        {
            sockaddr_in         sa_battery          = sa_server;
            for(uint32_t j=3; j < 3; ++j) {
            for(uint32_t i=16*1024; i < 64*1024; ++i) {
                sa_battery.sin_port = htons((u_short)i);
                gpk_necall(sendto(handle, (const char*)&commandToSend, (int)sizeof(char), 0, (sockaddr *)&sa_battery, sa_length), "Failed to send connect request to server.");
                //::gpk::sleep(1);
            }}
        }
        ::gpk::tcpipAddressFromSockaddr(sa_server, addrRemote);
        info_printf("Send connect request to server: %c to %u.%u.%u.%u:%u", commandToSend, GPK_IPV4_EXPAND(addrRemote));

        char                connectAcknowledge  = 0;
        sa_server.sin_port  = 0;
        gpk_necall(recvfrom(handle, (char *)&connectAcknowledge, (int)sizeof(char), 0, (sockaddr *)&sa_server, &sa_length), "Failed to receive response from server");
        addrRemote          = {};
        ::gpk::tcpipAddressFromSockaddr(sa_server, addrRemote);
        info_printf("Received connect response from server: %c from %u.%u.%u.%u:%u.", connectAcknowledge, GPK_IPV4_EXPAND(addrRemote));
        ::gpk::sleep(1000);
        gpk_safe_closesocket(handle);
    }
    ::gpk::tcpipShutdown();
    return 0;
}

服务器代码:

#define GPK_CONSOLE_LOG_ENABLED
#include "gpk_stdsocket.h"
#include "gpk_sync.h"

int main() { 
    ::gpk::tcpipInitialize();
    sockaddr_in             sa_server               = {};
    ::gpk::SIPv4            addrLocal               = {{}, 9898};
    ::gpk::tcpipAddress(0, 9898, 1, ::gpk::TRANSPORT_PROTOCOL_UDP, addrLocal);
    ::gpk::tcpipAddressToSockaddr(addrLocal, sa_server);

    SOCKET                  handle                  = socket(AF_INET, SOCK_DGRAM, 0);
    ree_if(INVALID_SOCKET == handle, "Failed to create socket.");
    gpk_necall(::bind(handle, (sockaddr *)&sa_server, sizeof(sockaddr_in)), "Failed to bind listener to address");
    info_printf("Server listening on %u.%u.%u.%u:%u.", GPK_IPV4_EXPAND(addrLocal));
    while(true) {
        ::gpk::SIPv4            addrLocalClient         = addrLocal;
        addrLocalClient.Port = 0;
        SOCKET                  clientHandle            = socket(AF_INET, SOCK_DGRAM, 0);

        sockaddr_in             sa_server_client        = {AF_INET};
        ::gpk::tcpipAddressToSockaddr(addrLocalClient, sa_server_client);
        gpk_necall(::bind(clientHandle, (sockaddr *)&sa_server_client, sizeof(sockaddr_in)), "Failed to bind listener to address");

        sockaddr_in             sa_client               = {AF_INET};
        int                     client_length           = sizeof(sa_client);
        char                    connectReceived         = 0;
        gpk_necall(::recvfrom(handle, (char*)&connectReceived, (int)sizeof(char), 0, (sockaddr*)&sa_client, &client_length), "Failed to receive connect request.");
        ::gpk::SIPv4            addrRemote;
        ::gpk::tcpipAddressFromSockaddr(sa_client, addrRemote);
        info_printf("Received connect request: %c from %u.%u.%u.%u:%u.", connectReceived, GPK_IPV4_EXPAND(addrRemote));

        char                    commandToSend           = '2';
        //::gpk::tcpipAddressFromSockaddr(sa_server, addrLocal);
        ::gpk::tcpipAddress(clientHandle, addrLocal);
        info_printf("Sending connect response %c from %u.%u.%u.%u:%u to %u.%u.%u.%u:%u.", commandToSend, GPK_IPV4_EXPAND(addrLocal), GPK_IPV4_EXPAND(addrRemote));
        ::gpk::sleep(10);
        ree_if(INVALID_SOCKET == clientHandle, "Failed to create socket.");
        for(uint32_t i=16*1024; i < 65535; ++i)
            gpk_necall(::sendto(clientHandle, (const char*)&commandToSend, (int)sizeof(char), 0, (sockaddr*)&sa_client, sizeof(sockaddr_in)), "Failed to respond.");
        info_printf("Sent connect response %c from %u.%u.%u.%u:%u to %u.%u.%u.%u:%u.", commandToSend, GPK_IPV4_EXPAND(addrLocal), GPK_IPV4_EXPAND(addrRemote));
        if(handle != clientHandle)
            gpk_safe_closesocket(clientHandle);
    }
    ::gpk::tcpipShutdown();
    return 0; 
}

注意:我将示例 udp_server 和 udp_client 项目保留在 https://github.com/asm128/gpk,以防您需要一个工作示例,通过取消注释 //#define MAKE_IT_WORK 并将您的 IP 地址替换为我的,有条件地编译工作和损坏的案例。

【问题讨论】:

  • 将您尝试的代码添加到问题中。没有所有不重要的部分。
  • 看来你自己解决了问题。服务器需要使用接收请求的同一个套接字发送响应。正如我之前提到的,要让 UDP 通过 NAT 工作,端口号必须在两个方向上匹配。
  • 另外,修正名称并不是为了让事情更容易理解,而是为了准确。计算机会完全按照您告诉他们的操作,所以如果那是您的真实代码,它可能已经解释了问题所在。此处发布的代码应该是您运行的显示您的问题的确切代码,否则您在浪费每个人的时间。根据提供帮助的人的专业知识,暗示问题可能出在您的代码中并不是侮辱性的。
  • @PabloAriel 除了您最初发布的消息外,您尝试同时使用新套接字和现有套接字,但两种方法都失败了。
  • UDP 是无连接的,因此您必须为所有通信使用单个套接字,以便服务器端口号保持不变。它与 TCP 不同。 TCP 套接字由本地 IP/端口和远程 IP/端口定义,而 UDP 套接字仅由本地 IP/端口定义。

标签: c++ c sockets udp nat


【解决方案1】:

您的响应没有返回给客户端,因为您使用单独的套接字发回响应。此套接字与从客户端接收数据包的套接字具有不同的本地端口号,因此对于 NAT,它似乎来自不同的源,因此不会被转发。

当 UDP 数据报退出 NAT 时,NAT 会跟踪目标 IP 和端口以及 NAT 使用的本地 IP 和端口,并将其与内部网络上的原始源 IP 和端口匹配。传入数据包要回传到原始源,传入数据包的源 IP 和端口必须与前一个传出数据包的目标 IP 和端口匹配,并且传入数据包的目标 IP 和端口必须与 IP 和端口匹配用于传出数据包的 NAT。如果满足该条件,NAT 会将数据包转发到原始源 IP 和端口。这被称为UDP hole punching

让我们用一个例子来说明这一点。假设您有以下主机:

  • 服务器(在 NAT 之外):IP 192.168.0.10
  • NAT:内部 IP 192.168.0.1,外部 IP 10.0.0.1
  • 客户端(NAT 内部):IP 10.0.0.2

您的服务器创建一个绑定到点 9898 的套接字并等待。然后,您的客户端创建一个绑定到端口 0 的套接字,这意味着选择了一个随机端口。假设它是端口 10000。然后客户端向 192.168.0.10:9898 发送一个 UDP 数据包。所以数据包有:

  • 源IP:10.0.0.2
  • 目标IP:192.168.0.10
  • 源端口:10000
  • 目的港:9898

然后数据包通过 NAT,它会调整源 IP 和端口,以便可以将响应发送回客户端。它选择端口 15000。所以现在数据包看起来像这样:

  • 源IP:192.168.0.1
  • 目标IP:192.168.0.10
  • 源端口:15000
  • 目的港:9898

如果 NAT 稍后看到来自外部网络的数据包具有相同的 IP/端口对但源/目标相反,它将发送到 10.0.0.2:10000。

然后服务器接收到这个数据包。但是现在您在服务器上创建了一个新套接字并将其绑定到端口 0,因此选择了一个随机端口,例如 12000。然后服务器使用此套接字将响应发送回它的来源。所以响应包是这样的:

  • 源IP:192.168.0.10
  • 目标IP:192.168.0.1
  • 源端口:12000
  • 目的港:15000

然后NAT 接收到这个数据包,并需要决定是否将它转发到内部主机。如果源端口是 9898,它会将目标 IP/端口更改为 10.0.0.2:10000 并将其发送到那里。但它不匹配,所以 NAT 丢弃了数据包。

服务器需要使用与从客户端接收数据包相同的套接字来发送响应。如果是这样,数据包将如下所示:

  • 源IP:192.168.0.10
  • 目标IP:192.168.0.1
  • 源端口:9898
  • 目的港:15000

并且 NAT 会将其转发给客户端,因为它匹配发出但源/目标交换的数据包。

对于处理来自多个客户端的请求的服务器,它需要跟踪请求的来源,并有某种机制来保持每个客户端的状态以确定要发送的适当响应。

【讨论】:

  • 谢谢!虽然它仍然没有解释为什么,如果我在发送连接请求后向 165535 以上的每个端口发送 10 个数据包,我在请求新端口后发送的无限响应没有到达客户端。现在,我不止一次地向每个端口发送一个数据包,所以数据包应该返回,因为我希望每个端口都收到高于该数字的连接响应,对吧?或者我错过了什么?
  • 在发布的代码中,客户端首先在端口 9898 发送到服务器,服务器应该接收到,然后它每次发送 3 次到服务器没有监听的端口 16384 到 65535,所以它不应该看到那些。在服务器端,收到客户端请求后,它会向同一个 NAT 端口发送响应 65535 - 16384 次,客户端应该看不到任何响应,因为您使用的是具有不同服务器端口的不同套接字。如果您使用相同的套接字进行发送和接收,客户端应该得到所有这些响应。
  • 你说得对,我想解决的问题可能是不可能的。我拿起并构建了一些我发现的 UDP 打孔示例,它们有同样的问题,但它们不起作用。他们的表现似乎与我在上次测试中所做的几乎相同。另外,我修改了代码,所以现在我不断地从服务器和客户端发送到另一端,服务器总是发送到初始请求端口,客户端发送到服务器中每个可能的端口。然而,即使明显遇到端口号冲突,我也无法让单个字节通过。
  • @PabloAriel 很可能,来自客户端的每个传出数据包都映射到不同的 NAT 端口,因为目标端口不同。在服务器上运行 wireshark 应该可以确认。
  • 谢谢!你发现了我失败的地方,这是对通信的正确调试。我承认,我的错,这可能会给我一个明确的答案,即使它是我不喜欢的那个。一旦我有时间尝试,我会告诉你我发现了什么。
猜你喜欢
  • 1970-01-01
  • 2020-08-13
  • 2012-01-06
  • 1970-01-01
  • 2012-09-23
  • 2018-05-23
  • 2018-02-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多