用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杀手信息吧!