UDP內網穿透和打洞原理的C語言程式碼實現

舟清颺發表於2024-06-05

v1.0 2024年6月5日 釋出於部落格園

目錄
  • 序言
    • UDP打洞的原理
    • 應用場景
  • 基本理論
  • 程式碼實現
    • udp_client_NAT.c
    • udp_server_NAT.c
    • 結果
  • 參考連結

序言

image

UDP打洞(UDP Hole Punching)是一種用於在NAT(網路地址轉換)裝置後面建立直接P2P(點對點)連線的技術。NAT裝置通常會阻止外部裝置直接與內部裝置通訊,因為它們隱藏了內部網路的IP地址。UDP打洞透過利用NAT裝置的行為特性來繞過這些限制,從而實現直接通訊。

UDP打洞的原理

  1. 初始連線:兩個希望進行P2P通訊的裝置(稱為A和B)首先都與一個公共伺服器(稱為中繼伺服器)建立連線。中繼伺服器記錄下它們的公共IP地址和埠號。

  2. 交換資訊:中繼伺服器將A的公共IP地址和埠號傳送給B,同時將B的公共IP地址和埠號傳送給A。

  3. 打洞嘗試:A和B使用從中繼伺服器獲得的對方的公共IP地址和埠號,嘗試直接向對方傳送UDP資料包。由於NAT裝置通常會允許內部裝置發起的連線透過,因此這些資料包會在NAT裝置上開啟一個臨時的“洞”。

  4. 建立連線:如果A和B的NAT裝置都允許這種臨時的“洞”,那麼A和B就可以透過這些洞進行直接的P2P通訊,而不再需要透過中繼伺服器。

應用場景

UDP打洞技術在許多應用中非常有用,尤其是在需要高效、低延遲的P2P通訊時。以下是一些常見的應用場景:

  1. 實時通訊應用:如VoIP(網路電話)、影片聊天和線上遊戲等。這些應用需要低延遲的通訊,而透過中繼伺服器轉發資料會增加延遲。

  2. 檔案共享:P2P檔案共享網路(如BitTorrent)可以利用UDP打洞技術來建立直接連線,從而提高傳輸速度和效率。

  3. 遠端控制和協作:如遠端桌面、線上協作工具等,透過直接P2P連線可以提供更流暢的使用者體驗。

  4. 物聯網(IoT)裝置:許多IoT裝置位於NAT裝置後面,UDP打洞可以使它們更容易與外部伺服器或其他裝置直接通訊。

  5. 遊戲主機和客戶端:線上遊戲通常需要快速的P2P連線來同步遊戲狀態和動作,UDP打洞技術可以顯著改善遊戲體驗。

UDP打洞是一種非常有用的技術,尤其是在需要高效、低延遲的P2P通訊的應用中。它透過巧妙地利用NAT裝置的行為特性,使得位於NAT裝置後面的裝置也能夠進行直接的P2P通訊。

基本理論

/**
 * 前提: 伺服器具有公網ip, 客戶端和服務端已經協商好埠號
 *
 * 第一步: 客戶端傳送打洞包給伺服器 C---NET--->S (此時客戶端看得見伺服器, 伺服器看不見客戶端)
 *         客戶端向伺服器傳送一個UDP包。NAT裝置會為這個連線分配一個公網IP和埠,並將包轉發給伺服器。
 *
 * 第二步: 伺服器接收並記錄客戶端資訊  (伺服器記錄客戶端的網路資訊, 但不知道萬惡的運營商有沒有關掉這條網路)
 *         伺服器接收到包後,記錄下客戶端的公網IP和埠。
 *
 * 第三步: 伺服器傳送確認資訊給客戶端: C<---NET---S (伺服器沿著原來的網路回傳資訊, 客戶端若收到後維持網路)
 *         伺服器向客戶端傳送確認訊息,確保NAT裝置為這對IP和埠建立了對映。
 *
 * 第四步: 儲存對映:C<---NET--->S 需要不斷髮送保活包, 建議為1/2超時時間 (這條路可通, 不斷維持這條路)
 *          客戶端和伺服器透過傳送UDP包來保持這個對映。只要對映存在,後續的UDP包可以直接穿過NAT裝置。
 *          NAT裝置會在一段時間內沒有檢測到任何活動後關閉對映。這段時間通常被稱為“空閒超時時間”或“會話超時時間”。
 *          UDP超時時間:常見的預設值是30秒、60秒或120秒。
 *          TCP超時時間: 通常在數分鐘到數小時之間。
 */

由於我並不需要實現2個客戶端的直接通訊(增加一箇中間伺服器即可), 而是在典型的NAT穿透場景中,知道伺服器端的公網IP和埠,但不知道客戶端的公網IP,可以透過一些技巧來實現UDP打洞。以下是一個可能的方案:

  1. 伺服器端:伺服器端監聽來自客戶端的連線請求,並記錄客戶端的公網IP和埠。
  2. 客戶端:客戶端向伺服器傳送一個初始訊息,伺服器記錄該訊息的來源IP和埠。
  3. 伺服器:伺服器將記錄的客戶端IP和埠返回給客戶端。
  4. 雙方打洞:客戶端和伺服器透過傳送UDP包到對方的IP和埠來打洞。

程式碼實現

編譯命令:cc udp_client_NAT.c -o udp_client_NAT.out -pthread

udp_client_NAT.c

/**
 * @file name : udp_client_NAT.c
 * @brief     : 用於實現基本的UDP客戶端和伺服器端打洞
 * @author    : RISE_AND_GRIND@163.com
 * @date      : 2024/04/07
 * @version   : 1.0
 * @note      :
 * CopyRight (c)  2023-2024   RISE_AND_GRIND@163.com   All Right Reseverd
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <errno.h>

#define SERVER_PORT 50001            // 公網伺服器的埠
#define SERVER_ADDR "120.79.143.250" // 公網伺服器的IP地址
#define BUF_SIZE 1024                // 緩衝區大小(位元組)
#define KEEP_ALIVE_INTERVAL 25       // 保活包傳送間隔
// 保活包的網路資訊
typedef struct
{
    int sock_fd;                    // 套接字檔案描述符
    struct sockaddr_in socket_addr; // 定義套接字所需的地址資訊結構體
    socklen_t addr_len;             // 目標地址的長度
    pthread_mutex_t *mutex;         // 互斥鎖變數
} KeepAlivePackageArgs_t;

/**
 * @name      keep_alive
 * @brief     保活執行緒函式, 用於保持活路
 * @param     args 執行緒例程引數, 傳入保活包的網路資訊
 * @note
 */
void *keep_alive(void *args)
{
    // 用於傳入的是void* 需要強轉才能正確指向
    KeepAlivePackageArgs_t *ka_args = (KeepAlivePackageArgs_t *)args;
    char keep_alive_msg[] = "KEEP_ALIVE_CLIENT"; // 傳送給伺服器的保活包, 表示我是客戶端, C---NET-->S
    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[BUF_SIZE]; // 傳回的有效資料
    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[BUF_SIZE] = "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*****************************************/

    // 第二步由伺服器完成

    /**********************第三步: 伺服器傳送確認資訊給客戶端: C<---NET---S******************************/
    // 接收來自伺服器的確認訊息
    int n = recvfrom(client_sock_fd,                  // 套接字檔案描述符
                     buffer,                          // 接收資料的緩衝區
                     BUF_SIZE,                        // 緩衝區的長度
                     0,                               // MSG_WAITALL 嚴格等待完整的資料,會一直阻塞,直到接收到指定數量的位元組(即 BUF_SIZE)或者發生錯誤為止。它確保接收到的資料量滿足請求的大小。
                     (struct sockaddr *)&server_addr, // 指向儲存源地址的 sockaddr 結構體
                     &addr_len);                      // 指向源地址長度的指標
    buffer[n] = '\0';                                 // 將接收到的資料轉換為字串
    printf("打洞中, 從伺服器收到: %s\n", buffer);
    /************************************END*****************************************/

    /**********************第四步: 儲存對映:C<---NET--->S *******************************************/
    // 新執行緒的TID
    pthread_t keep_alive_thread;
    // 定義執行緒保活包的網路資訊
    KeepAlivePackageArgs_t ka_args;
    // 設定保活執行緒引數
    ka_args.sock_fd = client_sock_fd;
    ka_args.socket_addr = server_addr;
    ka_args.addr_len = addr_len;
    ka_args.mutex = &mutex;
    // 建立保活執行緒  並將保活包的網路資訊傳入執行緒
    if (pthread_create(&keep_alive_thread, NULL, keep_alive, (void *)&ka_args) != 0)
    {
        fprintf(stderr, "建立保活執行緒錯誤, errno:%d,%s\n", errno, strerror(errno));
        close(client_sock_fd);
        exit(1);
    }
    /************************************END*****************************************/

    /************************************正常收發資料部分*******************************************/
    // // 傳送訊息給伺服器端
    // const char *message = "我是客戶端!";
    // size_t message_len = strlen(message);
    // 主迴圈:接收和處理伺服器訊息
    while (1)
    {
        // 傳送訊息給伺服器端

        // 從鍵盤輸入字串
        char message[BUF_SIZE];
        printf("請輸入要傳送給伺服器的訊息: ");
        if (fgets(message, BUF_SIZE, stdin) == NULL)
        {
            perror("fgets error");
            continue;
        }

        // 移除換行符
        size_t message_len = strlen(message);
        if (message[message_len - 1] == '\n')
        {
            message[message_len - 1] = '\0';
            message_len--;
        }

        // 向伺服器傳送資料
        pthread_mutex_lock(&mutex); // 對套接字檔案上鎖
        sendto(client_sock_fd, message, message_len, MSG_CONFIRM, (const struct sockaddr *)&server_addr, addr_len);
        pthread_mutex_unlock(&mutex); // 對套接字檔案解鎖

        // 接收來自伺服器的訊息
        pthread_mutex_lock(&mutex); // 對套接字檔案上鎖
        n = recvfrom(client_sock_fd, buffer, BUF_SIZE, 0, (struct sockaddr *)&server_addr, &addr_len);
        pthread_mutex_unlock(&mutex); // 對套接字檔案解鎖
        if (n < 0)
        {
            perror("recvfrom error");
            continue;
        }
        buffer[n] = '\0';

        // 判斷是否是保活資訊
        if (strcmp(buffer, "KEEP_ALIVE_SERVER") != 0) // 若收到的不是保活資訊, 則為有效資訊
        {
            // 有效資訊, 不是保活資訊,處理並儲存
            printf("★收到有效資訊: %s\n", buffer);
            memcpy(validbuffer, buffer, n + 1); // 使用 memcpy 代替 strncpy
        }
        else
        {
            printf("\n從伺服器收到保活資訊: %s\n", buffer);
        }

        // 清空 buffer
        memset(buffer, 0, BUF_SIZE);
    }
    /************************************END*****************************************/

    close(client_sock_fd); // 關閉套接字檔案
    return 0;
}
/**
 * 前提: 伺服器具有公網ip, 客戶端和服務端已經協商好埠號
 *
 * 第一步: 客戶端傳送打洞包給伺服器 C---NET--->S (此時客戶端看得見伺服器, 伺服器看不見客戶端)
 *         客戶端向伺服器傳送一個UDP包。NAT裝置會為這個連線分配一個公網IP和埠,並將包轉發給伺服器。
 *
 * 第二步: 伺服器接收並記錄客戶端資訊  (伺服器記錄客戶端的網路資訊, 但不知道萬惡的運營商有沒有關掉這條網路)
 *         伺服器接收到包後,記錄下客戶端的公網IP和埠。
 *
 * 第三步: 伺服器傳送確認資訊給客戶端: C<---NET---S (伺服器沿著原來的網路回傳資訊, 客戶端若收到後維持網路)
 *         伺服器向客戶端傳送確認訊息,確保NAT裝置為這對IP和埠建立了對映。
 *
 * 第四步: 儲存對映:C<---NET--->S 需要不斷髮送保活包, 建議為1/2超時時間 (這條路可通, 不斷維持這條路)
 *          客戶端和伺服器透過傳送UDP包來保持這個對映。只要對映存在,後續的UDP包可以直接穿過NAT裝置。
 *          NAT裝置會在一段時間內沒有檢測到任何活動後關閉對映。這段時間通常被稱為“空閒超時時間”或“會話超時時間”。
 *          UDP超時時間:常見的預設值是30秒、60秒或120秒。
 *          TCP超時時間: 通常在數分鐘到數小時之間。
 */

udp_server_NAT.c

/**
 * @file name : udp_server_NAT.c
 * @brief     : 用於實現基本的UDP客戶端和伺服器端打洞
 * @author    : RISE_AND_GRIND@163.com
 * @date      : 2024/04/07
 * @version   : 1.0
 * @note      :
 * CopyRight (c)  2023-2024   RISE_AND_GRIND@163.com   All Right Reseverd
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <errno.h>

#define CLIENT_PORT 50001      // 客戶端的埠
#define BUF_SIZE 1024          // 緩衝區大小(位元組)
#define KEEP_ALIVE_INTERVAL 25 // 保活包傳送間隔

// 保活包的網路資訊
typedef struct
{
    int sock_fd;                    // 套接字檔案描述符
    struct sockaddr_in socket_addr; // 定義套接字所需的地址資訊結構體
    socklen_t addr_len;             // 目標地址的長度
    pthread_mutex_t *mutex;         // 互斥鎖變數
} KeepAlivePackageArgs_t;

/**
 * @name      keep_alive
 * @brief     保活執行緒函式, 用於保持活路
 * @param     args 執行緒例程引數, 傳入保活包的網路資訊
 * @note
 */
void *keep_alive(void *args)
{
    // 用於傳入的是void* 需要強轉才能正確指向
    KeepAlivePackageArgs_t *ka_args = (KeepAlivePackageArgs_t *)args;
    char keep_alive_msg[] = "KEEP_ALIVE_SERVER"; // 傳送給客戶端的保活包, 表示我是伺服器, C<---NET--S

    for (;;)
    {
        /**
         * 對互斥鎖進行上鎖,如果主執行緒未上鎖,則此次呼叫會上鎖成功,函式呼叫將立馬返回;
         * 如果互斥鎖此時已經被其它執行緒鎖定了,會一直阻塞,直到該互斥鎖被解鎖,到那時,呼叫將鎖定互斥鎖並返回。
         */
        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[BUF_SIZE]; // 傳回的有效資料
    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[BUF_SIZE];               // 存放接收到的資料緩衝區
    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[n] = '\0';

    printf("解除阻塞, 從客戶端收到資訊: %s\n", buffer);
    printf("客戶端NAT地址: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    /****************END***************/
    /************************************END*****************************************/
    /**********************第三步: 伺服器傳送確認資訊給客戶端: C<---NET---S******************************/
    // 向客戶端傳送確認訊息
    char ack_msg[BUF_SIZE];
    snprintf(ack_msg, BUF_SIZE, "ACK %s %d", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    sendto(server_sock_fd, ack_msg, strlen(ack_msg), MSG_CONFIRM, (const struct sockaddr *)&client_addr, addr_len);
    memset(buffer, 0x0, sizeof(buffer)); // 清空buffer
    printf("打洞完成, 進入互動\n");
    /************************************END*****************************************/
    /**********************第四步: 儲存對映:C<---NET--->S *******************************************/
    // 新執行緒的TID
    pthread_t keep_alive_thread;
    // 定義執行緒保活包的網路資訊
    KeepAlivePackageArgs_t ka_args;
    // 設定保活執行緒引數
    ka_args.sock_fd = server_sock_fd;
    ka_args.socket_addr = client_addr;
    ka_args.addr_len = addr_len;
    ka_args.mutex = &mutex;
    // 建立保活執行緒  並將保活包的網路資訊傳入執行緒
    if (pthread_create(&keep_alive_thread, NULL, keep_alive, (void *)&ka_args) != 0)
    {
        fprintf(stderr, "建立保活執行緒錯誤, errno:%d,%s\n", errno, strerror(errno));
        close(server_sock_fd);
        exit(1);
    }
    /************************************END*****************************************/
    // 傳送訊息給客戶端
    const char *message = "我是伺服器, 收到請回答!";
    size_t message_len = strlen(message);
    // 主迴圈:接收和響應客戶端訊息
    while (1)
    {
        // 傳送訊息給客戶端
        pthread_mutex_lock(&mutex); // 對套接字檔案上鎖
        sendto(server_sock_fd, message, message_len, MSG_CONFIRM, (const struct sockaddr *)&client_addr, addr_len);
        pthread_mutex_unlock(&mutex); // 對套接字檔案解鎖

        // 接收來自客戶端的訊息
        pthread_mutex_lock(&mutex); // 對套接字檔案上鎖
        n = recvfrom(server_sock_fd, buffer, BUF_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);
        pthread_mutex_unlock(&mutex); // 對套接字檔案解鎖
        if (n < 0)
        {
            perror("recvfrom error");
            continue;
        }
        buffer[n] = '\0';

        // 判斷是否是保活資訊
        if (strcmp(buffer, "KEEP_ALIVE_CLIENT") != 0) // 若收到的不是保活資訊, 則為有效資訊
        {
            // 有效資訊, 不是保活資訊,處理並儲存
            printf("★收到有效資訊: %s\n", buffer);
            memcpy(validbuffer, buffer, n + 1); // 使用 memcpy 代替 strncpy
        }
        else
        {
            printf("\n從客戶端收到保活資訊: %s\n", buffer);
        }

        // 清空 buffer
        memset(buffer, 0, BUF_SIZE);
    }

    close(server_sock_fd);
    return 0;
}
/**
 * 前提: 伺服器具有公網ip, 客戶端和服務端已經協商好埠號
 *
 * 第一步: 客戶端傳送打洞包給伺服器 C---NET--->S (此時客戶端看得見伺服器, 伺服器看不見客戶端)
 *         客戶端向伺服器傳送一個UDP包。NAT裝置會為這個連線分配一個公網IP和埠,並將包轉發給伺服器。
 *
 * 第二步: 伺服器接收並記錄客戶端資訊  (伺服器記錄客戶端的網路資訊, 但不知道萬惡的運營商有沒有關掉這條網路)
 *         伺服器接收到包後,記錄下客戶端的公網IP和埠。
 *
 * 第三步: 伺服器傳送確認資訊給客戶端: C<---NET---S (伺服器沿著原來的網路回傳資訊, 客戶端若收到後維持網路)
 *         伺服器向客戶端傳送確認訊息,確保NAT裝置為這對IP和埠建立了對映。
 *
 * 第四步: 儲存對映:C<---NET--->S 需要不斷髮送保活包, 建議為1/2超時時間 (這條路可通, 不斷維持這條路)
 *          客戶端和伺服器透過傳送UDP包來保持這個對映。只要對映存在,後續的UDP包可以直接穿過NAT裝置。
 *          NAT裝置會在一段時間內沒有檢測到任何活動後關閉對映。這段時間通常被稱為“空閒超時時間”或“會話超時時間”。
 *          UDP超時時間:常見的預設值是30秒、60秒或120秒。
 *          TCP超時時間: 通常在數分鐘到數小時之間。
 */

結果

打洞模組編寫成功, 可實現內網客戶端與伺服器相互UDP通訊

image

image

參考連結

  • P2P通訊原理與實現(C語言) - 知乎 (zhihu.com)
  • UDP內網穿透和打洞原理與程式碼實現 - 知乎 (zhihu.com)

相關文章