主页 > 互联网  > 

【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(成功版)

【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(成功版)

【Linux】【网络】UDP打洞–>不同子网下的客户端和服务器通信(成功版) 根据上个文章的分析 问题可能出现在代码逻辑上面 我这里重新查找资料怀疑:

1 NAT映射可能需要多次数据包的发送才能建立。

2 NAT映射保存时间太短,并且 NAT 可能会在短时间内改变这些映射,需要一直保持映射。

有些 NAT 设备会因为短时间内没有数据而回收端口映射,导致服务器提供的 IP:Port 失效。

保活机制:双方定期发送保活包以防 NAT 超时关闭映射。

3 服务器只是向双方发送了IP和端口后直接退出了,并未发送数据包给客户端,导致NAT 设备未建立映射 (服务器只发送了一次数据,但部分 NAT 设备需要多次交互才会创建映射,一些 NAT 需要收到外部数据包后才会保持映射。)

4 我在代码中绑定了固定端口,NAT内部可能会有自己的实现,让系统自己分配端口可能更合适

5 需要双方在获得对方公网地址后,立即向对方发送“敲门”数据包,以便各自的 NAT 建立映射。

NAT 设备通常只允许曾经主动发送数据给对方的地址接收数据。客户端第一次sendto()的数据包会被丢掉 后续才能正确通信

这里贴一下udp通信接口

整体流程 1. 注册阶段

客户端动作:

每个客户端(C1 和 C2)启动时创建至少一个 UDP 套接字(有些实现中为了隔离控制与数据可能创建两个,但最关键的是: 用于向服务器发送注册消息,并接收服务器返回的信息)。 客户端向服务器发送注册消息(例如 “HELLO”)。此时,通过调用 recvfrom(),服务器能够获得客户端发送数据时的源地址信息,即 NAT 映射后的公网 IP 和端口。

服务器动作:

服务器启动后创建一个 UDP 套接字并绑定到固定端口(例如 50001)。服务器循环等待接收客户端的注册消息,同时记录每个客户端的公网映射地址(从 recvfrom 返回的 sockaddr 信息中获得)。
2. 地址交换阶段 服务器端: 用于存储两个客户端的地址信息和当前注册的客户端数量(例如使用数组 struct sockaddr_in clientAddrs[2] 和计数器 clientCount)。当服务器接收到两个客户端的注册消息后(比如两条 “HELLO” 消息),服务器将这两个客户端的公网映射地址互换: 向客户端1发送消息,格式例如 "PEER <C2公网IP>^<C2映射端口>";向客户端2发送消息,格式例如 "PEER <C1公网IP>^<C1映射端口>"。 为确保 NAT 映射持续有效(避免超时关闭),服务器还可以再额外向两个客户端发送探测数据包(如 “ping”),激活双方的 NAT 映射。
3. NAT 打洞阶段 客户端(C1 和 C2): 在收到服务器返回的包含对方公网映射地址的信息后,每个客户端解析出对方的公网 IP 和映射端口。然后,客户端立即向对方发送一个“敲门”数据包(例如 “knock” 或 “ping”),目的是: 主动触发自己 NAT 设备建立向对方的映射,同时激活对方 NAT 中关于本端发送数据的映射。 双方互发“敲门”包后,各自的 NAT 设备就会允许对方返回数据,即使之前还没有真正建立起正式的数据通信。
4. 双向通信阶段

建立通信:

双方均使用服务器返回的地址(即各自 NAT 映射后的公网 IP 和端口)作为目标地址来发送数据包。客户端进入一个循环: 定时(例如每 500ms 或每 5 秒)向对方发送数据包(这可以是用户输入的消息,也可以是保活数据,如 “KEEP_ALIVE”),同时接收对方的回复,从而确认通信通道已建立。

保活机制:

为防止 NAT 映射因长时间无数据而超时关闭,每个客户端可启动独立的保活线程,定时向对方发送“保活包”,确保映射持续有效。
代码 server.cpp #include <iostream> #include <cstring> #include <unistd.h> #include <arpa/inet.h> #include <errno.h> #include <pthread.h> // 定义服务器监听端口和缓冲区大小 #define SERVER_PORT 50001 #define BUF_SIZE 1024 // 全局变量:用于存储两个客户端的地址信息和当前注册的客户端数量 struct sockaddr_in clientAddrs[2]; int clientCount = 0; // 互斥锁保护对 clientAddrs 和 clientCount 的访问 pthread_mutex_t clients_mutex = PTHREAD_MUTEX_INITIALIZER; int main() { int sockfd; // UDP 套接字描述符 char buffer[BUF_SIZE]; // 用于接收数据的缓冲区 struct sockaddr_in serverAddr, clientAddr; socklen_t addrLen = sizeof(clientAddr); // 1. 创建 UDP 套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { std::cerr << "Server: Socket creation error: " << strerror(errno) << std::endl; return -1; } // 2. 设置并初始化服务器地址结构体 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; // IPv4 serverAddr.sin_addr.s_addr = INADDR_ANY; // 接受任意IP serverAddr.sin_port = htons(SERVER_PORT); // 绑定到 SERVER_PORT // 3. 绑定套接字到指定地址和端口 if (bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) { std::cerr << "Server: Bind error: " << strerror(errno) << std::endl; close(sockfd); return -1; } std::cout << "Server is running on port " << SERVER_PORT << ". Waiting for clients..." << std::endl; // 4. 进入无限循环,不断接收客户端消息 while (true) { // 清空缓冲区,准备接收新的数据 memset(buffer, 0, BUF_SIZE); // 使用 recvfrom 接收客户端数据,同时获取发送者地址 int n = recvfrom(sockfd, buffer, BUF_SIZE, 0, (struct sockaddr*)&clientAddr, &addrLen); if (n < 0) { std::cerr << "Server: recvfrom error: " << strerror(errno) << std::endl; continue; } buffer[n] = '\0'; // 确保数据以字符串形式结束 std::cout << "Received from client: " << buffer << " from " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port) << std::endl; // 加锁后处理客户端注册数据 pthread_mutex_lock(&clients_mutex); // 当收到 "HELLO" 消息且当前注册客户端数不足 2 时,保存客户端地址信息 if (strcmp(buffer, "HELLO") == 0 && clientCount < 2) { clientAddrs[clientCount] = clientAddr; clientCount++; // 回复 ACK 给注册的客户端,确认收到消息 const char* ack = "ACK"; sendto(sockfd, ack, strlen(ack), 0, (struct sockaddr*)&clientAddr, addrLen); } // 如果两个客户端都已注册,交换它们的公网映射地址信息 if (clientCount == 2) { char msg[BUF_SIZE] = { 0 }; // 构造发送给客户端1的消息:包含客户端2的公网IP和映射端口,格式为 "PEER <ip> <port>" snprintf(msg, BUF_SIZE, "PEER %s %d", inet_ntoa(clientAddrs[1].sin_addr), ntohs(clientAddrs[1].sin_port)); sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&clientAddrs[0], sizeof(clientAddrs[0])); std::cout << "Sent to client1: " << msg << std::endl; // 构造发送给客户端2的消息:包含客户端1的公网IP和映射端口 snprintf(msg, BUF_SIZE, "PEER %s %d", inet_ntoa(clientAddrs[0].sin_addr), ntohs(clientAddrs[0].sin_port)); sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&clientAddrs[1], sizeof(clientAddrs[1])); std::cout << "Sent to client2: " << msg << std::endl; // 为保持 NAT 映射有效,可选:服务器额外发送“ping”包给双方 sendto(sockfd, "ping", 4, 0, (struct sockaddr*)&clientAddrs[0], sizeof(clientAddrs[0])); sendto(sockfd, "ping", 4, 0, (struct sockaddr*)&clientAddrs[1], sizeof(clientAddrs[1])); // 重置客户端计数,等待下一组客户端注册 clientCount = 0; } pthread_mutex_unlock(&clients_mutex); } close(sockfd); return 0; }

头文件包含与宏定义

包含了网络编程、字符串操作、线程和错误处理所需的头文件。定义了服务器端口(50001)和缓冲区大小(1024字节)。

全局变量声明

clientAddrs[2] 用于存储两个客户端的地址。clientCount 记录已注册的客户端数量。使用 clients_mutex 保护这些共享变量的并发访问。

主函数开始

创建 UDP 套接字,并检查是否成功;若失败则打印错误信息并退出。初始化服务器地址结构体(IPv4、任意地址、端口转换为网络字节序)。绑定套接字到服务器地址。如果绑定失败则退出。

打印服务器启动信息

输出服务器已启动并监听的提示信息。

进入无限循环,接收客户端消息

每次循环开始前清空缓冲区。调用 recvfrom() 接收客户端数据,并填充客户端地址(即 NAT 映射的公网地址)。打印接收到的消息和客户端地址(转换为点分十进制 IP 与主机字节序的端口)。

客户端注册处理(加锁区域)

当接收到 “HELLO” 消息且 clientCount < 2 时,将当前客户端地址保存到 clientAddrs 数组中,同时发送 “ACK” 消息确认注册。这一步确保服务器能记录每个客户端 NAT 映射后的公网地址。

地址互换与探测(当两个客户端均已注册时)

如果 clientCount == 2,服务器构造两个字符串: 分别包含对方的公网 IP 和映射后的端口,格式为 "PEER <ip> <port>"。 分别将该信息发送给两个客户端,完成地址信息的互换。额外发送“ping”数据包给双方,以确保双方 NAT 映射不会因长时间无数据而关闭。最后重置 clientCount,等待下一组客户端注册。

退出与关闭套接字

循环结束后关闭套接字并返回。 client.cpp(双方一致) #include <iostream> #include <cstring> #include <unistd.h> #include <arpa/inet.h> #include <errno.h> #include <pthread.h> #include <stdlib.h> #include <net/if.h> // 用于 IFNAMSIZ 和 if_nametoindex #define SERVER_PORT 50001 // 服务器端口号 #define BUF_SIZE 1024 // 缓冲区大小 #define KEEP_ALIVE_INTERVAL 25 // 保活包发送间隔(秒) // 全局变量:保存对等端(peer)的地址信息 struct sockaddr_in peerAddr; // 全局的 UDP 套接字描述符(用于所有操作) int sockfd; // 定义互斥锁,用于保护共享的套接字操作,避免多线程竞争 pthread_mutex_t sock_mutex = PTHREAD_MUTEX_INITIALIZER; // 保活线程函数:定时向对方发送 "KEEP_ALIVE" 消息,保持 NAT 映射 void* keep_alive(void* arg) { const char* keepAliveMsg = "KEEP_ALIVE"; while (true) { pthread_mutex_lock(&sock_mutex); // 使用 sendto() 发送保活包到对方地址 sendto(sockfd, keepAliveMsg, strlen(keepAliveMsg), 0, (struct sockaddr*)&peerAddr, sizeof(peerAddr)); pthread_mutex_unlock(&sock_mutex); std::cout << "[KeepAlive] Sent keep alive to peer." << std::endl; sleep(KEEP_ALIVE_INTERVAL); } return NULL; } int main(int argc, char* argv[]) { // 命令行参数:需要传入服务器 IP 和本地绑定的网络接口(例如 "eth0") if (argc < 3) { std::cerr << "Usage: " << argv[0] << " <server_ip> <bind_interface>" << std::endl; std::cerr << "Example: " << argv[0] << " 203.0.113.5 eth0" << std::endl; return -1; } const char* serverIP = argv[1]; const char* bindInterface = argv[2]; // 指定绑定的网络接口 // 1. 创建 UDP 套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { std::cerr << "Client: Socket creation error: " << strerror(errno) << std::endl; return -1; } // 2. 绑定套接字到指定网络接口 // 这一步可以确保数据包通过指定的接口发送,防止“Network is unreachable”错误 if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, bindInterface, strlen(bindInterface)) < 0) { std::cerr << "Client: SO_BINDTODEVICE error: " << strerror(errno) << std::endl; close(sockfd); return -1; } // 3. 绑定本地地址,让系统自动分配端口(无需固定端口) struct sockaddr_in localAddr; memset(&localAddr, 0, sizeof(localAddr)); localAddr.sin_family = AF_INET; localAddr.sin_addr.s_addr = INADDR_ANY; localAddr.sin_port = htons(0); // 0 表示由系统自动分配 if (bind(sockfd, (struct sockaddr*)&localAddr, sizeof(localAddr)) < 0) { std::cerr << "Client: Bind error: " << strerror(errno) << std::endl; close(sockfd); return -1; } // 4. 设置服务器地址结构 struct sockaddr_in serverAddr; memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; // 将服务器 IP 转换为网络地址格式 if (inet_pton(AF_INET, serverIP, &serverAddr.sin_addr) <= 0) { std::cerr << "Client: Invalid server IP." << std::endl; close(sockfd); return -1; } serverAddr.sin_port = htons(SERVER_PORT); socklen_t serverLen = sizeof(serverAddr); // 5. 向服务器发送注册消息 "HELLO" const char* helloMsg = "HELLO"; if (sendto(sockfd, helloMsg, strlen(helloMsg), 0, (struct sockaddr*)&serverAddr, serverLen) < 0) { std::cerr << "Client: sendto error: " << strerror(errno) << std::endl; close(sockfd); return -1; } std::cout << "Sent HELLO to server." << std::endl; // 6. 接收来自服务器的回复 char buffer[BUF_SIZE] = { 0 }; struct sockaddr_in fromAddr; socklen_t fromLen = sizeof(fromAddr); int n = recvfrom(sockfd, buffer, BUF_SIZE, 0, (struct sockaddr*)&fromAddr, &fromLen); if (n < 0) { std::cerr << "Client: recvfrom error: " << strerror(errno) << std::endl; close(sockfd); return -1; } buffer[n] = '\0'; std::cout << "Received from server: " << buffer << std::endl; // 7. 如果收到的是 ACK,则继续等待服务器发来的 PEER 信息 if (strncmp(buffer, "ACK", 3) == 0) { memset(buffer, 0, BUF_SIZE); n = recvfrom(sockfd, buffer, BUF_SIZE, 0, (struct sockaddr*)&fromAddr, &fromLen); if (n < 0) { std::cerr << "Client: recvfrom error: " << strerror(errno) << std::endl; close(sockfd); return -1; } buffer[n] = '\0'; std::cout << "Received from server: " << buffer << std::endl; } // 8. 解析服务器返回的 PEER 信息,格式为 "PEER <peer_ip> <peer_port>" char peerIP[INET_ADDRSTRLEN]; int peerPort; if (sscanf(buffer, "PEER %s %d", peerIP, &peerPort) == 2) { memset(&peerAddr, 0, sizeof(peerAddr)); peerAddr.sin_family = AF_INET; if (inet_pton(AF_INET, peerIP, &peerAddr.sin_addr) <= 0) { std::cerr << "Client: Invalid peer IP." << std::endl; close(sockfd); return -1; } peerAddr.sin_port = htons(peerPort); std::cout << "Peer address: " << peerIP << ":" << peerPort << std::endl; } else { std::cerr << "Client: Failed to parse peer info." << std::endl; close(sockfd); return -1; } // 9. 在收到 PEER 信息后,客户端可以立即向对方发送“敲门”包,以触发 NAT 映射 const char* knockMsg = "knock"; sendto(sockfd, knockMsg, strlen(knockMsg), 0, (struct sockaddr*)&peerAddr, sizeof(peerAddr)); std::cout << "Sent knock to peer." << std::endl; // 10. 启动保活线程,定时向对方发送 "KEEP_ALIVE" 包,保持 NAT 映射 pthread_t kaThread; if (pthread_create(&kaThread, NULL, keep_alive, NULL) != 0) { std::cerr << "Client: pthread_create error: " << strerror(errno) << std::endl; close(sockfd); return -1; } // 11. P2P 交互:从标准输入读取消息,向对方发送,并接收对方回复 while (true) { std::cout << "Enter message to send to peer: "; std::string input; std::getline(std::cin, input); if (input.empty()) continue; // 加锁确保发送数据时套接字操作不会与保活线程冲突 pthread_mutex_lock(&sock_mutex); sendto(sockfd, input.c_str(), input.length(), 0, (struct sockaddr*)&peerAddr, sizeof(peerAddr)); pthread_mutex_unlock(&sock_mutex); // 设置接收超时,等待对方回复 memset(buffer, 0, BUF_SIZE); struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 0; setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); n = recvfrom(sockfd, buffer, BUF_SIZE, 0, NULL, NULL); if (n > 0) { buffer[n] = '\0'; std::cout << "Received from peer: " << buffer << std::endl; } } close(sockfd); return 0; }

头文件包含与宏定义

包含了进行网络编程、线程同步、字符串处理、错误处理等所需的头文件。定义服务器端口号、缓冲区大小和保活包的发送间隔。

全局变量

peerAddr:用于保存对方客户端的公网映射地址。sockfd:全局 UDP 套接字,用于所有数据传输。sock_mutex:互斥锁,确保多线程(保活线程与主线程)对同一套接字的操作不会冲突。

keep_alive() 保活线程

循环内使用互斥锁保护对 sockfd 的 sendto 操作,向 peerAddr 发送 “KEEP_ALIVE” 消息,间隔 25 秒一次,帮助保持 NAT 映射。

main() 函数开始

检查命令行参数,要求传入服务器 IP 和本地绑定的网络接口(例如 “eth0”)。

创建 UDP 套接字

使用 socket() 创建一个 UDP 套接字,并检查是否成功。

绑定网络接口

通过 setsockopt() 使用 SO_BINDTODEVICE 将套接字绑定到指定网络接口,确保数据包走正确的网络。

绑定本地地址

绑定本地地址,让系统自动分配端口(使用端口 0)。

设置服务器地址

构造服务器地址结构体,转换服务器 IP 字符串为网络格式,并设置服务器端口。

向服务器发送注册消息 “HELLO”

使用 sendto() 发送 “HELLO” 消息给服务器,通知服务器本客户端注册。

接收服务器回复

调用 recvfrom() 接收服务器的回应。第一次可能收到 ACK,如果收到 ACK,则继续等待服务器发送包含对方地址的 PEER 信息。

解析 PEER 信息

使用 sscanf() 解析服务器返回的字符串,提取出对方的公网 IP 和映射端口,并存入 peerAddr 结构体中。

主动向对方发送“敲门”包

收到对方地址后,立即使用 sendto() 向该地址发送 “knock” 包,以触发 NAT 映射的建立。

启动保活线程

创建一个新线程运行 keep_alive(),定时向对方发送保活包,防止 NAT 映射超时关闭。

进入 P2P 交互循环

循环中提示用户输入消息,然后加锁通过 sendto() 发送给对方。设置接收超时,使用 recvfrom() 尝试接收对方的回复,并打印收到的数据。

关闭套接字并结束程序

循环退出后关闭套接字。
标签:

【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(成功版)由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(成功版)