UDP内网穿透和打洞原理的C语言代码实现
v1.0 2024年6月5日 发布于博客园目录
[*]序言
[*]UDP打洞的原理
[*]应用场景
[*]基本理论
[*]代码实现
[*]udp_client_NAT.c
[*]udp_server_NAT.c
[*]结果
[*]参考链接
序言
https://img2024.cnblogs.com/blog/3129082/202406/3129082-20240605090324103-1929718601.png
UDP打洞(UDP Hole Punching)是一种用于在NAT(网络地址转换)设备背面建立直接P2P(点对点)连接的技术。NAT设备通常会阻止外部设备直接与内部设备通信,因为它们隐藏了内部网络的IP地址。UDP打洞通过利用NAT设备的行为特性来绕过这些限定,从而实现直接通信。
UDP打洞的原理
[*]初始连接:两个希望进行P2P通信的设备(称为A和B)首先都与一个公共服务器(称为中继服务器)建立连接。中继服务器记载下它们的公共IP地址和端标语。
[*]互换信息:中继服务器将A的公共IP地址和端标语发送给B,同时将B的公共IP地址和端标语发送给A。
[*]打洞尝试:A和B使用从中继服务器获得的对方的公共IP地址和端标语,尝试直接向对方发送UDP数据包。由于NAT设备通常会允许内部设备发起的连接通过,因此这些数据包会在NAT设备上打开一个临时的“洞”。
[*]建立连接:如果A和B的NAT设备都允许这种临时的“洞”,那么A和B就可以通过这些洞进行直接的P2P通信,而不再需要通过中继服务器。
应用场景
UDP打洞技术在许多应用中非常有用,尤其是在需要高效、低延迟的P2P通信时。以下是一些常见的应用场景:
[*]及时通信应用:如VoIP(网络电话)、视频谈天和在线游戏等。这些应用需要低延迟的通信,而通过中继服务器转发数据会增加延迟。
[*]文件共享:P2P文件共享网络(如BitTorrent)可以利用UDP打洞技术来建立直接连接,从而进步传输速度和效率。
[*]远程控制和协作:如远程桌面、在线协作工具等,通过直接P2P连接可以提供更流通的用户体验。
[*]物联网(IoT)设备:许多IoT设备位于NAT设备背面,UDP打洞可以使它们更轻易与外部服务器或其他设备直接通信。
[*]游戏主机和客户端:在线游戏通常需要快速的P2P连接来同步游戏状态和动作,UDP打洞技术可以显著改善游戏体验。
UDP打洞是一种非常有用的技术,尤其是在需要高效、低延迟的P2P通信的应用中。它通过巧妙地利用NAT设备的行为特性,使得位于NAT设备背面的设备也能够进行直接的P2P通信。
基本理论
/** * 前提: 服务器具有公网ip, 客户端和服务端已经协商好端标语 * * 第一步: 客户端发送打洞包给服务器 C---NET--->S (此时客户端看得见服务器, 服务器看不见客户端) * 客户端向服务器发送一个UDP包。NAT设备会为这个连接分配一个公网IP和端口,并将包转发给服务器。 * * 第二步: 服务器接收并记载客户端信息(服务器记载客户端的网路信息, 但不知道万恶的运营商有没有关掉这条网路) * 服务器接收到包后,记载下客户端的公网IP和端口。 * * 第三步: 服务器发送确认信息给客户端: CS while (1) { /** * 对互斥锁进行上锁,如果主线程未上锁,则此次调用会上锁乐成,函数调用将立马返回; * 如果互斥锁此时已经被其它线程锁定了,会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。 */ pthread_mutex_lock(ka_args->mutex); sendto(ka_args->sock_fd, keep_alive_msg, strlen(keep_alive_msg), MSG_CONFIRM, // 资助你确认数据包的路径可达性。具体地,内核会尝试确认目的地址是可达的,并且路径是有效的。且避免不必要的探测. (const struct sockaddr *)&ka_args->socket_addr, ka_args->addr_len); pthread_mutex_unlock(ka_args->mutex); // 解锁 printf("\n客户端已发服务器送保活包\n"); sleep(KEEP_ALIVE_INTERVAL); // 定期保活 }}int main(int argc, char const *argv[]){ char validbuffer; // 传回的有效数据 pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 初始化套接字文件互斥锁 /**********************第一步: 客户端发送打洞包给服务器 C---NET--->S******************************/ /*****①创建套接字文件描述符****/ int client_sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建客户端套接字文件描述符 ipv4 udp 默认协议选择 if (0 > client_sock_fd) { fprintf(stderr, "客户端UDP套接字文件错误,errno:%d,%s\n", errno, strerror(errno)); exit(1); } /****************END***************/ /****************②发送信息给服务器****************/ // 服务器的IP信息布局体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); // 配置服务器IP信息布局体 server_addr.sin_family = AF_INET; // ipv4协议簇 server_addr.sin_addr.s_addr = inet_addr(SERVER_ADDR); // 服务器公网IP server_addr.sin_port = htons(SERVER_PORT); // 服务器端口 // 向服务器发送打洞包 char buffer = "HELLO_SERVER"; // 发送给服务器的打洞包 内容无所谓 socklen_t addr_len = sizeof(struct sockaddr_in); // 信息布局体长度 ssize_t sent_bytes = sendto(client_sock_fd, // 客户端套接字文件描述符 buffer, // 要发送的数据缓冲区 strlen(buffer), // 要发送的字符串长度 MSG_CONFIRM, // 确认数据包有效性标志位 (const struct sockaddr *)&server_addr, // 指向包含目的地址的 sockaddr 布局体 addr_len); // 目的地址的长度 if (sent_bytes == -1) { fprintf(stderr, "发送数据失败, errno:%d, %s\n", errno, strerror(errno)); close(client_sock_fd); exit(1); } memset(buffer, 0x0, sizeof(buffer)); // 清空buffer /****************END***************/ /************************************END*****************************************/ // 第二步由服务器完成 /**********************第三步: 服务器发送确认信息给客户端: CS (此时客户端看得见服务器, 服务器看不见客户端) * 客户端向服务器发送一个UDP包。NAT设备会为这个连接分配一个公网IP和端口,并将包转发给服务器。 * * 第二步: 服务器接收并记载客户端信息(服务器记载客户端的网路信息, 但不知道万恶的运营商有没有关掉这条网路) * 服务器接收到包后,记载下客户端的公网IP和端口。 * * 第三步: 服务器发送确认信息给客户端: Csock_fd, keep_alive_msg, strlen(keep_alive_msg), MSG_CONFIRM, // 资助你确认数据包的路径可达性。具体地,内核会尝试确认目的地址是可达的,并且路径是有效的。且避免不必要的探测. (const struct sockaddr *)&ka_args->socket_addr, ka_args->addr_len); pthread_mutex_unlock(ka_args->mutex); // 解锁 printf("\n服务器已向客户端发送保活包\n"); sleep(KEEP_ALIVE_INTERVAL); }}int main(int argc, char const *argv[]){ char validbuffer; // 传回的有效数据 pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 初始化套接字文件互斥锁 /**********************第二步: 服务器接收并记载客户端信息 C---NET--->S******************************/ /*****①创建套接字文件描述符并绑定接收****/ int server_sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建客户端套接字文件描述符 ipv4 udp 默认协议选择 if (0 > server_sock_fd) { fprintf(stderr, "服务器端创建UDP套接字文件错误,errno:%d,%s\n", errno, strerror(errno)); exit(1); } // 服务器端的IP信息布局体 struct sockaddr_in server_addr; // 配置服务器地址信息 接受来自任何地方的数据 包有效 但只解析50001端口的包 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(CLIENT_PORT); // 绑定socket到指定端口 if (bind(server_sock_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { fprintf(stderr, "将服务器套接字文件描述符绑定IP失败, errno:%d,%s\n", errno, strerror(errno)); close(server_sock_fd); exit(1); } printf("服务器已经运行, 等待客户端相应中...\n"); /****************END***************/ /****************②接收客户端的打洞包****************/ char buffer; // 存放接收到的数据缓冲区 memset(buffer, 0x0, sizeof(buffer)); // 清空buffer struct sockaddr_in client_addr; socklen_t addr_len = sizeof(struct sockaddr_in); int n = recvfrom(server_sock_fd, buffer, BUF_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len); buffer = '\0'; printf("解除阻塞, 从客户端收到信息: %s\n", buffer); printf("客户端NAT地址: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); /****************END***************/ /************************************END*****************************************/ /**********************第三步: 服务器发送确认信息给客户端: CS (此时客户端看得见服务器, 服务器看不见客户端) * 客户端向服务器发送一个UDP包。NAT设备会为这个连接分配一个公网IP和端口,并将包转发给服务器。 * * 第二步: 服务器接收并记载客户端信息(服务器记载客户端的网路信息, 但不知道万恶的运营商有没有关掉这条网路) * 服务器接收到包后,记载下客户端的公网IP和端口。 * * 第三步: 服务器发送确认信息给客户端: C
页:
[1]