1. NAT分类
根据Stun协议(RFC3489),NAT大致分为下面四类
1) Full Cone
这种NAT内部的机器A连接过外网机器C后,NAT会打开一个端口.然后外网的任何发到这个打开的端口的UDP数据报都可以到达A.不管是不是C发过来的.
例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
任何发送到 NAT(202.100.100.100:8000)的数据都可以到达A(192.168.8.100:5000)
2) Restricted Cone
这种NAT内部的机器A连接过外网的机器C后,NAT打开一个端口.然后C可以用任何端口和A通信.其他的外网机器不行.
例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
任何从C发送到 NAT(202.100.100.100:8000)的数据都可以到达A(192.168.8.100:5000)
3) Port Restricted Cone
这种NAT内部的机器A连接过外网的机器C后,NAT打开一个端口.然后C可以用原来的端口和A通信.其他的外网机器不行.
例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
C(202.88.88.88:2000)发送到 NAT(202.100.100.100:8000)的数据都可以到达A(192.168.8.100:5000)
以上三种NAT通称Cone NAT.我们只能用这种NAT进行UDP打洞.
4) Symmetic
对于这种NAT.连接不同的外部目标.原来NAT打开的端口会变化.而Cone NAT不会.虽然可以用端口猜测.但是成功的概率很小.因此放弃这种NAT的UDP打洞.
2. UDP hole punching
对于Cone NAT.要采用UDP打洞.需要一个公网机器C来充当”介绍人”.内网的A,B先分别和C通信.打开各自的NAT端口.C这个时候知道A,B的公网IP: Port. 现在A和B想直接连接.比如A给B发.除非B是Full Cone.否则不能通信.反之亦然.但是我们可以这样.
A要连接B.A给B发一个UDP包.同时.A让那个介绍人给B发一个命令,让B同时给A发一个UDP包.这样双方的NAT都会记录对方的IP,然后就会允许互相通信.
3. 同一个NAT后面的情况
如果A,B在同一个NAT后面.如果用上面的技术来进行互连.那么如果NAT支持loopback(就是本地到本地的转换),A,B可以连接,但是比较浪费带宽和NAT.有一种办法是,A,B和介绍人通信的时候,同时把自己的local IP也告诉服务器.A,B通信的时候,同时发local ip和公网IP.谁先到就用哪个IP.但是local ip就有可能不知道发到什么地方去了.比如A,B在不同的NAT后面但是他们各自的local ip段一样.A给B的local IP发的UDP就可能发给自己内部网里面的某某某了.
Client1 --- Server --- Client2
如果,c1与c2需要进行p2p,c1通过某中途径告诉c2。
然后,c1向s发握手包,握手包的内容为请求c2的外网ip和port。在一个超时的时间范围内,不停的发这种握手包。
同时,c2已经得到了通知,也向s发握手包,握手包的内容为请求c1的外网ip和port,同上。
s一旦在内存中查找到了c2的ip和port,就会把这些消息回给c1,这时,c1停止向s发握手包,转而向c2发握手包。
同样,如过s在内存中发现了c1的ip和port,就会把这些消息回给c2,同上。
现在已经进入了c1和c2的握手阶段,c1一旦接收到c2的握手包,或c2一旦接收到c1的握手包,就可以宣布握手成功,可以进行下一步的动作了。
---------------------------------------------------------------
呵呵,“打洞”原理:
只有NAT确信内部想与外部通信,NAT才会让外部的数据包进入内部。那么NAT是靠什么“确信内部想与外部通信”,就是内部发送一个数据包到这个“外部地址”
比如下面的网络环境:
Client A ->Nat A->....internet <-Nat B<-Client B
比如Client A想发送给Client B一个信息“Hello”。那么直接给Client B的公网地址发送信息,会被NAT B丢掉,那么怎么让NAT B不丢掉这个信息呢?就是告诉NAT B“Client B确实想与Client A通信”,如果告诉NAT B?简单,让Client B发送一个“信息”到Client A的公网地址就好(这个信息就是“打洞”包)
//作者dzend
//form http://www.dzend.com
//版权所有,未经许可不得转载
过程:
1 Client A告诉Server : 我想与Client B通信
2 Server通知Client B :Client A想与你通信
3 Client B发送一个“打洞包”到Client A的公网地址(这个打洞包不需要携带任何有效信息)
4 Client A发送“Hello”到Client B,由于第三步的原因,NAT不会拒绝这个请求,通信成功。
udp 打洞 udp hole puching
关键词: udp 打洞 , NAT 穿越/穿透
用QQ进行视频或者语音聊天的时候总看到他窗体上显示一条信息说 udp直连已经建立起来。还有就是用bitcomet拉东西的时候有时也会看到下面有提示说端口已经允许外网连接之类的。因为我是共享Adsl上网,所以感到有点奇怪,我是内网ip ,按理说是不能让外网连上的,除非网管做了端口映射。可是我那个网关哪有那么好人,你一拉BT,估计他就想拔你的网线了,怎么会帮你做端口映射?
联想到以前那些招聘信息一提到网络编程的总是说懂什么什么穿透防火墙的技术啊等等。所以上网查了下资料,发现果然有一种方法是可以实现内网直连的。这就是“udp打洞”。不过就我所看的资料看来,应该是穿透NAT设备不是穿透防火墙。
NAT(Network Address Translators) 中文意思就是网络地址转换。这个是一个标准,如果想详细了解那就去查看官方文档吧,直接在google里搜索“NAT” 也可以找的到很多资料的。常见的NAT又有 NAPT(Network Address/Port Translator),就是不但把内网地址转换了,也转换了端口了。 我也没有仔细去读文档,反正知道NAT是把内网地址和端口 转换成外网(公网)ip和端口的东西就行了。而NAT设备的工作就是根据内网的ip和端口生成新的NAT设备ip (公网的)和端口。这样外网发送到NAT设备ip 和端口上的数据,NAT会自动转发给对应的内网的ip和端口。当然内网发出的数据,NAT设备以会通过生成的NAT ip 端口转发到外网ip和端口。这样内网用户就可以和外网用户连接起来了。
_________________________________
| 外网 server |
| ip :202.16.38.49 port : 55555 |
|________________________________|
^ ^
| |
V V
______________________________ ______________________________
| NAT 设备 A | | NAT 设备 B |
| ip :202.45.45.45 port : 44444 | | ip :202.16.22.45 port : 33333 |
|______________________________| |_____________________________|
^ ^
| |
V V
___________________________ _____________________________
|内网 ClientA | | 内网 ClientB |
| ip :196.168.0.5 port : 4567 | | ip :196.168.0.11 port : 4567 |
|___________________________| |____________________________|
举个例子,比如我的电脑(clientA,内网ip 196。) 是采用ADSL共享上网,那么我的电脑要去连外网的服务器( ip :202.16.38.49 port : 55555),那么ClientA就会连到内网IP为192.168.0.1 的路由器(路由器实现了NAT功能,也算是是NAT设备)。那么路由器就会根据我发起请求的来源( ip :196.168.0.5 port : 4567) 生成一个外网的ip和端口( ip :202.45.45.45 port : 44444 )。然后用这个ip端口去连接外网服务器(ip :202.16.38.49 port : 55555)。然后等会外网服务器发送回来到 ip :202.45.45.45 port : 44444上的数据,路由器就自动转发到内网的 ip :196.168.0.5 port : 4567。这样连接就建立起来了。同样ClientB要连到外网服务器也要 NAT 设备 B 的协作。
现在如果ClientA 要去连ClientB,那要怎么办呢?直接 给clientB的内网 ip :196.168.0.11 port : 4567 发送信息,那肯定是不行的了,clientB不在同一个内网里面就连不起来了。那马上想到的就是把直接给NAT 设备 B的 ip :202.16.22.45 port : 33333发送信息了,因为NAT生成的ip是公网的ip可以连起来,而且前面说了NAT设备B会自动发这个ip :202.16.22.45 port : 33333接受到的数据转发给ClientB(ip :196.168.0.11 port : 4567)。但这里有几个问题。1、ClientA不知道NAT 设备 B 生成的ip和端口。2、NAT设备会自动拒绝位置的数据,也就是说,应为 clientB首先主动的到server(ip :202.16.38.49 port : 55555 )所以server发送回来的数据,NAT设备是会转发给ClientB的。但其他的ip包括ClientA发送给NAT 设备 B ip :202.16.22.45 port : 33333 的 数据是会被丢弃的。
而有UDP打洞的这么一种方法刚好可以解决这两个问题:ClientA和ClientB首先连到server上,这样server端就可以得到NAT生成的两个ip和端口( ip :202.45.45.45 port : 44444 , ip :202.16.22.45 port : 33333 ),然后server把这个NAT的ip和端口数据转发给ClientA和ClientB。然后ClientA主动给NAT 设备 B ip :202.16.22.45 port : 33333端口发送信息,因为是ClientA主动发起请求,所以等会NAT 设备 B ip :202.16.22.45 port : 33333发送到 NAT 设备 A ip :202.45.45.45 port : 44444 的数据就不会被NAT 设备 A抛弃了。同时ClientB也要做同样的事情,这样NAT设备A上发送到clientB的对应端口上的数据才不会被clientB丢弃。这样 ClientA和ClientB的直连就建立起来了,clientA要给ClientB发送数据,只要往 NAT 设备 B ip :202.16.22.45 port : 33333发送就行了。ClientB要给clientA发送数据同理,给NAT设备A对应的端口发送就行了,NAT设备A会自动转发给内网 ClientA 。这就是“UDP打洞”。
不过可以看出udp打洞这种技术是有一个前提的,那就是 NAT 设备A 为 clinetA 主动发起连接到server 和ClientA主动发起连接到 NAT 设备 B ip :202.16.22.45 port : 33333生成的ip和端口号都一样是 “ip :202.45.45.45 port : 44444 ”。同样 NAT 设备B也要为ClientB相同端口发起连接到不同外网ip和端口的请求生成同样的NAT设备的ip和端口。 这就是对NAT设备的要求,并不是所有的NAT设备都满足这个要求的。这里有个术语称这种满足要求的NAT设备叫做完成锥形NAT设备。很幸运,大多数的NAT设备都是这种,所以 QQ视频和P2P软件可以工作的很好。
要说的是,关于这个NAT也有一种协议叫做UPnP协议。通过UPnP协议,内网的电脑可以得到自己的NAT转换之后的公网地址。很明显,有了这种协议,也就不用通过UDP打洞这种技巧性的技术了! 我发现Bitcomet 等Bt软件也都是有支持UPnP协议的。不过好像看起来UPnP协议和复杂的样子哦,当然也很强大了!看网上文章说其很多NAT设备都还不支持UPnP协议,所以即使采用了用处也不大。我也不了解现在的情况到底是怎样的,我又不是专门做网络这一块的,所以就没看了。有个UDP 打洞可以用就不错了,呵呵!
-------------------------
说得不清不楚,看一下维客的解释吧,最初我看到的udp打洞的文章也就在这里
在计算机科学中,UDP打洞指的是一种普遍使用的NAT穿越技术。
描述
通过UDP打洞实现NAT穿越是一种在处于使用了NAT的私有网络中的Internet主机之间建立双向UDP连接的方法。由于NAT的行为是非标准化的,因此它并不能应用于所有类型的NAT。
其基本思想是这样的:让位于NAT后的两台主机都与处于公共地址空间的、众所周知的第三台服务器相连,然后,一旦NAT设备建立好UDP状态信息就转为直接通信,并寄希望于NAT设备会在分组其实是从另外一个主机传送过来的情况下仍然保持当前状态。
这项技术需要一个完全圆锥型NAT设备才能够正常工作。受限圆锥型NAT和对称型NAT都不能使用这项技术。
这项技术在P2P软件和VoIP电话领域被广泛采用。它是Skype用以绕过防火墙和NAT设备的技术之一。
相同的技术有时还被用于TCP连接——尽管远没有UDP成功。
算法
假设有两台分别处于各自的私有网络中的主机:A和B;N1和N2是两个NAT设备;S是一个使用了一个众所周知的、从全球任何地方都能访问得到的IP地址的公共服务器
步骤一:A和B分别和S建立UDP连接;NAT设备N1和N2创建UDP转换状态并分配临时的外部端口号
步骤二:S将这些端口号传回A和B
步骤三:A和B通过转换好的端口直接联系到各自的NAT设备;NAT设备则利用先前创建的转换状态将分组发往A和B
----------------------------------
写了个测试代码,不过找不到人来测试,竟然问遍了所有的QQ好友也找不到一个有公网IP的来测试服务端。如果谁对这个感兴趣可以找我一起测试一下了。
-----server.cpp--------------------------------
#pragma comment(lib, "ws2_32.lib")
#include <winsock2.h>
#include <stdio.h>
struct MyMessage
{
int type;
union Content
{
char text[256];
sockaddr_in client;
}content;
};
void main ()
{
MyMessage RecvBuf;
SOCKET sock;
WSADATA WSAData;
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
{
printf("Windows sockets 2.2 Startup error\n");
}
else
{
printf("Windows sockets Startup 通过\n");
}
//SOCK_DGRAM Supports unreliable connectionless datagram communication
sock = socket(AF_INET,SOCK_DGRAM, 0);
if (sock == INVALID_SOCKET )
{
printf("socket create fail !\n");
}
// 填充SOCKADDR_IN结构
sockaddr_in addr_in;
addr_in.sin_addr.s_addr = htonl(INADDR_ANY);
addr_in.sin_family = AF_INET;
addr_in.sin_port = htons(55555); //服务器绑定开通的端口号
// 把sock 绑定到本地地址上
if( 0!=bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in)))
{
printf("bind failed ! \n");
}
else
{
printf("服务器已经启动了! \n");
}
sockaddr_in clientA;
clientA.sin_port=0;
while (true)
{
sockaddr_in sender;
int dwSender = sizeof(sender);
int ret = recvfrom(sock,(char *) &RecvBuf, sizeof(MyMessage),0, (sockaddr *)&sender,& dwSender);
if (ret > 0)
{
switch ( RecvBuf.type )
{
int iResult;
case 0: //接收到客户端发过来登陆消息
if (clientA.sin_port==0) //记录第一个登录的客户端的ip和端口。
{
printf ("clientA login\n");
printf ("ip=%s,port=%d\n",inet_ntoa(sender.sin_addr),ntohs(sender.sin_port));
MyMessage tempMessage;
//发送登录确认消息
tempMessage.type =1;
memcpy ( &tempMessage.content.text,"你已经连上服务器了,第一个连上的标记为clientA" ,sizeof ("你已经连上服务器了,第一个连上的标记为clientA" ));
iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&sender, sizeof(sender));
//记录客户端的ip和端口
memcpy (&clientA,&sender ,sizeof (sender));
}
else //第二个客户端也登录了,则分别转发ip和端口给对方,完成udp打洞
{
printf ("clientB login\n");
printf ("ip=%s,port=%d\n",inet_ntoa(sender.sin_addr),ntohs(sender.sin_port));
MyMessage tempMessage;
//发送登录确认消息
tempMessage.type =1;
memcpy ( &tempMessage.content.text,"你已经连上服务器了,第二个连上的标记为clientB" ,sizeof ("你已经连上服务器了,第一个连上的标记为clientA" ));
iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&sender, sizeof(sender));
tempMessage.type =0;
//给clientA发送ClientB的ip和端口
memcpy (&tempMessage.content.client, &sender,sizeof (sender));
iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&clientA, sizeof(clientA));
//给clientB发送ClientA的ip和端口
memcpy (&tempMessage.content.client, &clientA,sizeof (clientA));
iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&sender, sizeof(sender));
}
break;
case 1: //接受到 文本消息
printf ("ip=%s,port=%d\n",inet_ntoa(sender.sin_addr),ntohs(sender.sin_port));
printf("%s\n",RecvBuf.content.text);
break;
//case 2: //没有意义的连通确认消息,要回应一条以保持UDP连接的有效性,有的NAT设备要求一定时间内有
//
// printf("收到一条维持连通消息\n");
// MyMessage tempMessage;
// tempMessage.type =2;
// iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&sender, sizeof(sender));
// break;
default:
printf("收到未知消息\n");
break;
}
}
else if (ret == 0)
{
printf ("the connection has been gracefully closed\r\n");
}
}
}
--------------server.cpp------------------------------------------------
------------------------------client.cpp-------------------------------------------------------------------
#pragma comment(lib, "ws2_32.lib")
#include <winsock2.h>
#include <stdio.h>
struct MyMessage
{
int type;
union Content
{
char text[256];
sockaddr_in client;
}content;
};
void main ()
{
MyMessage RecvBuf;
SOCKET sock;
WSADATA WSAData;
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
{
printf("Windows sockets 2.2 Startup error\n");
}
else
{
printf("Windows sockets Startup 通过\n");
}
//SOCK_DGRAM Supports unreliable connectionless datagram communication
sock = socket(AF_INET,SOCK_DGRAM, 0);
if (sock == INVALID_SOCKET )
{
printf("socket create fail !\n");
}
// 填充SOCKADDR_IN结构
sockaddr_in addr_in;
addr_in.sin_addr.s_addr = htonl(INADDR_ANY);
addr_in.sin_family = AF_INET;
addr_in.sin_port =0; //让系统自己指定空闲端口
// 把sock 绑定到本地地址上
if( 0!=bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in)))
{
printf("bind failed ! \n");
}
else
{
printf("客户端已经启动了! \n");
}
//----------------------
//登录服务器,就是给服务器发送一条消息而已
printf("请输入服务器ip:");
char serverip[16] ;
scanf ("%16s",serverip);
printf("请输入服务器端口号:");
int serverport;
scanf("%d",&serverport);
sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(serverip);
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
MyMessage loginMessage;
loginMessage.type =0;
sendto(sock, (const char*)&loginMessage, sizeof(loginMessage), 0, (const sockaddr*)&server,sizeof(server));
//-------------------
sockaddr_in client;
while (true)
{
sockaddr_in sender;
int dwSender = sizeof(sender);
int ret = recvfrom(sock,(char *) &RecvBuf, sizeof(MyMessage),0, (sockaddr *)&sender,& dwSender);
if (ret > 0)
{
switch ( RecvBuf.type )
{
int iResult;
case 0: //收到服务器传来的打洞消息,即是收到另外一个客户端的NAT 设备的Ip和端口
memcpy (&client, &RecvBuf.content.client,sizeof (client));
printf("收到服务器发过来的打洞消息,得到另外一个客户端的ip和端口了");
printf ("ip=%s,port=%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
MyMessage tempMessage;
//发送打洞消息,给指定的ip发送信息之后,NAT设备才会打开相应的端口,这样才能
//收到这个ip发过来的消息,不然 其他ip发送的消息会被NAT设备丢弃
tempMessage.type =1;
memcpy ( &tempMessage.content.text,"另外一个客户端给你发来打洞消息" ,sizeof ("另外一个客户端给你发来打洞消息" ));
iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&client, sizeof(client));
//发送一条维持连通的消息
tempMessage.type =2;
iResult = sendto(sock, (const char *)&tempMessage, sizeof(tempMessage), 0, (const sockaddr*)&client, sizeof(client));
break;
case 1: //接受到 文本消息
printf ("ip=%s,port=%d\n",inet_ntoa(sender.sin_addr),ntohs(sender.sin_port));
printf("%s\n",RecvBuf.content.text);
MyMessage tempMessage1;
tempMessage1.type =1;
printf ("输入你要给对方发送的消息:");
scanf("%256s",&tempMessage1.content.text);
iResult = sendto(sock, (const char *)&tempMessage1, sizeof(tempMessage1), 0, (const sockaddr*)&sender, sizeof(sender));
break;
//case 2: //没有意义的连通确认消息,要回应一条以保持UDP连接的有效性,有的NAT设备要求一定时间内有
//
// printf("收到一条维持连通消息\n");
// MyMessage tempMessage2;
// tempMessage2.type =2;
// iResult = sendto(sock, (const char *)&tempMessage2, sizeof(tempMessage2), 0, (const sockaddr*)&sender, sizeof(sender));
// break;
default:
printf("收到未知消息\n");
break;
}
}
else if (ret == 0)
{
printf ("the connection has been gracefully closed\r\n");
}
}
}
-------------------------------client.cpp-------------------------------------------------------------------
道听途说的问题,没测试过:
1) NAT设备开通的端口是有时间限制的,所以直连打通之后,不要两个人都不说话。NAT设备发现相应的端口很长时间都没有收到数据是会自动关闭的。
2) 有的 NAT设备对未知ip发送过来的数据回应一条ICMP信息。想一下吧,当ClientA给ClientB发送第一条数据时,ClientB还没有朝clientA发送主动发起连接,
所以NAT设备B就毫不留情面的拒绝ClientA的请求,同时给ClientA回应一条ICMP消息。NAT设备A收到这条ICMP信息之后就主动关闭 clientA的端口。这样即使ClientB后来再打洞以没用了,这时ClientA已经关闭了。希望不倒霉到碰到这种情况才好!赶紧设置一下NAT设备不要发送这种ICMP杀手信息吧!
参考文章:
1) Wiki 维客 UDP打洞 条目 http://www.wiki.cn/wiki/UDP%E6%89%93%E6%B4%9E
2)P2P 之 UDP穿透NAT的原理与实现(附源代码) 作者:shootingstars (有容乃大,无欲则刚) http://epan.cnblogs.com/epan/articles/98295.html
3)P2P之UDP穿透NAT的原理与实现 - 增强篇(附修改过的源代码) 原始作者: Hwycheng Leo(FlashBT@Hotmail.com)
http://hwycheng.bokee.com/2404843.html
4) UDP Hole Puching技术:穿透防火墙建立UDP连接 http://www.51cto.com/html/2006/0206/20269.htm
5) Peer-to-Peer Communication Across Network Address Translators http://gnunet.org/papers/nat.pdf