`
wujianjun12315
  • 浏览: 111876 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

UDP打洞

阅读更多
P2P 之 UDP穿透NAT的原理与实现(附源代码)

原创:shootingstars
参考:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt

论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念:
    NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。
    最开始NAT是运行在路由器上的一个功能模块。
   
    最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。
    因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)
    关于基本的NAT可以参看RFC 1631
   
    另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:
                                Server S1                        
                         18.181.0.31:1235                         
                                      |
          ^  Session 1 (A-S1)  ^      | 
          |  18.181.0.31:1235  |      |  
          v 155.99.25.11:62000 v      |   
                                      |
                                     NAT
                                 155.99.25.11
                                      |
          ^  Session 1 (A-S1)  ^      | 
          |  18.181.0.31:1235  |      | 
          v   10.0.0.1:1234    v      | 
                                      |
                                   Client A
                                10.0.0.1:1234
    有一个私有网络10.*.*.*,Client A是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDP Socket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
    首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。
    一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样Client A就与Server S1建立以了一个连接。

    呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。
    看看下面的情况:
    Server S1                                     Server S2
18.181.0.31:1235                              138.76.29.7:1235
        |                                             |
        |                                             |
        +----------------------+----------------------+
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
   v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v
                               |
                            Cone NAT
                          155.99.25.11
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
   v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                               |
                            Client A
                         10.0.0.1:1234
    接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?
    这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT,后一种叫做Cone NAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(可以庆幸的是,现在绝大多数的NAT属于后者,即Cone NAT)
  
    好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。
    但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。
    那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个Session),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,(这就是称为UDP Hole Punching的技术)以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。
    现在我们来看看一个P2P软件的流程,以下图为例:

                       Server S (219.237.60.1)
                          |
                          |
   +----------------------+----------------------+
   |                                             |
NAT A (外网IP:202.187.45.3)                 NAT B (外网IP:187.34.1.56)
   |   (内网IP:192.168.0.1)                      | (内网IP:192.168.0.1)
   |                                             |
Client A  (192.168.0.20:4000)             Client B (192.168.0.10:40000)

    首先,Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。
    此时,Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是Server S。
    总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。
   
    注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。
   
    下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopback translation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send username message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。
    程序现在支持三个命令:send , getu , exit
   
    send格式:send username message
    功能:发送信息给username
   
    getu格式:getu
    功能:获得当前服务器用户列表
   
    exit格式:exit
    功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)
       
    代码很短,相信很容易懂,如果有什么问题,可以给我发邮件zhouhuis22@sina.com  或者在CSDN上发送短消息。同时,欢迎转发此文,但希望保留作者版权8-)。
   
    最后感谢CSDN网友 PiggyXP 和 Seilfer的测试帮助




P2PServer.c

1./* P2P 程序服务端

2. *

3.  * 文件名:P2PServer.c

4.  *

5.  * 日期:2004-5-21

6.  *

7.  * 作者:shootingstars(zhouhuis22@sina.com)

8.  *

9.  */

10.#pragma comment(lib, "ws2_32.lib")

11.

12.#include "windows.h"

13.#include "..\proto.h"

14.#include "..\Exception.h"

15.

16. UserList ClientList;

17.

18.void InitWinSock()

19.{

20.  WSADATA wsaData;

21.

22.  if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

23.  {

24.   printf("Windows sockets 2.2 startup");

25.   throw Exception("");

26.  }

27.  else{

28.   printf("Using %s (Status: %s)\n",

29.    wsaData.szDescription, wsaData.szSystemStatus);

30.   printf("with API versions %d.%d to %d.%d\n\n",

31.    LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),

32.    LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));

33.  

34.  }

35.}

36.

37.SOCKET mksock(int type)

38.{

39.  SOCKET sock = socket(AF_INET, type, 0);

40.  if (sock < 0)

41.  {

42.         printf("create socket error");

43.   throw Exception("");

44.  }

45.  return sock;

46.}

47.

48. stUserListNode GetUser(char *username)

49.{

50.  for(UserList::iterator UserIterator=ClientList.begin();

51.       UserIterator!=ClientList.end();

52.        ++UserIterator)

53.  {

54.   if( strcmp( ((*UserIterator)->userName), username) == 0 )

55.    return *(*UserIterator);

56.  }

57.  throw Exception("not find this user");

58.}

59.

60.int main(int argc, char* argv[])

61.{

62.  try{

63.   InitWinSock();

64.  

65.   SOCKET PrimaryUDP;

66.   PrimaryUDP = mksock(SOCK_DGRAM);

67.

68.   sockaddr_in local;

69.   local.sin_family=AF_INET;

70.   local.sin_port= htons(SERVER_PORT);

71.   local.sin_addr.s_addr = htonl(INADDR_ANY);

72.   int nResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));

73.   if(nResult==SOCKET_ERROR)

74.    throw Exception("bind error");

75.

76.   sockaddr_in sender;

77.   stMessage recvbuf;

78.   memset(&recvbuf,0,sizeof(stMessage));

79.

80.   // 开始主循环.

81.   // 主循环负责下面几件事情:

82.   // 一:读取客户端登陆和登出消息,记录客户列表

83.  // 二:转发客户p2p请求

84.  for(;;)

85.   {

86.    int dwSender = sizeof(sender);

87.    int ret = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(stMessage), 0, (sockaddr *)&sender, &dwSender);

88.    if(ret <= 0)

89.    {

90.     printf("recv error");

91.     continue;

92.    }

93.    else

94.    {

95.     int messageType = recvbuf.iMessageType;

96.     switch(messageType){

97.     case LOGIN:

98.      {

99.       // 将这个用户的信息记录到用户列表中

100.      printf("has a user login : %s\n", recvbuf.message.loginmember.userName);

101.       stUserListNode *currentuser = new stUserListNode();

102.       strcpy(currentuser->userName, recvbuf.message.loginmember.userName);

103.       currentuser->ip = ntohl(sender.sin_addr.S_un.S_addr);

104.       currentuser->port = ntohs(sender.sin_port);

105.      

106.       ClientList.push_back(currentuser);

107.

108.       // 发送已经登陆的客户信息

109.      int nodecount = (int)ClientList.size();

110.       sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

111.       for(UserList::iterator UserIterator=ClientList.begin();

112.         UserIterator!=ClientList.end();

113.         ++UserIterator)

114.       {

115.        sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender));

116.       }

117.

118.       break;

119.      }

120.     case LOGOUT:

121.      {

122.       // 将此客户信息删除

123.      printf("has a user logout : %s\n", recvbuf.message.logoutmember.userName);

124.       UserList::iterator removeiterator = NULL;

125.       for(UserList::iterator UserIterator=ClientList.begin();

126.        UserIterator!=ClientList.end();

127.        ++UserIterator)

128.       {

129.        if( strcmp( ((*UserIterator)->userName), recvbuf.message.logoutmember.userName) == 0 )

130.        {

131.         removeiterator = UserIterator;

132.         break;

133.        }

134.       }

135.       if(removeiterator != NULL)

136.        ClientList.remove(*removeiterator);

137.       break;

138.      }

139.     case P2PTRANS:

140.      {

141.       // 某个客户希望服务端向另外一个客户发送一个打洞消息

142.      printf("%s wants to p2p %s\n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);

143.       stUserListNode node = GetUser(recvbuf.message.translatemessage.userName);

144.       sockaddr_in remote;

145.       remote.sin_family=AF_INET;

146.       remote.sin_port= htons(node.port);

147.       remote.sin_addr.s_addr = htonl(node.ip);

148.

149.       in_addr tmp;

150.       tmp.S_un.S_addr = htonl(node.ip);

151.       printf("the address is %s,and port is %d\n",inet_ntoa(tmp), node.port);

152.

153.       stP2PMessage transMessage;

154.       transMessage.iMessageType = P2PSOMEONEWANTTOCALLYOU;

155.       transMessage.iStringLen = ntohl(sender.sin_addr.S_un.S_addr);

156.       transMessage.Port = ntohs(sender.sin_port);

157.                        

158.       sendto(PrimaryUDP,(const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr *)&remote, sizeof(remote));

159.

160.       break;

161.      }

162.    

163.     case GETALLUSER:

164.      {

165.       int command = GETALLUSER;

166.       sendto(PrimaryUDP, (const char*)&command, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

167.

168.       int nodecount = (int)ClientList.size();

169.       sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

170.

171.       for(UserList::iterator UserIterator=ClientList.begin();

172.         UserIterator!=ClientList.end();

173.         ++UserIterator)

174.       {

175.        sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender));

176.       }

177.       break;

178.      }

179.     }

180.    }

181.   }

182.

183.  }

184.  catch(Exception &e)

185.  {

186.   printf(e.GetMessage());

187.   return 1;

188.  }

189.

190.  return 0;

191.}

1./* P2P 程序客户端

2. *

3.  * 文件名:P2PClient.c

4.  *

5.  * 日期:2004-5-21

6.  *

7.  * 作者:shootingstars(zhouhuis22@sina.com)

8.  *

9.  */

10.

11.#pragma comment(lib,"ws2_32.lib")

12.

13.#include "windows.h"

14.#include "..\proto.h"

15.#include "..\Exception.h"

16.#include <iostream>

17.using namespace std;

18.

19. UserList ClientList;

20.

21. 

22.

23.#define COMMANDMAXC 256

24.#define MAXRETRY 5

25.

26.SOCKET PrimaryUDP;

27.char UserName[10];

28.char ServerIP[20];

29.

30.bool RecvedACK;

31.

32.void InitWinSock()

33.{

34.  WSADATA wsaData;

35.

36.  if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

37.  {

38.   printf("Windows sockets 2.2 startup");

39.   throw Exception("");

40.  }

41.  else{

42.   printf("Using %s (Status: %s)\n",

43.    wsaData.szDescription, wsaData.szSystemStatus);

44.   printf("with API versions %d.%d to %d.%d\n\n",

45.    LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),

46.    LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));

47.  }

48.}

49.

50.SOCKET mksock(int type)

51.{

52.  SOCKET sock = socket(AF_INET, type, 0);

53.  if (sock < 0)

54.  {

55.         printf("create socket error");

56.   throw Exception("");

57.  }

58.  return sock;

59.}

60.

61. stUserListNode GetUser(char *username)

62.{

63.  for(UserList::iterator UserIterator=ClientList.begin();

64.       UserIterator!=ClientList.end();

65.        ++UserIterator)

66.  {

67.   if( strcmp( ((*UserIterator)->userName), username) == 0 )

68.    return *(*UserIterator);

69.  }

70.  throw Exception("not find this user");

71.}

72.

73.void BindSock(SOCKET sock)

74.{

75.  sockaddr_in sin;

76.  sin.sin_addr.S_un.S_addr = INADDR_ANY;

77.  sin.sin_family = AF_INET;

78.  sin.sin_port = 0;

79. 

80.  if (bind(sock, (struct sockaddr*)&sin, sizeof(sin)) < 0)

81.   throw Exception("bind error");

82.}

83.

84.void ConnectToServer(SOCKET sock,char *username, char *serverip)

85.{

86.  sockaddr_in remote;

87.  remote.sin_addr.S_un.S_addr = inet_addr(serverip);

88.  remote.sin_family = AF_INET;

89.  remote.sin_port = htons(SERVER_PORT);

90. 

91.  stMessage sendbuf;

92.  sendbuf.iMessageType = LOGIN;

93.  strncpy(sendbuf.message.loginmember.userName, username, 10);

94.

95.  sendto(sock, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote,sizeof(remote));

96.

97.  int usercount;

98.  int fromlen = sizeof(remote);

99.  int iread = recvfrom(sock, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);

100.  if(iread<=0)

101.  {

102.   throw Exception("Login error\n");

103.  }

104.

105.  // 登录到服务端后,接收服务端发来的已经登录的用户的信息

106. cout<<"Have "<<usercount<<" users logined server:"<<endl;

107.  for(int i = 0;i<usercount;i++)

108.  {

109.   stUserListNode *node = new stUserListNode;

110.   recvfrom(sock, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);

111.   ClientList.push_back(node);

112.   cout<<"Username:"<<node->userName<<endl;

113.   in_addr tmp;

114.   tmp.S_un.S_addr = htonl(node->ip);

115.   cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;

116.   cout<<"UserPort:"<<node->port<<endl;

117.   cout<<""<<endl;

118.  }

119.}

120.

121.void OutputUsage()

122.{

123.  cout<<"You can input you command:\n"

124.   <<"Command Type:\"send\",\"exit\",\"getu\"\n"

125.   <<"Example : send Username Message\n"

126.   <<" exit\n"

127.   <<" getu\n"

128.   <<endl;

129.}

130.

131./* 这是主要的函数:发送一个消息给某个用户(C)

132.  *流程:直接向某个用户的外网IP发送消息,如果此前没有联系过

133. * 那么此消息将无法发送,发送端等待超时。

134. * 超时后,发送端将发送一个请求信息到服务端,

135. * 要求服务端发送给客户C一个请求,请求C给本机发送打洞消息

136. * 以上流程将重复MAXRETRY次

137. */

138.bool SendMessageTo(char *UserName, char *Message)

139.{

140.  char realmessage[256];

141.  unsigned int UserIP;

142.  unsigned short UserPort;

143.  bool FindUser = false;

144.  for(UserList::iterator UserIterator=ClientList.begin();

145.       UserIterator!=ClientList.end();

146.       ++UserIterator)

147.  {

148.   if( strcmp( ((*UserIterator)->userName), UserName) == 0 )

149.   {

150.    UserIP = (*UserIterator)->ip;

151.    UserPort = (*UserIterator)->port;

152.    FindUser = true;

153.   }

154.  }

155.

156.  if(!FindUser)

157.   return false;

158.

159.  strcpy(realmessage, Message);

160.  for(int i=0;i<MAXRETRY;i++)

161.  {

162.   RecvedACK = false;

163.

164.   sockaddr_in remote;

165.   remote.sin_addr.S_un.S_addr = htonl(UserIP);

166.   remote.sin_family = AF_INET;

167.   remote.sin_port = htons(UserPort);

168.   stP2PMessage MessageHead;

169.   MessageHead.iMessageType = P2PMESSAGE;

170.   MessageHead.iStringLen = (int)strlen(realmessage)+1;

171.   int isend = sendto(PrimaryUDP, (const char *)&MessageHead, sizeof(MessageHead), 0, (const sockaddr*)&remote, sizeof(remote));

172.   isend = sendto(PrimaryUDP, (const char *)&realmessage, MessageHead.iStringLen, 0, (const sockaddr*)&remote, sizeof(remote));

173.  

174.   // 等待接收线程将此标记修改

175.  for(int j=0;j<10;j++)

176.   {

177.    if(RecvedACK)

178.     return true;

179.    else

180.     Sleep(300);

181.   }

182.

183.   // 没有接收到目标主机的回应,认为目标主机的端口映射没有

184.  // 打开,那么发送请求信息给服务器,要服务器告诉目标主机

185.  // 打开映射端口(UDP打洞)

186.  sockaddr_in server;

187.   server.sin_addr.S_un.S_addr = inet_addr(ServerIP);

188.   server.sin_family = AF_INET;

189.   server.sin_port = htons(SERVER_PORT);

190. 

191.   stMessage transMessage;

192.   transMessage.iMessageType = P2PTRANS;

193.   strcpy(transMessage.message.translatemessage.userName, UserName);

194.

195.   sendto(PrimaryUDP, (const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr*)&server, sizeof(server));

196.   Sleep(100);// 等待对方先发送信息。

197. }

198.  return false;

199.}

200.

201.// 解析命令,暂时只有exit和send命令

202.// 新增getu命令,获取当前服务器的所有用户

203.void ParseCommand(char * CommandLine)

204.{

205.  if(strlen(CommandLine)<4)

206.   return;

207.  char Command[10];

208.  strncpy(Command, CommandLine, 4);

209.  Command[4]='\0';

210.

211.  if(strcmp(Command,"exit")==0)

212.  {

213.   stMessage sendbuf;

214.   sendbuf.iMessageType = LOGOUT;

215.   strncpy(sendbuf.message.logoutmember.userName, UserName, 10);

216.   sockaddr_in server;

217.   server.sin_addr.S_un.S_addr = inet_addr(ServerIP);

218.   server.sin_family = AF_INET;

219.   server.sin_port = htons(SERVER_PORT);

220.

221.   sendto(PrimaryUDP,(const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr *)&server, sizeof(server));

222.   shutdown(PrimaryUDP, 2);

223.   closesocket(PrimaryUDP);

224.   exit(0);

225.  }

226.  else if(strcmp(Command,"send")==0)

227.  {

228.   char sendname[20];

229.   char message[COMMANDMAXC];

230.   int i;

231.   for(i=5;;i++)

232.   {

233.    if(CommandLine[i]!=' ')

234.     sendname[i-5]=CommandLine[i];

235.    else

236.    {

237.     sendname[i-5]='\0';

238.     break;

239.    }

240.   }

241.   strcpy(message, &(CommandLine[i+1]));

242.   if(SendMessageTo(sendname, message))

243.    printf("Send OK!\n");

244.   else

245.    printf("Send Failure!\n");

246.  }

247.  else if(strcmp(Command,"getu")==0)

248.  {

249.   int command = GETALLUSER;

250.   sockaddr_in server;

251.   server.sin_addr.S_un.S_addr = inet_addr(ServerIP);

252.   server.sin_family = AF_INET;

253.   server.sin_port = htons(SERVER_PORT);

254.

255.   sendto(PrimaryUDP,(const char*)&command, sizeof(command), 0, (const sockaddr *)&server, sizeof(server));

256.  }

257.}

258.

259.// 接受消息线程

260.DWORD WINAPI RecvThreadProc(LPVOID lpParameter)

261.{

262.  sockaddr_in remote;

263.  int sinlen = sizeof(remote);

264.  stP2PMessage recvbuf;

265.  for(;;)

266.  {

267.   int iread = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(recvbuf), 0, (sockaddr *)&remote, &sinlen);

268.   if(iread<=0)

269.   {

270.    printf("recv error\n");

271.    continue;

272.   }

273.   switch(recvbuf.iMessageType)

274.   {

275.   case P2PMESSAGE:

276.    {

277.     // 接收到P2P的消息

278.    char *comemessage= new char[recvbuf.iStringLen];

279.     int iread1 = recvfrom(PrimaryUDP, comemessage, 256, 0, (sockaddr *)&remote, &sinlen);

280.     comemessage[iread1-1] = '\0';

281.     if(iread1<=0)

282.      throw Exception("Recv Message Error\n");

283.     else

284.     {

285.      printf("Recv a Message:%s\n",comemessage);

286.     

287.      stP2PMessage sendbuf;

288.      sendbuf.iMessageType = P2PMESSAGEACK;

289.      sendto(PrimaryUDP, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote, sizeof(remote));

290.     }

291.

292.     delete []comemessage;

293.     break;

294.

295.    }

296.   case P2PSOMEONEWANTTOCALLYOU:

297.    {

298.     // 接收到打洞命令,向指定的IP地址打洞

299.    printf("Recv p2someonewanttocallyou data\n");

300.     sockaddr_in remote;

301.     remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen);

302.     remote.sin_family = AF_INET;

303.     remote.sin_port = htons(recvbuf.Port);

304.

305.     // UDP hole punching

306.     stP2PMessage message;

307.     message.iMessageType = P2PTRASH;

308.     sendto(PrimaryUDP, (const char *)&message, sizeof(message), 0, (const sockaddr*)&remote, sizeof(remote));

309.                

310.     break;

311.    }

312.   case P2PMESSAGEACK:

313.    {

314.     // 发送消息的应答

315.    RecvedACK = true;

316.     break;

317.    }

318.   case P2PTRASH:

319.    {

320.     // 对方发送的打洞消息,忽略掉。

321.    //do nothing ...

322.     printf("Recv p2ptrash data\n");

323.     break;

324.    }

325.   case GETALLUSER:

326.    {

327.     int usercount;

328.     int fromlen = sizeof(remote);

329.     int iread = recvfrom(PrimaryUDP, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);

330.     if(iread<=0)

331.     {

332.      throw Exception("Login error\n");

333.     }

334.    

335.     ClientList.clear();

336.

337.     cout<<"Have "<<usercount<<" users logined server:"<<endl;

338.     for(int i = 0;i<usercount;i++)

339.     {

340.      stUserListNode *node = new stUserListNode;

341.      recvfrom(PrimaryUDP, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);

342.      ClientList.push_back(node);

343.      cout<<"Username:"<<node->userName<<endl;

344.      in_addr tmp;

345.      tmp.S_un.S_addr = htonl(node->ip);

346.      cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;

347.      cout<<"UserPort:"<<node->port<<endl;

348.      cout<<""<<endl;

349.     }

350.     break;

351.    }

352.   }

353.  }

354.}

355.

356.

357.int main(int argc, char* argv[])

358.{

359.  try

360.  {

361.   InitWinSock();

362.  

363.   PrimaryUDP = mksock(SOCK_DGRAM);

364.   BindSock(PrimaryUDP);

365.

366.   cout<<"Please input server ip:";

367.   cin>>ServerIP;

368.

369.   cout<<"Please input your name:";

370.   cin>>UserName;

371.

372.   ConnectToServer(PrimaryUDP, UserName, ServerIP);

373.

374.   HANDLE threadhandle = CreateThread(NULL, 0, RecvThreadProc, NULL, NULL, NULL);

375.   CloseHandle(threadhandle);

376.   OutputUsage();

377.

378.   for(;;)

379.   {

380.    char Command[COMMANDMAXC];

381.    gets(Command);

382.    ParseCommand(Command);

383.   }

384.  }

385.  catch(Exception &e)

386.  {

387.   printf(e.GetMessage());

388.   return 1;

389.  }

390.  return 0;

391.}

392.

393./* 异常类

394. *

395.  * 文件名:Exception.h

396.  *

397.  * 日期:2004.5.5

398.  *

399.  * 作者:shootingstars(zhouhuis22@sina.com)

400.  */

401.

402.#ifndef __HZH_Exception__

403.#define __HZH_Exception__

404.

405.#define EXCEPTION_MESSAGE_MAXLEN 256

406.#include "string.h"

407.

408.class Exception

409.{

410.private:

411.  char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];

412.public:

413.  Exception(char *msg)

414.  {

415.   strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);

416.  }

417.

418.  char *GetMessage()

419.  {

420.   return m_ExceptionMessage;

421.  }

422.};

423.

424.#endif

425.

426./* P2P 程序传输协议

427. *

428.  * 日期:2004-5-21

429.  *

430.  * 作者:shootingstars(zhouhuis22@sina.com)

431.  *

432.  */

433.

434.#pragma once

435.#include <list>

436.

437.// 定义iMessageType的值

438.#define LOGIN 1

439.#define LOGOUT 2

440.#define P2PTRANS 3

441.#define GETALLUSER 4

442.

443.// 服务器端口

444.#define SERVER_PORT 2280

445.

446.// Client登录时向服务器发送的消息

447.struct stLoginMessage

448.{

449.  char userName[10];

450.  char password[10];

451.};

452.

453.// Client注销时发送的消息

454.struct stLogoutMessage

455.{

456.  char userName[10];

457.};

458.

459.// Client向服务器请求另外一个Client(userName)向自己方向发送UDP打洞消息

460.struct stP2PTranslate

461.{

462.  char userName[10];

463.};

464.

465.// Client向服务器发送的消息格式

466.struct stMessage

467.{

468.  int iMessageType;

469.  union _message

470.  {

471.   stLoginMessage loginmember;

472.   stLogoutMessage logoutmember;

473.   stP2PTranslate translatemessage;

474.  }message;

475.};

476.

477.// 客户节点信息

478.struct stUserListNode

479.{

480.  char userName[10];

481.  unsigned int ip;

482.  unsigned short port;

483.};

484.

485.// Server向Client发送的消息

486.struct stServerToClient

487.{

488.  int iMessageType;

489.  union _message

490.  {

491.   stUserListNode user;

492.  }message;

493.

494.};

495.

496.//======================================

497.// 下面的协议用于客户端之间的通信

498.//======================================

499.#define P2PMESSAGE 100 // 发送消息

500.#define P2PMESSAGEACK 101 // 收到消息的应答

501.#define P2PSOMEONEWANTTOCALLYOU 102 // 服务器向客户端发送的消息

502.                                     // 希望此客户端发送一个UDP打洞包

503.#define P2PTRASH 103 // 客户端发送的打洞包,接收端应该忽略此消息

504.

505.// 客户端之间发送消息格式

506.struct stP2PMessage

507.{

508.  int iMessageType;

509.  int iStringLen; // or IP address

510.  unsigned short Port;

511.};

512.

513.using namespace std;

514.typedef list<stUserListNode *> UserList;




==============具体的解释=======================
假设client A要向client B对话,但是A不知道B的地址,即使知道根据NAT的原理这个对话在第一次会被拒绝,因为client B的NAT认为这是一个从没有过的外部发来的请求.这个时候,A如果发现自己没有保存B的地址,或者说发送给B的会话请求失败了,它会要求server端让B向A打一个洞,这个B->A的会话意义在于它使NAT B认为A的地址/端口是可以通过的地址/端口,这样A再向B发送对话的时候就不会再被NAT B拒绝了.打一个比方来说明打洞的过程,A想来B家做客,但是遭到了B的管家NAT B的拒绝,理由是:我从来没有听我家B提过你的名字,这时A找到了A,B都认识的朋友server,要求server给B报一个信,让B去跟管家说A是我的朋友,于是,B跟管家NAT B说,A是我认识的朋友,这样A的访问请求就不会再被管家NAT B所拒绝了.简而言之,UDP打洞就是一个通过server保存下来的地址使得彼此之间能够直接通信的过程,server只管帮助建立连接,在建立间接之后就不再介入了.

1、每个客户端首先得到自己的私有地址/终端,然后向server端发送登陆请求
2、server端在得到这个请求之后就可以知道这个client端的公有地址/终端,server会为每一个登陆的client保存它们的私有地址/端口和公有地址/端口
3、如果A想和B联系,则A向server请求,server收到请求后,向B发出指令要求B向A发一个打洞包(A的地址和端口由Server向B提供,所谓的打洞包就是向A发个任意内容的包,内容不重要,关键是这个包指向A的地址和端口,而且这个包不会到达A),至此洞已打好。
4、B发完后,通知server,server将B的地址和端口告诉A,A则可以根据这个地址和端口直接和B通讯了。
5、由于A,B的内网地址他们是相互不知道的,其实知道也没有用;我们只要知道这个相互的用户名(或者类似的唯一定位),然后通过用户名去找相应的net映射的外网ip和端口。
分享到:
评论

相关推荐

    Java实现的udp打洞demo

    Java实现的udp打洞demo、Java实现的udp打洞demo、Java实现的udp打洞demo、

    UDP打洞原理,Delphi演示

    UDP打洞是一种网络技术,主要用于穿透NAT(网络地址转换)网络,使位于不同NAT后的设备能够直接通信。在互联网环境中,许多设备都通过NAT连接,这使得它们拥有私有IP地址,无法直接相互连接。UDP打洞解决了这个问题...

    UDP打洞demo

    UDP打洞(UDP Hole Punching)是一种通过网络地址转换(NAT)技术,使位于不同内网中的两台设备能够直接通信的技术。在通常情况下,由于NAT的存在,内部网络中的设备不能直接与外部网络中的设备通信,除非有一个固定...

    p2p-test[udp打洞测试程序].zip

    本程序实现了linux环境下的udp打洞功能,通过udp打洞实现P2P通信。

    UDP打洞的p2p聊天程序

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现P2P(点对点)通信的方法,尤其在建立直接连接以提高传输效率和降低服务器压力方面显得尤为重要。在这个场景中,我们有一个由服务器和客户端组成的聊天程序,利用...

    udp.zip_ICE NAT_UDP nat java_nat_udp打洞_最新防火墙

    就是非常有名的“UDP打洞技术”,UDP打洞技术依赖于由公共防火墙和cone NAT,允许适当的有计划的端对端应用程序通过NAT“打洞”,即使当双方的主机都处于NAT之后。这种技术在 RFC3027的5.1节[NAT PROT] 中进行了重点...

    udp打洞原理详解

    ### UDP打洞原理详解 随着互联网的不断发展以及IPv4地址资源的日益紧张,NAT(Network Address Translation,网络地址转换)设备被广泛应用于解决IP地址不足的问题。然而,NAT设备的存在导致了P2P(Peer-to-Peer,...

    UDP打洞软件

    UDP打洞技术是一种在内网环境下,通过利用UDP(User Datagram Protocol)协议,使得两个不在同一公网IP下的设备能够直接通信的技术。它主要用于解决NAT(Network Address Translation)环境下的点对点通信问题。NAT...

    java udp 打洞例子(p2p)

    java udp 打洞例子(p2p) .

    udp打洞源码服务端和客户端C# 实现

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现两个私有网络主机之间直接通信的方法,常用于P2P应用、多人在线游戏等场景。本文将深入探讨C#实现的UDP打洞服务端和客户端的关键知识点。 1. **UDP基础**: UDP...

    C语言实现UDP打洞代码

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现两个私有IP地址之间直接通信的方法,常用于P2P(点对点)通信。在本文中,我们将深入探讨C语言实现UDP打洞的原理、步骤以及代码实现的关键点。 首先,理解UDP打洞...

    UDP打洞的实现代码

    UDP打洞技术是一种在P2P网络中实现端到端通信的方法,特别是在存在网络地址转换(NAT)的环境中。NAT使得私有网络内的设备无法直接与其他网络的设备通信,因为它们都共享一个公共IP地址。UDP打洞通过利用NAT的某些...

    C#实现udp打洞

    UDP打洞(UDP Hole Punching)是一种通过NAT(网络地址转换)技术,使两个位于不同内网的设备能够直接通信的技术。在C#中实现UDP打洞,主要是利用了UDP协议的特性以及对NAT行为的理解。下面将详细介绍这个过程。 ...

    UDP打洞源码及原理分析.zip

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现两个位于不同内网的设备间直接通信的方法。在互联网上,许多用户设备由于IP地址稀缺,通常会通过NAT进行地址转换,这使得它们不能直接与其他内网设备通信。UDP打洞...

    演示实现UDP打洞,UDP打洞原理

    本程序为UDP打洞原理的测试程序。为了简单起见,该程序服务器就接收两个客户端的连接,使两客户端建立UDP通道。两客户端建立UDP通道后,互相发送的数据不经过服务器转发,直接进行数据交互,这时可以关闭服务器程序。...

    P2P之UDP打洞穿透NAT的源代码

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现P2P(点对点)通信的方法。在NAT环境中,内部网络的设备通常没有全局唯一的公网IP,而是通过一个公共IP来对外通信,这就导致了两个处于NAT后的设备无法直接通信。...

    C#UDP打洞UDP打洞C#UDP打洞UDP打洞UDP打洞UDP打洞

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现两个位于不同内网的设备间直接通信的方法。在互联网上,许多设备通过NAT连接,这使得它们不能直接与外部网络中的其他设备通信,因为它们都有相同的公共IP地址。UDP...

    udp 打洞示例代码 包含服务器 客户端

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现两个私有网络内的主机之间直接通信的方法。在互联网上,许多设备由于连接到ISP时采用了NAT,它们的公网IP实际上是路由器分配的内部IP,因此不能直接与其他网络的...

    UDP打洞(有心跳包和断网自动连接功能)

    UDP打洞技术是一种在NAT(网络地址转换)环境下实现两个私有网络主机间通信的方法。在互联网上,由于IP地址的稀缺性,许多设备通过NAT与公网交互,这导致了内网设备的IP地址对外不可见。UDP打洞正是为了解决这一问题...

Global site tag (gtag.js) - Google Analytics