C++对称NAT网络穿透原理与实现

最近有意向写一个p2p的分布式应用通讯架构,在构思测试应用间通讯功能的时候遇到了NAT网络问题。在网上找了很多资料,都只有锥形NAT的穿透原理。然并软,经测试得知学校的网络是`对称型NAT`,无奈之下只好自己推导了,我就不信在有内应(内网主机)的情况下不能成功。

以下为成功之后,我得出的方法。


该方法的功能与局限性

1.需要一台具有公网IP的主机/服务器

2.支持两台内网主机在udp(测试成功), tcp(理论上,未测试)协议上进行通讯

3.无法针对某个固定的端口通讯(无法确定双方连上的端口号)

注:其实也可以确定端口号,不过在实现上很麻烦(现在用不上所有没研究)

4.只适用于线性分配端口的对称NAT网络,对于随机的对称NAT或者更加复杂的类型估计无法成功,具体见下文链接(写文章找原理链接的时候发现的一篇牛文,为什么以前没找到....)P2P打洞技术详解_rebootcat的博客-CSDN博客_p2p打洞


方法原理

1.NAT类型介绍 

可以在以下链接了解:NAT的几种类型 - 知乎 (zhihu.com)

2.穿透打洞原理与实现细节

因本人技术水平有限,此方法打洞可能有点费系统资源。

一下会附上一些必要代码, 但是整个程序需要读者自己实现

角色:服务器 S (Linux)        内网主机A (Windows)       内网主机B(Windows)       

使用UDP

(1) 首先位于公网的服务器S监听固定的端口 p0

    in_port_t port = p0;
	
	struct sockaddr_in serv_addr;//服务器地址结构体
	memset(&serv_addr, 0, sizeof(serv_addr));

	//设置本地服务器要监听的地址和端口
	serv_addr.sin_family = AF_INET;               //选择协议族为IPV4
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址

	sock = socket(AF_INET, SOCK_DGRAM, 0);
	//建立socket套接字
	if (sock == -1)
	{
		//套接字建立失败记录日志;
		cout << "The tcp port " << to_string(port) << " socket establish failed:> " <<         strerror(errno) << endl;

		return 1;
	}

	//设置要监听的地址与端口
	serv_addr.sin_port = htons(port);

	//绑定服务器地址结构
	if (bind(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
	{
		//服务器地址结构绑定失败,记录日志
		cout << "The tcp port " << to_string(port) << " bind failed:> " << strerror(errno) << endl;

		return 1;
	}

(2)  A & B 分别连接服务器 S

    SOCKET fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (fd == SOCK_CREATE_ERR)
	{
		cerr << "create socket failed, " << GetErrorInfo(ERROR_CODE) << endl;
		return -1;
	}

	struct sockaddr_in addr;					
	memset(&addr, 0, addrsize);

	addr.sin_family = AF_INET;	
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);

    sendto(fd, buf, buflen, 0, (const sockaddr*)addr, sizeof(addr));
    //buf 与 buflen需要读者自己设计

(3) 服务器接收到A & B的请求,创建心跳线程维护连接(防止内网网关转发表更新)

   //这是写在线程函数内的内容,我用的是std::thread创建的线程
    while (!isExit)
	{
		sendto(sock, NULL, 0, 0, (const sockaddr*)caddr, addrsize);

		usleep(500000);
	}

(4) 客户端A 向 服务器S 发送对 客户端B 的连接请求(请求的方式须读者设计)

(5) 服务器收到请求后, 分别向客户端A & B 发送端口`预测请求`

`预测请求` :: 让 A & B 用一个新的套接字向其发送信息,以获取目前网关开放端口情况

//必要结构体声明
#pragma pack(1)//设置系统默认对齐字节序为 1

typedef struct
{
	uint16_t port;			 		//报文总长
	char ip[16];					//用于区分报文内容
}RegistInfo;

#pragma pack()//还原系统默认对其字符序

typedef struct
{
	RegistInfo* info;
	sockaddr_in* caddr;
	string other;
}UserInfo;

uint16_t RSize = sizeof(RegistInfo);

unordered_map<string, UserInfo*> infomap;

/*************************************************************************************/

//端口预测请求发送,写在实现函数内

tmpIp = inet_ntoa(caddr->sin_addr);
UserInfo* tmpinfo = infomap[tmpIp];

forwordPort = *((uint16_t*)(buffer + 8));

sockaddr_in* tmpaddr = infomap[tmpinfo->other]->caddr;
			
sendto(sock, "port_forecast", 14, 0, (const sockaddr*)tmpaddr, addrsize);
sendto(sock, "port_forecast", 14, 0, (const sockaddr*)caddr, addrsize);

(6) 服务器接收到 A & B 发送的新信息后, 立刻将 A 的端口现状给 B , B 的给 A

(7) A 接收到 B 的端口现状 `m`后, 创建 Ka 个线程用不同的 `socket` 分别连接 B 端的 m+1 —> m + Ka 的端口(Ka个线程连接Ka个端口)

(8) B 接收到 A 的端口现状`n`后,创建Kb个线程用不同的 `socket` 分别连接 A 端的 n + Kb 端口(Kb个线程连接n + Kb这一个端口)

注:因为 A B 所在的NAT网络为线性分配端口的对称NAT网络, 所以在 AB 进行 P2P 连接前,服务器要先得知ab的端口分配规律, 现假设

A端口分配规律为 :: nextPort_A = currPort_A + e

B端口分配规律为 :: nextPort_A = currPort_A + f

e,f为无符号整型

则, Ka = e * num, Kb = f * num。num为预期的端口变化值,由程序编写者自身设置

附上这套流程的示意图

A B 将要连接的端口由服务器通知 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值