淮安网站建设案例,网站seo排名优化工具在线,wordpress 静态 弊端,普洱高端网站建设价格【Linux】【网络】UDP打洞–不同子网下的客户端和服务器通信#xff08;未成功版#xff09;
上次说基于UDP的打洞程序改了五版一直没有成功#xff0c;要写一下问题所在#xff0c;但是我后续又查询了一些资料#xff0c;成功实现了#xff0c;这次先写一下未成功的…【Linux】【网络】UDP打洞–不同子网下的客户端和服务器通信未成功版
上次说基于UDP的打洞程序改了五版一直没有成功要写一下问题所在但是我后续又查询了一些资料成功实现了这次先写一下未成功的逻辑我认为未成功的排查错误的部分也很重要如果想直接看成功的可以直接看我的下一篇文章。
首先 基于上篇文章的UDP打洞逻辑 这里直接将图贴出来 我的逻辑思路ps:会把代码贴到最后面。)
逻辑梳理
1 服务器端server.c
监听与接收注册 服务器创建 UDP 套接字并绑定到固定端口5050。依次调用 recvfrom() 接收两个客户端先后为 C1 和 C2的注册消息获取各自的源地址即 NAT 映射后的公网 IP 和端口。 地址交换 服务器把 C2 的公网地址IP 和端口格式化成字符串用“^”分隔发送给 C1。同样把 C1 的地址发送给 C2。 后续处理 服务器完成地址交换后退出没有额外发送探测包。 客户端 C1UDPClientcc1.c
两个套接字 使用一个套接字sockS与服务器通信另一个sockC用于后续对等通信并绑定到固定端口6003。 注册阶段 C1 向服务器发送注册消息“I am C1”。接收服务器返回的字符串解析出对方地址信息格式 “ip^port”存入 oppositeSideAddr。 P2P 交互循环 在循环中每隔 500ms 使用 sockC 向 oppositeSideAddr 发送数据keep-alive/消息并尝试接收对方回复。 客户端 C2UDPClientcc2.c
逻辑与 C1 类似 使用两个套接字一个与服务器通信sockS一个用于 P2PsockC绑定固定端口6002。向服务器发送注册消息“I am C2”接收并解析服务器返回的对方地址信息存入 oppositeSideAddr。进入循环每隔 500ms 向 oppositeSideAddr 发送数据并等待回复。 执行结果
服务器
客户端c1: 客户端c2:
可以看到c1,c2 一直在向从服务器获取的公网ip和端口发送数据 但是一直未收到对端回复。 服务器在向双方发送数据后就直接退出了。
排查问题
考虑可能存在的问题并逐步排查
服务器配置问题 确保服务器S正确交换了双方的公网IP和端口信息并且客户端解析无误。 防火墙设置 检查云服务器、客户端以及NAT设备的防火墙是否允许UDP流量通过特别是目标端口是否开放。也需要确保双方的UDP打洞程序所在主机允许接收来自对端的UDP数据包。 NAT映射问题 可能两端的NAT设备类型不支持直接UDP打洞或映射策略比较严格例如对称NAT。您可以检查客户端所在网络的NAT类型尝试在不同网络环境下测试。 端口绑定和映射问题 确认代码中绑定的本地端口6003、6002与NAT映射结果是否符合预期。有些NAT设备可能会复用端口或调整外部映射导致双方看到相同的公网端口从而影响打洞效果。 代码逻辑问题 您的代码中目前只是不断发送数据包但并未实现对收到数据包进行有效处理。如果对端也没有收到数据包可能是由于发送方向NAT设备发送的数据包没有成功映射到对端。
1 服务器是否正确交换了双方的ip和端口
这个测试结果是我第四版的结果在里面已经打印出来对应的ip端口我这边对比了并未出现问题 你们可以再看看上面的图片 结论:正常
2防火墙设置
本地防火墙 检查客户端和服务器上的防火墙状态使用 ufw status、iptables -L 等命令确认UDP目标端口是否被允许。 云防火墙 登录云服务器控制台或路由器管理界面检查是否设置了安全组或防火墙规则确保允许相应的UDP流量包括注册端口和通信端口。
2.1 本地防火墙
本地防火墙未打开
2.2 云服务器
防火墙对应端口已开启 结论:正常
3 抓包查看数据包是否发送出去
在Ubuntu下使用抓包工具来监控和分析网络数据包的流向常用的工具包括 tcpdump命令行和 Wireshark图形界面。 3.1. 使用 tcpdump
安装
sudo apt-get update
sudo apt-get install tcpdump基本用法 抓取所有数据包 sudo tcpdump -i eth0其中 eth0 是您要监控的网络接口可以通过命令 ifconfig 查看接口名称。 我的就是ens33 过滤特定协议和端口 例如抓取UDP数据包 sudo tcpdump -i ens33 udp抓取目的端口为5050的UDP数据包 sudo tcpdump -i ens33 udp port 5050抓包发现数据发送出去了
3 NAT映射问题
使用 stun 工具
1. 安装 stun 客户端 在 Ubuntu系统上运行
sudo apt update
sudo apt install stun-client -y2. 运行 STUN 客户端测试 NAT 类型
stun stun.l.google.com或者
stun stun.sipgate.net这是我的结果 Independent Mapping独立映射 每个内部端口的映射是独立的即无论目标地址如何变化都保持相同的映射。对 UDP 打洞来说这通常是有利的。 Independent Filter独立过滤 外部数据包只要符合映射的端口就会被放行与发送目标无关。这意味着只要内网设备先发起通信外部的回复通常能通过 NAT 设备到达内网。 Random Port随机端口 每个新连接可能会被 NAT 分配一个随机的外部端口这可能会导致端口映射不固定。为了保持连接客户端需要持续发送数据包以维持映射。 No Hairpin 表示 NAT 不支持内部设备通过公网地址直接访问同一 NAT 内的其他设备NAT 回环。这通常对 UDP 打洞影响不大因为 C1 和 C2 是处于不同 NAT 或在不同网络下。 Return value is 0x000012 表示 STUN 客户端检测成功但没有显示映射的端口详细信息通常这意味着端口由 NAT 设备随机分配。
这个结果说明 NAT 环境是相对有利于 UDP 打洞的非对称 NAT但由于随机端口的特性客户端必须持续发送保持 UDP 映射Keep-Alive。
3. NAT 类型
Full Cone NAT全锥形 NAT ✅ UDP 打洞最容易成功Restricted Cone NAT受限锥形 NAT ✅ 需要双向数据包打洞Port-Restricted Cone NAT端口受限锥形 NAT ⚠ 可能无法直接打洞Symmetric NAT对称 NAT ❌ UDP 打洞几乎不可能成功
证明NAT映射支持打洞
最后怀疑问题出在代码逻辑上服务器返回的端口虽然正确但 NAT设备 在一段时间后修改了端口映射或者端口映射被丢弃导致 C1 发送到错误端口。因此后续需要修改端口。
server.c
#include sys/socket.h
#include arpa/inet.h
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h#define DEFAULT_PORT 5050
#define BUFFER_SIZE 100int main() {// server即外网服务器int serverPort DEFAULT_PORT;int serverListen;struct sockaddr_in serverAddr;// 建立监听socketserverListen socket(AF_INET, SOCK_DGRAM, 0);if (serverListen -1) {perror(socket() failed);return -1;}serverAddr.sin_family AF_INET;serverAddr.sin_port htons(serverPort);serverAddr.sin_addr.s_addr INADDR_ANY;if (bind(serverListen, (struct sockaddr *)serverAddr, sizeof(serverAddr)) -1) {perror(bind() failed);return -1;}// 接收来自客户端的连接source1即先连接到S的客户端C1struct sockaddr_in sourceAddr1;socklen_t sourceAddrLen1 sizeof(sourceAddr1);char bufRecv1[BUFFER_SIZE];int len;len recvfrom(serverListen, bufRecv1, sizeof(bufRecv1), 0, (struct sockaddr *)sourceAddr1, sourceAddrLen1);if (len -1) {perror(recvfrom() failed);return -1;}bufRecv1[len] \0;printf(C1 IP:[%s],PORT:[%d]\n, inet_ntoa(sourceAddr1.sin_addr), ntohs(sourceAddr1.sin_port));// 接收来自客户端的连接source2即后连接到S的客户端C2struct sockaddr_in sourceAddr2;socklen_t sourceAddrLen2 sizeof(sourceAddr2);char bufRecv2[BUFFER_SIZE];len recvfrom(serverListen, bufRecv2, sizeof(bufRecv2), 0, (struct sockaddr *)sourceAddr2, sourceAddrLen2);if (len -1) {perror(recvfrom() failed);return -1;}bufRecv2[len] \0;printf(C2 IP:[%s],PORT:[%d]\n, inet_ntoa(sourceAddr2.sin_addr), ntohs(sourceAddr2.sin_port));// 向C1发送C2的外网ip和portchar bufSend1[BUFFER_SIZE];// bufSend1中存储C2的外网ip和portmemset(bufSend1, \0, sizeof(bufSend1));char *ip2 inet_ntoa(sourceAddr2.sin_addr);// C2的ipchar port2[10];// C2的portsnprintf(port2, sizeof(port2), %d, ntohs(sourceAddr2.sin_port));snprintf(bufSend1, sizeof(bufSend1), %s^%s, ip2, port2);len sendto(serverListen, bufSend1, strlen(bufSend1), 0, (struct sockaddr *)sourceAddr1, sourceAddrLen1);if (len -1) {perror(sendto() failed);return -1;} else {printf(send() byte:%d\n, len);}// 向C2发送C1的外网ip和portchar bufSend2[BUFFER_SIZE];// bufSend2中存储C1的外网ip和portmemset(bufSend2, \0, sizeof(bufSend2));char *ip1 inet_ntoa(sourceAddr1.sin_addr);// C1的ipchar port1[10];// C1的portsnprintf(port1, sizeof(port1), %d, ntohs(sourceAddr1.sin_port));snprintf(bufSend2, sizeof(bufSend2), %s^%s, ip1, port1);len sendto(serverListen, bufSend2, strlen(bufSend2), 0, (struct sockaddr *)sourceAddr2, sourceAddrLen2);if (len -1) {perror(sendto() failed);return -1;} else {printf(send() byte:%d\n, len);}// server的中间人工作已完成退出即可剩下的交给C1与C2相互通信close(serverListen);return 0;
}client1.c
#include sys/socket.h
#include arpa/inet.h
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include fcntl.h
#include errno.h#define PORT 6003
#define BUFFER_SIZE 100int main(int argc, char* argv[]) {struct sockaddr_in serverAddr;struct sockaddr_in thisAddr;thisAddr.sin_family AF_INET;thisAddr.sin_port htons(PORT);thisAddr.sin_addr.s_addr INADDR_ANY;if (argc 3) {printf(Usage: UDPClient1 Server IP address Server Port\n);return -1;}int sockS socket(AF_INET, SOCK_DGRAM, 0);if (sockS -1) {perror(socket() failed);return -1;}if (bind(sockS, (struct sockaddr *)thisAddr, sizeof(thisAddr)) -1) {perror(bind() failed);return -1;}int sockC socket(AF_INET, SOCK_DGRAM, 0);if (sockC -1) {perror(socket() failed);return -1;}// 允许端口复用int optval 1;setsockopt(sockC, SOL_SOCKET, SO_REUSEADDR, optval, sizeof(optval));// 绑定固定端口 6003struct sockaddr_in bindAddr;bindAddr.sin_family AF_INET;bindAddr.sin_port htons(6003);bindAddr.sin_addr.s_addr INADDR_ANY;bind(sockC, (struct sockaddr *)bindAddr, sizeof(bindAddr));char bufSend[] I am C1;char bufRecv[BUFFER_SIZE];memset(bufRecv, \0, sizeof(bufRecv));struct sockaddr_in sourceAddr;socklen_t sourceAddrLen sizeof(sourceAddr);struct sockaddr_in oppositeSideAddr;int len;serverAddr.sin_family AF_INET;serverAddr.sin_port htons(atoi(argv[2]));serverAddr.sin_addr.s_addr inet_addr(argv[1]);len sendto(sockS, bufSend, sizeof(bufSend), 0, (struct sockaddr *)serverAddr, sizeof(serverAddr));if (len -1) {perror(sendto() to S failed);return -1;}printf(C1 sent registration packet to server S.\n);len recvfrom(sockS, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)sourceAddr, sourceAddrLen);if (len -1) {perror(recvfrom() from S failed);return -1;}bufRecv[len] \0;printf(C1 received from S: %s\n, bufRecv);close(sockS);char ip[20];char port[10];int i 0;while (i strlen(bufRecv) bufRecv[i] ! ^) {ip[i] bufRecv[i];i;}ip[i] \0;int j 0;i;while (i strlen(bufRecv)) {port[j] bufRecv[i];}port[j] \0;oppositeSideAddr.sin_family AF_INET;oppositeSideAddr.sin_port htons(atoi(port));oppositeSideAddr.sin_addr.s_addr inet_addr(ip);int flags fcntl(sockC, F_GETFL, 0);fcntl(sockC, F_SETFL, flags | O_NONBLOCK);printf(C1 will now try to communicate directly with C2 at %s:%s\n, ip, port);int attempts 0;while (1) {usleep(500000); // 500ms 发送一次len sendto(sockC, bufSend, sizeof(bufSend), 0, (struct sockaddr *)oppositeSideAddr, sizeof(oppositeSideAddr));if (len -1) {perror(sendto() to C2 failed);} else {printf(Sent keep-alive UDP packet to %s:%d\n, inet_ntoa(oppositeSideAddr.sin_addr), ntohs(oppositeSideAddr.sin_port));}len recvfrom(sockC, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)sourceAddr, sourceAddrLen);if (len -1) {if (errno EAGAIN || errno EWOULDBLOCK) {attempts;if (attempts % 10 0) {printf(No response from C2 after 5 seconds. Retrying...\n);}continue;} else {perror(recvfrom() failed);break;}} else {bufRecv[len] \0;printf(C1 received from C2 [%s:%d]: %s\n, inet_ntoa(sourceAddr.sin_addr), ntohs(sourceAddr.sin_port), bufRecv);attempts 0; // 成功收到数据重置重试计数}}close(sockC);return 0;
}client2.c
#include sys/socket.h
#include arpa/inet.h
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include fcntl.h
#include errno.h#define PORT 6002
#define BUFFER_SIZE 100int main(int argc, char* argv[]) {struct sockaddr_in serverAddr;struct sockaddr_in thisAddr;thisAddr.sin_family AF_INET;thisAddr.sin_port htons(PORT);thisAddr.sin_addr.s_addr INADDR_ANY;if (argc 3) {printf(Usage: UDPClient2 Server IP address Server Port\n);return -1;}int sockS socket(AF_INET, SOCK_DGRAM, 0);if (sockS -1) {perror(socket() failed);return -1;}if (bind(sockS, (struct sockaddr *)thisAddr, sizeof(thisAddr)) -1) {perror(bind() failed);return -1;}int sockC socket(AF_INET, SOCK_DGRAM, 0);if (sockC -1) {perror(socket() failed);return -1;}// 允许端口复用int optval 1;setsockopt(sockC, SOL_SOCKET, SO_REUSEADDR, optval, sizeof(optval));// 绑定固定端口 6002struct sockaddr_in bindAddr;bindAddr.sin_family AF_INET;bindAddr.sin_port htons(6002);bindAddr.sin_addr.s_addr INADDR_ANY;bind(sockC, (struct sockaddr *)bindAddr, sizeof(bindAddr));char bufSend[] I am C2;char bufRecv[BUFFER_SIZE];memset(bufRecv, \0, sizeof(bufRecv));struct sockaddr_in sourceAddr;socklen_t sourceAddrLen sizeof(sourceAddr);struct sockaddr_in oppositeSideAddr;int len;serverAddr.sin_family AF_INET;serverAddr.sin_port htons(atoi(argv[2]));serverAddr.sin_addr.s_addr inet_addr(argv[1]);len sendto(sockS, bufSend, sizeof(bufSend), 0, (struct sockaddr *)serverAddr, sizeof(serverAddr));if (len -1) {perror(sendto() to S failed);return -1;}printf(C2 sent registration packet to server S.\n);len recvfrom(sockS, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)sourceAddr, sourceAddrLen);if (len -1) {perror(recvfrom() from S failed);return -1;}bufRecv[len] \0;printf(C2 received from S: %s\n, bufRecv);close(sockS);char ip[20];char port[10];int i 0;while (i strlen(bufRecv) bufRecv[i] ! ^) {ip[i] bufRecv[i];i;}ip[i] \0;int j 0;i;while (i strlen(bufRecv)) {port[j] bufRecv[i];}port[j] \0;oppositeSideAddr.sin_family AF_INET;oppositeSideAddr.sin_port htons(atoi(port));oppositeSideAddr.sin_addr.s_addr inet_addr(ip);int flags fcntl(sockC, F_GETFL, 0);fcntl(sockC, F_SETFL, flags | O_NONBLOCK);printf(C2 will now try to communicate directly with C1 at %s:%s\n, ip, port);int attempts 0;while (1) {usleep(500000); // 500ms 发送一次len sendto(sockC, bufSend, sizeof(bufSend), 0, (struct sockaddr *)oppositeSideAddr, sizeof(oppositeSideAddr));if (len -1) {perror(sendto() to C1 failed);} else {printf(Sent keep-alive UDP packet to %s:%d\n, inet_ntoa(oppositeSideAddr.sin_addr), ntohs(oppositeSideAddr.sin_port));}len recvfrom(sockC, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)sourceAddr, sourceAddrLen);if (len -1) {if (errno EAGAIN || errno EWOULDBLOCK) {attempts;if (attempts % 10 0) {printf(No response from C1 after 5 seconds. Retrying...\n);}continue;} else {perror(recvfrom() failed);break;}} else {bufRecv[len] \0;printf(C2 received from C1 [%s:%d]: %s\n, inet_ntoa(sourceAddr.sin_addr), ntohs(sourceAddr.sin_port), bufRecv);attempts 0; // 成功收到数据重置重试计数}}close(sockC);return 0;
}