魏晓东 发表于 2024-6-5 08:27:55

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]
查看完整版本: UDP内网穿透和打洞原理的C语言代码实现