?NAT穿透技術原理:P2P通信中的打洞機制解析?

要說網絡世界里的 “幕后功臣”,NAT 絕對得算一個,大家伙兒有沒有琢磨過,為啥家里的電腦、手機,還有公司那一堆設備,都能同時連上網,還不打架呢?

NAT 這東西,全名叫網絡地址轉換,聽著挺唬人,其實說白了就是個 “地址翻譯官”。

你家Wi-Fi路由器其實是臺網絡地址轉換器(NAT),它就像個精通TCP/IP協議棧的門衛大爺,左手拿IP地址簿,右手握端口分配表,用NAPT(網絡地址端口轉換)的黑科技,讓你家100臺設備能共用一個公網IP瘋狂沖浪!

為什么需要NAT?

這就得說說 IPv4 了,這哥們兒是個 32 位的整數,最多也就撐死了表達 40 多億個 IP 地址。但是,你想想,現在全球多少設備要上網啊,手機、電腦、平板,還有各種智能家居,這 40 多億哪夠分啊,不夠用那是板上釘釘的事兒。

而 NAT 呢,NAT通過私有IP地址池(192.168.x.x/10.x.x.x/172.16.x.x-172.31.x.x)配合端口多路復用,實現了1:N的地址復用。就這么一下,有限的公網 IP 就能讓無數設備同時上網,你說神不神?雖說 IPv6 能解決地址不夠的問題,但現在好多設備還是認 IPv4 這老伙計,所以 NAT 還得繼續發光發熱。

舉個技術栗子🌰:

你用192.168.1.100:5000訪問www.qq.com,NAT會將其轉換為公網IP:65535隨機端口(比如114.34.12.55:49152),服務器回包時再根據五元組(源IP、源端口、目標IP、目標端口、協議類型)精準投遞。

NAT 的核心作用

  • 省 IPv4 地址,讓多個設備共享一個公網 IP,大大緩解了地址不夠用的難題。
  • 能藏住內部網絡,外面的人沒法直接摸到私有 IP,提高了安全性。
  • 簡化網絡管理,內網里換個設備啥的,無需調整公共IP配置。

NAT的三大派系

類型

技術原理

典型場景

靜態NAT

1:1映射,公網IP與私網IP永久綁定

對外服務的Web服務器(如Nginx反向代理)

動態NAT

IP池分配,從預定義公網IP池動態分配地址

企業內網臨時對外訪問(如FTP匿名登錄)

PAT(NAPT)

端口級復用,通過狀態表(Connection Tracking Table)記錄會話

家庭路由器、云服務器VPC網關

  • 靜態地址 NAT,也叫 1:1 NAT,就是私網主機地址和公網地址一對一固定轉換,這輩子就綁死了。它的用途也挺專一,一般是給那些藏在內部網絡,卻又需要從互聯網被訪問到的服務器用的,比如 Web 服務器、郵件服務器這些。外面的用戶想訪問這些服務器,直接敲公網 IP 就行,NAT 設備會悄咪咪地把請求轉到內部對應的私有 IP 服務器上,跟變魔術似的。它的映射表都是手動配置的,條目一旦定了就雷打不動,而且這哥們兒最大的特點是雙向都能通,外面能訪問進來,里面也能出去,全靠 NAT 規則給開綠燈。
  • 動態地址 NAT 呢,也叫 Pooled NAT,手里攥著一個公網 IP 地址池,當內部主機想往外連網時,它就從池子里隨便挑一個沒用過的公網 IP 地址,分給這個內部主機的私有 IP,而且不搞端口映射那套。它的映射表是動態變化的,連接一建立就生成條目,要是連接擱那兒不用,超時了就自動刪了,跟臨時工似的,用完就走。不過它也有個小毛病,一個私有 IP 在連接活動的時候,得獨自霸占一個公網 IP,所以同時能上網的內部主機數量,全看公網 IP 池里有多少地址。而且通常情況下,外面想主動連進來可不太容易,除非專門配置了端口轉發。
  • 網絡地址端口NAPT就更厲害了,不光換地址,還換端口,多個私網地址能對應同一個公網地址,就靠不同的端口區分,這家伙最大的好處就是特省公網 IP 地址,成百上千臺主機共用一個公網 IP 都沒問題,堪稱解決 IPv4 地址不夠用的大救星。不過默認情況下,外面想主動連進來也會被攔著,畢竟它跟狀態防火墻是好搭檔,想讓外面連進來,得專門配置端口轉發或者觸發規則才行。

NAT的優缺點

??優點:

  • 節省公網IP:千臺設備共享1個IP,IPv4利用率提升N倍
  • 增強安全性:私網IP對外不可見,防火墻規則更易管理
  • 簡化網絡拓撲:內網設備變更IP時無需通知外網

??缺點:

  • 破壞端到端通信:P2P直連需依賴STUN/ICE打洞
  • 增加延遲:NAT轉換需占用CPU資源(尤其在高并發場景)
  • 狀態表限制:超過系統最大連接數(如Linux默認65536)會觸發丟包

NAT穿透原理與能力

NAT穿透六步流程:

1.?Client1?&?Client2?與?Server?建立連接 ?→?Server?獲取兩者的公網映射地址(IP1:Port1?和?IP2:Port2) ?
2.?Server?將?IP1:Port1?通知?Client2??→?Client2?向?IP1:Port1?發送請求 ?
3. 數據包被?NAT1?丟棄(無映射表項) ?→?NAT2?記錄?IP1:Port1?的映射 ?
4.?Server?將?IP2:Port2?通知?Client1??→?Client1?主動向?IP2:Port2?發起連接 ?
5.?Client1?的連接請求通過?NAT2??→?NAT1?創建?IP2:Port2?的映射 ?
6. 雙向通信建立(穿透成功) ?→ 若失敗,需通過?TURN?中繼或?TCP?打洞 ?

在識別出需要穿越的NAT類型后,基于該NAT類型的特性制定相應的穿透策略,由此可以得出以下結論:

NAT的底層工作流程詳解:

場景設定:

  • 內部網絡:私有地址段為?192.168.1.0/24。
  • NAT路由器:公網接口 IP 為?203.0.113.5。
  • 內部主機:192.168.1.100?想訪問公網服務器?8.8.8.8?的 Web 服務(端口 80)。

內部主機發起連接:

生成數據包

  • 源 IP:192.168.1.100(內網私有地址)
  • 源端口:49152(隨機選擇的臨時端口)
  • 目的 IP:8.8.8.8(公網服務器地址)
  • 目的端口:80(Web 服務默認端口)
  • 發送數據包:數據包通過默認網關(即 NAT 路由器)發送到公網。

NAT 路由器接收數據包

  • 路由器查看其?NAT 狀態表(連接跟蹤表),查找是否存在匹配的條目:(192.168.1.100, 49152, 8.8.8.8, 80, TCP)。
  • 結果:未找到匹配項(新連接)。

NAPT 轉換(地址和端口重寫)

  • 公網 IP:203.0.113.5(唯一可用的公網地址)。
  • 公網端口:從可用端口范圍(通常?1024-65535)中隨機分配一個未被占用的端口,例如?60001

重寫數據包頭

  • 新源 IP:203.0.113.5(替換私有 IP?192.168.1.100)。
  • 新源端口:60001(替換原始端口?49152)。
  • 目的 IP 和端口:保持不變(8.8.8.8:80)。
  • 更新 NAT 狀態表:創建一條新的映射條目:
協議: TCP ?
內部地址和端口: 192.168.1.100:49152 ?
外部地址和端口: 203.0.113.5:60001 ?
目的地址和端口: 8.8.8.8:80 ?
狀態: SYN_SENT ?
計時器: 啟動空閑超時(如 TCP 連接通常為幾分鐘)

轉發數據包到互聯網

  • 修改后的數據包:源 IP 和端口變為?203.0.113.5:60001,目的 IP 和端口仍為?8.8.8.8:80。
  • 數據包被路由到公網,最終到達服務器?8.8.8.8:80。
  • 服務器響應:服務器?8.8.8.8?發送 TCP?SYN-ACK?包,目標地址為?203.0.113.5:60001。

NAT 路由器接收返回數據包

數據包到達 NAT 路由器

  • 源 IP:8.8.8.8
  • 源端口:80
  • 目的 IP:203.0.113.5
  • 目的端口:60001
  • 查找 NAT 狀態表:路由器查找匹配項:(203.0.113.5:60001, TCP)。
  • 結果:找到映射條目?192.168.1.100:49152。

反向轉換(地址和端口還原)

重寫數據包頭

  • 新目的 IP:192.168.1.100(替換公網 IP?203.0.113.5)。
  • 新目的端口:49152(替換映射端口?60001)。
  • 源 IP 和端口:保持不變(8.8.8.8:80)。
  • 更新狀態表:將條目狀態更新為?ESTABLISHED,并重置超時計時器。

轉發數據包到內部主機

  • 修改后的數據包:目的 IP 和端口變為?192.168.1.100:49152,源 IP 和端口仍為?8.8.8.8:80。
  • 數據包被路由到內部主機?192.168.1.100:49152,完成 TCP 三次握手。

連接維持與超時處理

連接狀態跟蹤

  • NAT 設備持續監控連接狀態(通過 TCP 的?FIN/RST?包或 UDP 流量)。
  • 如果連接長時間無數據傳輸(超過超時時間,如 TCP 默認幾分鐘),NAT 會刪除對應的映射條目,釋放端口?60001。

資源回收

  • 釋放的端口可被其他內部主機的新連接復用,實現高效的地址和端口共享。

網絡穿透實戰

接下來進入網絡穿透實戰:TCP 打洞、UDP 打洞和 UPn。

1、TCP 打洞

TCP 打洞(TCP Hole Punching)這玩意兒,說白了就是讓兩個被 NAT 擋著的客戶端,借助第三方服務器搭個橋,從而建立直接連接的招兒。你想啊,NAT 這東西平常就跟個門神似的,不讓外面的主機直接跟內部的主機嘮嗑,所以就得找個外部服務器來從中協調協調。

工作原理:

  • 中繼服務器連接:兩個被 NAT 罩著的客戶端 A 和 B,得先分別跟公共服務器 S 建立連接。
  • 交換外部地址:服務器 S 這時候就跟個信息中轉站似的,知道了 A 和 B 的外部 IP 和端口,接著就把這些信息互相告訴對方。
  • 嘗試著直接連接:A 和 B 拿到對方的外部 IP 和端口后,就分別試著往對方那兒連。要是兩邊的 NAT 設備都放行,那這連接就算成了,倆客戶端就能直接嘮上了。

示例代碼

以下是一個簡單的?C++?示例,演示了通過 TCP 打洞進行連接的過程。

#include?<iostream>
#include?<cstring>
#include?<cstdlib>
#include?<sys/socket.h>
#include?<netinet/in.h>
#include?<arpa/inet.h>
#include?<unistd.h>
#include?<csignal>
#include?<cerrno>
#include?<fcntl.h>
// 全局變量用于優雅關閉
static?volatile?bool?g_running =?true;
// 信號處理:Ctrl+C 退出
void?signal_handler(int?sig)?{if?(sig == SIGINT || sig == SIGTERM) {std::cout <<?"\nShutting down..."?<< std::endl;g_running =?false;}
}
// 安全發送數據(確保全部發送)
bool?safe_send(int?sockfd,?const?char* buffer,?size_t?len)?{const?char* ptr = buffer;while?(len >?0) {ssize_t?sent =?send(sockfd, ptr, len,?0);if?(sent ==?-1) {if?(errno == EINTR)?continue; ?// 被中斷,重試perror("send failed");return?false;}ptr += sent;len -= sent;}return?true;
}
// 服務器端:循環處理每一對客戶端
void?server()?{// 注冊信號處理signal(SIGINT, signal_handler);signal(SIGTERM, signal_handler);// 創建監聽套接字int?listen_fd =?socket(AF_INET, SOCK_STREAM,?0);if?(listen_fd ==?-1) {perror("socket creation failed");return;}// 啟用地址復用int?opt =?1;setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt,?sizeof(opt));// 綁定地址struct?sockaddr_in?server_addr;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(12345);if?(bind(listen_fd, (struct?sockaddr*)&server_addr,?sizeof(server_addr)) ==?-1) {perror("bind failed");close(listen_fd);return;}if?(listen(listen_fd,?5) ==?-1) {perror("listen failed");close(listen_fd);return;}std::cout <<?"Server started on port 12345. Waiting for clients..."?<< std::endl;while?(g_running) {// 接受第一個客戶端struct?sockaddr_in?client_a_addr;socklen_t?addr_len =?sizeof(client_a_addr);int?client_a_fd =?accept(listen_fd, (struct?sockaddr*)&client_a_addr, &addr_len);if?(client_a_fd ==?-1) {if?(errno == EINTR && !g_running)?break;perror("accept client A failed");continue;}char?client_a_ip[INET_ADDRSTRLEN] = {0};inet_ntop(AF_INET, &client_a_addr.sin_addr, client_a_ip, INET_ADDRSTRLEN);int?client_a_port =?ntohs(client_a_addr.sin_port);std::cout <<?"Client A connected: "?<< client_a_ip <<?":"?<< client_a_port << std::endl;// 接受第二個客戶端struct?sockaddr_in?client_b_addr;addr_len =?sizeof(client_b_addr);int?client_b_fd =?accept(listen_fd, (struct?sockaddr*)&client_b_addr, &addr_len);if?(client_b_fd ==?-1) {std::cerr <<?"Failed to accept client B"?<< std::endl;close(client_a_fd);continue;}char?client_b_ip[INET_ADDRSTRLEN] = {0};inet_ntop(AF_INET, &client_b_addr.sin_addr, client_b_ip, INET_ADDRSTRLEN);int?client_b_port =?ntohs(client_b_addr.sin_port);std::cout <<?"Client B connected: "?<< client_b_ip <<?":"?<< client_b_port << std::endl;// 構造消息并發送(A -> B 信息,B -> A 信息)char?msg_to_a[64];int?len_a =?snprintf(msg_to_a,?sizeof(msg_to_a),?"%s:%d", client_b_ip, client_b_port);if?(len_a <?0?|| len_a >=?sizeof(msg_to_a)) {std::cerr <<?"Failed to format message for client A"?<< std::endl;close(client_a_fd);close(client_b_fd);continue;}char?msg_to_b[64];int?len_b =?snprintf(msg_to_b,?sizeof(msg_to_b),?"%s:%d", client_a_ip, client_a_port);if?(len_b <?0?|| len_b >=?sizeof(msg_to_b)) {std::cerr <<?"Failed to format message for client B"?<< std::endl;close(client_a_fd);close(client_b_fd);continue;}// 發送信息if?(!safe_send(client_a_fd, msg_to_a, len_a)) {std::cerr <<?"Send to client A failed"?<< std::endl;}if?(!safe_send(client_b_fd, msg_to_b, len_b)) {std::cerr <<?"Send to client B failed"?<< std::endl;}// 關閉連接(P2P 協調完成)close(client_a_fd);close(client_b_fd);std::cout <<?"Exchanged info between clients. Ready for next pair."?<< std::endl;}close(listen_fd);std::cout <<?"Server shutdown."?<< std::endl;
}
// 客戶端函數
void?client(const?char* server_ip)?{int?sock_fd =?socket(AF_INET, SOCK_STREAM,?0);if?(sock_fd ==?-1) {perror("socket creation failed");return;}struct?sockaddr_in?server_addr;memset(&server_addr,?0,?sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port =?htons(12345);if?(inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <=?0) {std::cerr <<?"Invalid server IP address: "?<< server_ip << std::endl;close(sock_fd);return;}if?(connect(sock_fd, (struct?sockaddr*)&server_addr,?sizeof(server_addr)) ==?-1) {perror("connect to server failed");close(sock_fd);return;}std::cout <<?"Connected to server at "?<< server_ip <<?":12345"?<< std::endl;// 接收對端信息char?buffer[128] = {0};ssize_t?n =?recv(sock_fd, buffer,?sizeof(buffer) -?1,?0);if?(n <=?0) {perror("receive peer info failed");close(sock_fd);return;}buffer[n] =?'\0';std::cout <<?"Received peer info: "?<< buffer << std::endl;// 解析對端地址(安全方式)char?peer_ip[16] = {0};int?peer_port =?0;char* colon =?strchr(buffer,?':');if?(!colon) {std::cerr <<?"Invalid peer info format (missing colon): "?<< buffer << std::endl;close(sock_fd);return;}*colon =?'\0';if?(strlen(buffer) >=?16) {std::cerr <<?"Peer IP too long"?<< std::endl;close(sock_fd);return;}strcpy(peer_ip, buffer);peer_port =?atoi(colon +?1);if?(peer_port <=?0?|| peer_port >?65535) {std::cerr <<?"Invalid peer port: "?<< peer_port << std::endl;close(sock_fd);return;}// 創建新套接字連接對端int?peer_fd =?socket(AF_INET, SOCK_STREAM,?0);if?(peer_fd ==?-1) {perror("create peer socket failed");close(sock_fd);return;}struct?sockaddr_in?peer_addr;memset(&peer_addr,?0,?sizeof(peer_addr));peer_addr.sin_family = AF_INET;if?(inet_pton(AF_INET, peer_ip, &peer_addr.sin_addr) <=?0) {std::cerr <<?"Invalid peer IP: "?<< peer_ip << std::endl;close(peer_fd);close(sock_fd);return;}peer_addr.sin_port =?htons(peer_port);std::cout <<?"Attempting to connect to peer: "?<< peer_ip <<?":"?<< peer_port << std::endl;if?(connect(peer_fd, (struct?sockaddr*)&peer_addr,?sizeof(peer_addr)) ==?-1) {perror("connect to peer failed (this is expected if behind NAT)");}?else?{std::cout <<?"? Successfully connected to peer!"?<< std::endl;// 這里可以發送測試消息const?char* test_msg =?"Hello from P2P client!";if?(safe_send(peer_fd, test_msg,?strlen(test_msg))) {std::cout <<?"Sent message to peer."?<< std::endl;}close(peer_fd);}close(sock_fd);std::cout <<?"Client finished."?<< std::endl;
}
// 主函數:解析命令行
int?main(int?argc,?char* argv[])?{if?(argc <?2) {std::cerr <<?"Usage: "?<< argv[0] <<?" server | client <server_ip>\n"<<?"Example:\n"<<?" ?"?<< argv[0] <<?" server ? ? ? ?# Start server\n"<<?" ?"?<< argv[0] <<?" client 127.0.0.1 ?# Run client\n";return?1;}if?(std::string(argv[1]) ==?"server") {server();}?else?if?(std::string(argv[1]) ==?"client") {if?(argc !=?3) {std::cerr <<?"Client requires server IP. Usage: "?<< argv[0] <<?" client <server_ip>"?<< std::endl;return?1;}client(argv[2]);}?else?{std::cerr <<?"Unknown mode: "?<< argv[1] <<?". Use 'server' or 'client'"?<< std::endl;return?1;}return?0;
}

2、UDP打洞

UDP 打洞(UDP Hole Punching)跟 TCP 打洞是一路貨色,都是讓被 NAT 攔著的兩臺主機,靠著第三方服務器搭線,建立直接的 UDP 連接的技術。不過它跟 TCP 不一樣,UDP 這哥們兒是無連接的協議,這就讓 NAT 主機更容易接受來自外面的連接請求,沒那么多彎彎繞繞。

工作原理

  • 服務器通信:兩臺客戶端 A 和 B 分別跟公共服務器 S 聊上幾句,服務器就跟個記賬的似的,把它們的外部 IP 和端口都記下來。
  • 交換地址:服務器把 A 和 B 的外部 IP 和端口互相轉告,就像中間人把倆人的位置信息互換一下,讓彼此知道對方在哪兒。
  • 直接發送 UDP 數據包:A 和 B 拿到對方的外部地址后,就試著直接往對方那兒發 UDP 數據包,借著 NAT 會話表里的記錄來傳輸數據。這一下要是成了,倆主機就能直接通過 UDP 嘮嗑了,方便得很。

示例代碼:

#include?<iostream>
#include?<cstring>
#include?<cstdlib>
#include?<sys/socket.h>
#include?<netinet/in.h>
#include?<arpa/inet.h>
#include?<unistd.h>
#include?<cerrno>
#include?<string>
// 服務器函數:接收兩個客戶端,交換地址
void?udp_server()?{int?sockfd =?socket(AF_INET, SOCK_DGRAM,?0);if?(sockfd ==?-1) {perror("socket creation failed");return;}// 設置地址可重用int?opt =?1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt,?sizeof(opt));struct?sockaddr_in?server_addr;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(12345);if?(bind(sockfd, (struct?sockaddr*)&server_addr,?sizeof(server_addr)) ==?-1) {perror("bind failed");close(sockfd);return;}std::cout <<?"UDP Server listening on port 12345..."?<< std::endl;// 接收第一個客戶端(A)的消息char?buffer[1024];struct?sockaddr_in?client_a_addr;socklen_t?addr_len =?sizeof(client_a_addr);ssize_t?recv_len =?recvfrom(sockfd, buffer,?sizeof(buffer),?0,(struct?sockaddr*)&client_a_addr, &addr_len);if?(recv_len ==?-1) {perror("recvfrom client A failed");close(sockfd);return;}char?client_a_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_a_addr.sin_addr, client_a_ip, INET_ADDRSTRLEN);int?client_a_port =?ntohs(client_a_addr.sin_port);std::cout <<?"Received from A: "?<< client_a_ip <<?":"?<< client_a_port << std::endl;// 接收第二個客戶端(B)的消息struct?sockaddr_in?client_b_addr;addr_len =?sizeof(client_b_addr);recv_len =?recvfrom(sockfd, buffer,?sizeof(buffer),?0,(struct?sockaddr*)&client_b_addr, &addr_len);if?(recv_len ==?-1) {perror("recvfrom client B failed");close(sockfd);return;}char?client_b_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_b_addr.sin_addr, client_b_ip, INET_ADDRSTRLEN);int?client_b_port =?ntohs(client_b_addr.sin_port);std::cout <<?"Received from B: "?<< client_b_ip <<?":"?<< client_b_port << std::endl;// 向 A 發送 B 的地址std::string msg_to_a = std::string(client_b_ip) +?":"?+ std::to_string(client_b_port);if?(sendto(sockfd, msg_to_a.c_str(), msg_to_a.length(),?0,(struct?sockaddr*)&client_a_addr,?sizeof(client_a_addr)) ==?-1) {perror("sendto client A failed");}// 向 B 發送 A 的地址std::string msg_to_b = std::string(client_a_ip) +?":"?+ std::to_string(client_a_port);if?(sendto(sockfd, msg_to_b.c_str(), msg_to_b.length(),?0,(struct?sockaddr*)&client_b_addr,?sizeof(client_b_addr)) ==?-1) {perror("sendto client B failed");}std::cout <<?"Exchanged addresses between clients."?<< std::endl;close(sockfd);
}
// 客戶端函數:注冊并嘗試連接對端
void?udp_client(const?char* server_ip)?{int?sockfd =?socket(AF_INET, SOCK_DGRAM,?0);if?(sockfd ==?-1) {perror("socket creation failed");return;}// 設置接收超時(10秒),用于判斷對端是否響應struct?timeval?timeout;timeout.tv_sec =?10;timeout.tv_usec =?0;setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout,?sizeof(timeout));struct?sockaddr_in?server_addr;memset(&server_addr,?0,?sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port =?htons(12345);if?(inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <=?0) {std::cerr <<?"Invalid server IP address: "?<< server_ip << std::endl;close(sockfd);return;}// 發送初始消息到服務器(打洞注冊)const?char* hello_msg =?"Hello from client";if?(sendto(sockfd, hello_msg,?strlen(hello_msg),?0,(struct?sockaddr*)&server_addr,?sizeof(server_addr)) ==?-1) {perror("sendto server failed");close(sockfd);return;}std::cout <<?"Sent registration to server at "?<< server_ip <<?":12345"?<< std::endl;// 接收服務器返回的對端地址char?buffer[1024];socklen_t?addr_len =?sizeof(server_addr);ssize_t?recv_len =?recvfrom(sockfd, buffer,?sizeof(buffer) -?1,?0,(struct?sockaddr*)&server_addr, &addr_len);if?(recv_len <=?0) {perror("recvfrom server (peer info) failed");close(sockfd);return;}buffer[recv_len] =?'\0';std::string?peer_info(buffer);std::cout <<?"Received peer info: "?<< peer_info << std::endl;// 解析 peer_info: "ip:port"size_t?colon_pos = peer_info.find(':');if?(colon_pos == std::string::npos) {std::cerr <<?"Invalid peer info format: "?<< peer_info << std::endl;close(sockfd);return;}std::string peer_ip = peer_info.substr(0, colon_pos);int?peer_port = std::stoi(peer_info.substr(colon_pos +?1));// 準備對端地址結構struct?sockaddr_in?peer_addr;memset(&peer_addr,?0,?sizeof(peer_addr));peer_addr.sin_family = AF_INET;peer_addr.sin_port =?htons(peer_port);if?(inet_pton(AF_INET, peer_ip.c_str(), &peer_addr.sin_addr) <=?0) {std::cerr <<?"Invalid peer IP: "?<< peer_ip << std::endl;close(sockfd);return;}// 發送消息到對端(嘗試打洞)const?char* punch_msg =?"Hello peer!";std::cout <<?"Sending hole-punch message to peer: "?<< peer_ip <<?":"?<< peer_port << std::endl;if?(sendto(sockfd, punch_msg,?strlen(punch_msg),?0,(struct?sockaddr*)&peer_addr,?sizeof(peer_addr)) ==?-1) {perror("sendto peer failed");}?else?{std::cout <<?"Hole-punch packet sent."?<< std::endl;}// 嘗試接收來自對端的響應(模擬 P2P 回應)std::cout <<?"Waiting for response from peer..."?<< std::endl;recv_len =?recvfrom(sockfd, buffer,?sizeof(buffer) -?1,?0,?nullptr,?nullptr);if?(recv_len >?0) {buffer[recv_len] =?'\0'

3、UPnP(通用即插即用)

UPnP(Universal Plug and Play,通用即插即用)這協議可有意思了,它就像個熱心的網絡向導,能讓設備在網絡里自動找到其他設備,還能順暢地跟它們嘮嗑。在 NAT 環境下,UPnP 更厲害,能自動給路由器的端口 “開門”,讓外面的設備順順當當地訪問內網里的設備。

這玩意兒主要在家庭網絡和小型局域網里派上用場,靠著設備自己自動配置,把網絡中設備通信的過程變得簡單多了,不用人瞎操心。

工作原理:

  • 設備發現:客戶端設備會發個 SSDP(簡單服務發現協議)請求,就像在網絡里喊一嗓子 “有沒有 UPnP 設備啊”,以此來尋找網絡中的 UPnP 設備。
  • 獲取路由器的設備描述:通過 SSDP 找到的設備,會提供一個設備描述 XML 文件,里面把自己的功能和端點都寫得明明白白,就像給對方遞了張名片,讓人家知道自己能干啥。
  • 請求端口映射:客戶端會給路由器發請求,要求把一個外部端口映射到內網設備的特定端口,相當于跟路由器說 “麻煩把這個門牌號對應的門打開,讓外面的人能找到我家這個房間”。這么一來,外部設備就能通過這個映射的端口訪問內網設備啦。

安裝?miniupnpc庫

Ubuntu/Debian:

sudo apt-get?update
sudo apt-get?install miniupnpc libminiupnpc-dev

macOS:

brew?install miniupnpc

Windows使用 vcpkg:

vcpkg?install miniupnpc

代碼實現:

#include?<iostream>
#include?<cstring>
#include?"upnpcommands.h"
#include?"miniupnpcstrings.h"
int?main()?{struct?UPNPDev* devlist =?nullptr;struct?UPNPUrls?urls;struct?IGDdatas?data;int?error =?0;// 1. 發現 UPnP 設備(最多等待 3 秒)std::cout <<?"Discovering UPnP devices on the network..."?<< std::endl;devlist =?upnpDiscover(2000,?nullptr,?nullptr,?0,?0,?2, &error);if?(!devlist) {std::cerr <<?"No UPnP devices found or network error."?<< std::endl;return?1;}// 2. 獲取 IGD(Internet Gateway Device)信息error =?UPNP_GetValidIGD(devlist, &urls, &data,?nullptr,?0);if?(error !=?1) {std::cerr <<?"No valid UPnP IGD router found."?<< std::endl;freeUPNPDevlist(devlist);return?1;}std::cout <<?"Found UPnP IGD: "?<< data.first.servicetype << std::endl;std::cout <<?"Control URL: "?<< urls.controlURL << std::endl;// 3. 獲取路由器的公網 IP 地址char?wan_ip[64];error =?UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, wan_ip);if?(error ==?0) {std::cout <<?"Public IP Address: "?<< wan_ip << std::endl;}?else?{std::cerr <<?"Failed to get public IP address."?<< std::endl;}// === 配置端口映射 ===const?char* local_ip =?"192.168.1.100"; ??// ? 改為你的本機內網 IPconst?unsigned?short?internal_port =?8080;?// 內網服務端口const?unsigned?short?external_port =?8080;?// 路由器對外開放的端口const?char* protocol =?"TCP"; ? ? ? ? ? ? ?// 或 "UDP"const?char* description =?"C++ UPnP Forward";std::cout <<?"Requesting port mapping: "<< external_port <<?"/"?<< protocol<<?" -> "?<< local_ip <<?":"?<< internal_port<< std::endl;// 4. 添加端口映射error =?UPNP_AddPortMapping(urls.controlURL,data.first.servicetype,external_port, ? ? ? ? ?// 外部端口internal_port, ? ? ? ? ?// 內部端口local_ip, ? ? ? ? ? ? ??// 內部客戶端 IPdescription, ? ? ? ? ? ?// 描述protocol, ? ? ? ? ? ? ??// 協議 (TCP/UDP)nullptr, ? ? ? ? ? ? ? ?// 端口映射的遠程主機(空 = 所有)nullptr? ? ? ? ? ? ? ? ?// 端口映射持續時間(空 = 永久或默認));if?(error ==?0) {std::cout <<?"? Port mapping added successfully!"?<< std::endl;}?else?{std::cerr <<?"? Failed to add port mapping. Error code: "?<< error << std::endl;FreeUPNPUrls(&urls);freeUPNPDevlist(devlist);return?1;}// 5. 驗證映射是否存在char?int_client[64], int_port[16], desc[64], proto[16], enabled[16];unsigned?int?duration;error =?UPNP_GetSpecificPortMappingEntry(urls.controlURL,data.first.servicetype,external_port,protocol,nullptr,int_client, int_port, desc, enabled, &duration);if?(error ==?0) {std::cout <<?"🔍 Port mapping verified:"?<< std::endl;std::cout <<?" ?Internal Client: "?<< int_client << std::endl;std::cout <<?" ?Internal Port: "?<< int_port << std::endl;std::cout <<?" ?Description: "?<< desc << std::endl;std::cout <<?" ?Enabled: "?<< enabled << std::endl;std::cout <<?" ?Duration (sec): "?<< duration << std::endl;}?else?{std::cerr <<?"?? ?Could not verify port mapping."?<< std::endl;}// 6. (可選)刪除端口映射std::cout <<?"Press Enter to remove the port mapping...";std::cin.get();error =?UPNP_DeletePortMapping(urls.controlURL,data.first.servicetype,external_port,protocol,nullptr);if?(error ==?0) {std::cout <<?"🗑? ?Port mapping removed."?<< std::endl;}?else?{std::cerr <<?"Failed to remove port mapping."?<< std::endl;}// 清理資源FreeUPNPUrls(&urls);freeUPNPDevlist(devlist);return?0;
}

總結:

  • UDP 穿透:是目前最成熟、最廣泛使用的 NAT 穿透方式,尤其適用于實時通信。
  • TCP 穿透:實現難度高,成功率受限,但在必須使用 TCP 的 P2P 場景中有其價值。
  • UPnP 穿透:最簡單高效,適合家庭內網環境,但因安全問題在企業網絡中不推薦。

在實際系統(如 WebRTC)中,通常會結合多種技術(如 ICE 框架)優先嘗試 UDP 打洞,失敗后回退到中繼(TURN)或嘗試 TCP 打洞等方式,以最大化連接成功率。

往期推薦

為什么很多人勸退學 C++,但大廠核心崗位還是要 C++?

手撕線程池:C++程序員的能力試金石

【大廠標準】Linux C/C++ 后端進階學習路線

打破認知:Linux管道到底有多快?

C++的三種參數傳遞機制:從底層原理到實戰

順時針螺旋移動法 | 徹底弄懂復雜C/C++嵌套聲明、const常量聲明!!!

阿里面試官:千萬級訂單表新增字段,你會怎么弄?

C++內存模型實例解析

字節跳動2面:為了性能,你會犧牲數據庫三范式嗎?

字節C++一面:enum和enum class的區別?

Redis分布式鎖:C++高并發開發的必修課

C++內存對齊:從實例看結構體大小的玄機

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/95281.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/95281.shtml
英文地址,請注明出處:http://en.pswp.cn/web/95281.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

工業 5G + AI:智能制造的未來引擎

工業 5G AI&#xff1a;智能制造的未來引擎 文章目錄工業 5G AI&#xff1a;智能制造的未來引擎摘要一、為什么工業需要 5G&#xff1f;二、工業 5G 的典型應用場景1. 智能制造工廠2. 遠程控制與運維3. 智慧物流與倉儲4. 能源、電力、礦山5. 智慧港口與交通三、成功案例解析1…

邊緣計算設備 RK3576芯片

RK3576是瑞芯微&#xff08;Rockchip&#xff09;公司專為人工智能物聯網&#xff08;AIoT&#xff09;市場精心設計的一款高算力、高性能及低功耗的國產化應用處理器。該處理器采用了先進的ARM架構&#xff0c;集成了四個ARM Cortex-A72高性能核心與四個ARM Cortex-A53高效能核…

ROS1系列學習筆記之T265的Python數據訂閱顯示、串口輸出到凌霄飛控,以及開機自啟動設置等一些問題處理方法(持續更新)

前言 關于T265的環境配置與安裝&#xff0c;在前兩期的ROS筆記中已經提及&#xff0c;包括英特爾本家的SDK安裝&#xff0c;以及對應支持版本的ROS支持開發工具包。 ROS1系列學習筆記之Linux&#xff08;Ubuntu&#xff09;的環境安裝、依賴準備、踩坑提示&#xff08;硬件以…

UART控制器——ZYNQ學習筆記14

UART 控制器是一個全雙工異步收發控制器&#xff0c; MPSoC 內部包含兩個 UART 控制器&#xff0c; UART0 和 UART1。每一個 UART 控制器支持可編程的波特率發生器、 64 字節的接收 FIFO 和發送 FIFO、產生中斷、 RXD 和TXD 信號的環回模式設置以及可配置的數據位長度、停止位和…

C++ 登錄狀態機項目知識筆記

C 登錄狀態機項目知識筆記 1. 項目源碼 1.1 login_state_machine.h #pragma once#include <string>// 登錄狀態枚舉 enum class LoginState { IDLE, AUTHENTICATING, SUCCESS, FAILURE, LOCKED };// 登錄事件枚舉 enum class LoginEvent { REQUEST, SUCCESS, FAILURE, RE…

docker-nacos-v3

nacos官網&#xff1a; Redirecting to: https://nacos.io/ 服務發現和服務健康監測 Nacos 支持基于 DNS 和基于 RPC 的服務發現。服務提供者使用 原生SDK、OpenAPI、或一個獨立的Agent TODO注冊 Service 后&#xff0c;服務消費者可以使用DNS TODO 或HTTP&API查找和發現服…

DevOps 詳解:文化、實踐與工具鏈

目錄一、DevOps 定義與核心目標二、DevOps 關鍵原則與實踐1. 持續集成&#xff08;CI&#xff0c;Continuous Integration&#xff09;2. 持續交付&#xff08;CD&#xff0c;Continuous Delivery&#xff09;3. 持續部署&#xff08;Continuous Deployment&#xff09;4. 監控…

人工智能之數學基礎:常用的連續型隨機變量的分布

本文重點 本文將介紹概率中非常重要的連續型隨機變量的分布,主要有均勻分布、指數分布、正態分布 均勻分布 若隨機變量X的概率密度為: 如果概率密度函數如上所示,則稱X服從區間[ a, b]上的均勻分布,記作X~U[a,b] 均勻分布的概率密度函數的計算如下: 指數分布 指數分布…

【開題答辯全過程】以 校園幫幫團跑腿系統的設計與實現為例,包含答辯的問題和答案

個人簡介一名14年經驗的資深畢設內行人&#xff0c;語言擅長Java、php、微信小程序、Python、Golang、安卓Android等開發項目包括大數據、深度學習、網站、小程序、安卓、算法。平常會做一些項目定制化開發、代碼講解、答辯教學、文檔編寫、也懂一些降重方面的技巧。感謝大家的…

Milvus 向量數據庫開發實戰指南

Milvus向量數據庫是什么&#xff1f;-CSDN博客 一、核心概念解析 1.1 基礎概念 1.1.1 Bitset&#xff08;位集&#xff09; 高效的數據表示方式&#xff0c;使用位數組替代傳統數據類型 默認情況下&#xff0c;位值根據特定條件設置為 0 或 1 1.1.2 通道機制 PChannel&am…

vcruntime140.dll丟失解決辦法

解決辦法 安裝Microsoft Visual C Redistributable https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?viewmsvc-170

LabVIEW實現跨 VI 簇按鈕控制功能

?在 LabVIEW 開發場景中&#xff0c;常需實現不同 VI 間的交互操作。本功能借助 VI Server 技術&#xff0c;突破 VI 邊界&#xff0c;實現對目標 VI 中簇內按鈕控件的屬性讀取與控制&#xff0c;為多 VI 協同、對VI里已經實現的功能&#xff0c;可以在其他VI中直接使用&#…

JS箭頭函數

JavaScript 的箭頭函數 (Arrow Function) 是 ES6 (ECMAScript 2015) 引入的一種重要的函數語法特性&#xff0c;它用更簡潔的方式定義函數&#xff0c;并改變了 this 的綁定行為。 箭頭函數和傳統函數的主要區別&#xff1a;特性箭頭函數傳統函數語法更簡潔&#xff0c;省略 fu…

linux內核 - 文件系統相關的幾個概念介紹

介紹文件系統之前&#xff0c;先了解下存儲管理的幾個概念&#xff1a;1. 硬盤&#xff1a;是最底層的存儲介質&#xff0c;比如 /dev/sda, /dev/nvme0n1. 一個物理硬盤就是一個塊設備&#xff0c;未經處理是只能順序讀寫二進制數據。 2. 分區&#xff1a;就是在硬盤上劃分出不…

邊緣計算(Edge Computing)+ AI:未來智能世界的核心引擎

邊緣計算&#xff08;Edge Computing&#xff09; AI&#xff1a;未來智能世界的核心引擎 文章目錄邊緣計算&#xff08;Edge Computing&#xff09; AI&#xff1a;未來智能世界的核心引擎摘要什么是邊緣計算&#xff1f;為什么需要邊緣計算&#xff1f;1. 延遲問題2. 帶寬壓力…

計算機視覺與深度學習 | ORB-SLAM3算法原理與Matlab復現指南

文章目錄 一、算法核心原理 1.1 系統架構概述 1.2 數學模型基礎 1.2.1 狀態估計框架 1.2.2 視覺-慣導融合模型 1.3 關鍵創新點 二、關鍵模塊實現細節 2.1 ORB特征提取與匹配 2.2 地圖初始化 2.3 視覺-慣導融合 2.4 回環檢測與優化 三、Matlab復現思路 3.1 系統模塊劃分 3.2 核心…

分布式光伏模式怎么選?從 “憑經驗” 到 “靠數據”,iSolarBP 幫你鎖定最優解

iSolarBP-陽光新能源旗下分布式光伏光儲智能評估設計軟件 iSolarBP是陽光新能源打造的分布式光伏/光儲項目智能設計平臺。提供無人機自動勘測、3D建模、高精度發電仿真、光儲容量優化與經濟分析一站式服務&#xff0c;助力開發者提升效率、降低成本和優化投資收益。https://iso…

MATLAB R2010b系統環境(四)MATLAB幫助系統

一、幫助命令MATLAB幫助命令包括help、lookfor以及模糊查詢。1.1 help命令在命令窗口中直接輸入help或help加函數名。&#xff08;1&#xff09;help&#xff1a;顯示當前幫助系統中所包含的所有項目&#xff0c;即搜索路徑中所有的目錄名稱&#xff0c;如下圖&#xff1a;&…

“便農惠農”智慧社區系統(代碼+數據庫+LW)

摘要 隨著城市化進程加速和信息技術快速發展&#xff0c;傳統社區管理模式已難以滿足現代社區高效管理和居民多元化服務需求。為解決社區管理中的信息孤島問題、提升服務效率并增強居民生活體驗&#xff0c;本文設計并實現了一套基于Spring Boot框架的智慧社區管理系統。該系統…

智慧金融服務平臺問題剖析與改進策略

智慧金融服務平臺問題剖析與改進策略 在數字化浪潮的推動下&#xff0c;智慧金融服務平臺蓬勃發展&#xff0c;為用戶帶來了便捷的金融服務體驗。然而&#xff0c;隨著用戶數量的不斷增加和業務的日益復雜&#xff0c;平臺也暴露出一些問題&#xff0c;其中數據準確性不足、異常…