当前位置: 首页 > news >正文

‌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.xdnf.cn/news/1419247.html

相关文章:

  • Python核心技术开发指南(033)——函数的嵌套
  • 【LeetCode 热题 100】5. 最长回文子串——中心扩散法
  • 数组基础及原理
  • NoteGen – 跨平台 AI 笔记应用,支持截图、插图和文本输入记录方式
  • 从零开始学习n8n-定时器+HTTP+飞书多维表格(下)
  • 在 Halo 中导入 Markdown 和 Word 文档
  • Go语言入门学习笔记
  • React前端开发笔记合集
  • Go 语言 sync 包解析
  • 三消消乐益智小游戏抖音快手微信小程序看广告流量主开源
  • 前端安全防护深度实践:从XSS到CSRF的完整安全解决方案
  • 大模型落地:从微调到部署的全景式实战指南
  • DAY02:【DL 第一弹】pytorch
  • 宋红康 JVM 笔记 Day09|方法区
  • 【阿里云实战】基于MQTT的Java SDK收发消息-终端和终端消息收发
  • 汽车曲柄连杆机构cad+ea113+设计说明书
  • 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第八章知识点问答(18题)
  • 从理论到RTL,实战实现高可靠ECC校验(附完整开源代码/脚本)(3) RTL实现实战
  • DBeaver社区版AI助手(AI Assistant)设置
  • 基于Hadoop与层次聚类技术的电子游戏销售分析系统的设计与实现
  • 机器翻译:python库PyGTranslator的详细使用
  • (论文速读)3DTopia-XL:高质量3D资产生成技术
  • FOUPK3云服务平台旗下产品
  • ARM-进阶汇编指令
  • linux安装gitlab详细教程,本地管理源代码
  • 存储掉电强制拉库引起ORA-01555和ORA-01189/ORA-01190故障处理---惜分飞
  • 英伟达Newton与OpenTwins如何重构具身智能“伴随式数采”范式
  • 【ElasticSearch实用篇-04】Boost权重底层原理和基本使用
  • Ruoyi项目MyBatis升级MyBatis-Plus指南
  • linux:离线/无网环境安装docker