c++ - 客户端未收到 UDP 连接响应
问题描述
我在实现 UDP 连接时遇到了麻烦,因为当我在 LAN 中尝试它时它可以工作,但是当来自 NAT 内部的某人尝试连接到公共服务器地址时,它会失败,因为从服务器发送的数据包作为响应,永远不会到达客户。
我的协议如下:
- 客户端 A 向服务器发送一个字节作为连接请求。
- 服务器 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;
}
注意:我在https://github.com/asm128/gpk留下了示例 udp_server 和 udp_client 项目,以防您需要一个工作示例,通过取消注释//#define MAKE_IT_WORK
并放置您的 IP 地址来有条件地编译工作和损坏的案例。
解决方案
您的响应没有返回给客户端,因为您使用单独的套接字来发送响应。此套接字与从客户端接收数据包的套接字具有不同的本地端口号,因此对于 NAT,它似乎来自不同的源,因此不会被转发。
当 UDP 数据报退出 NAT 时,NAT 会跟踪目标 IP 和端口以及 NAT 使用的本地 IP 和端口,并将其与内部网络上的原始源 IP 和端口匹配。传入数据包要回传到原始源,传入数据包的源 IP 和端口必须与前一个传出数据包的目标 IP 和端口匹配,并且传入数据包的目标 IP 和端口必须与 IP 和端口匹配用于传出数据包的 NAT。如果满足该条件,NAT 会将数据包转发到原始源 IP 和端口。这称为UDP 打孔
让我们用一个例子来说明这一点。假设您有以下主机:
- 服务器(在 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,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 会将其转发给客户端,因为它匹配发出但源/目标交换的数据包。
对于处理来自多个客户端的请求的服务器,它需要跟踪请求的来源,并具有某种机制来保持每个客户端的状态以确定要发送的适当响应。
推荐阅读
- python - 用python制作稀疏矩阵和向量乘法程序的问题
- node.js - 给定 id 的 MongoDB 计数
- html - Bootstrap Typography:没有找到合适的大写和反向值的类属性
- bash - 如何在 bash(Ubuntu 18.04)中一段时间后手动停止闪烁的 printf 命令(使用 tput)?
- python - 我不明白 for 循环和嵌套循环
- android - Android:在 Icon 中包装文件描述符
- spring-boot - Spring Boot 解码 POST 请求正文
- javascript - JavaScript 函数在我的托管服务器上无法正常工作
- javascript - 为什么我的表单不使用 ajax 和烧瓶 wtforms 提交到后端?
- r - sp::proj4string(obj) : CRS 对象有注释,在 R 的输出中丢失