要說網絡世界里的 “幕后功臣”,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++內存對齊:從實例看結構體大小的玄機