首先先介绍一些基本概念:
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软件失灵。(特别是如果双方都是Symmetric NAT,或者一方是Symmetric NAT,另一方是Restricted Cone NAT,这种情况下,建立p2p的连接将会比较困难。关于Restricted Cone NAT,请参看http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt)

好了,我们看到,通过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就能收到了。既然Client A不能够通知Client B来打这个洞,那么我们只能通过服务器来转发这个命令了。
总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。
这是一个Client A与Client B建立p2p连结的大概的流程:
Client A->Server S (Client A向Server S发送一个请求,请求信息是希望Client B向Client A方向打洞)
Server S->Client B (S要求B向A打洞)
Client B->Client A (打洞消息,这个消息Client A很可能不会收到,但是收不到没有关系,NAT B上的洞已经打好了)
Client A->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
功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)

代码如下:

/************************** proto.h **********************
#pragma once
#include <list>// 定义iMessageType的值
#define LOGIN 1
#define LOGOUT 2
#define P2PTRANS 3
#define GETALLUSER  4// 服务器端口
#define SERVER_PORT 2280// Client登录时向服务器发送的消息
struct stLoginMessage
{char userName[10];char password[10];
};// Client注销时发送的消息
struct stLogoutMessage
{char userName[10];
};// Client向服务器请求另外一个Client(userName)向自己方向发送UDP打洞消息
struct stP2PTranslate
{char userName[10];
};// Client向服务器发送的消息格式
struct stMessage
{int iMessageType;union _message{stLoginMessage loginmember;stLogoutMessage logoutmember;stP2PTranslate translatemessage;}message;
};// 客户节点信息
struct stUserListNode
{char userName[10];unsigned int ip;unsigned short port;
};// Server向Client发送的消息
struct stServerToClient
{int iMessageType;union _message{stUserListNode user;}message;};//======================================
// 下面的协议用于客户端之间的通信
//======================================
#define P2PMESSAGE 100               // 发送消息
#define P2PMESSAGEACK 101            // 收到消息的应答
#define P2PSOMEONEWANTTOCALLYOU 102  // 服务器向客户端发送的消息// 希望此客户端发送一个UDP打洞包
#define P2PTRASH        103          // 客户端发送的打洞包,接收端应该忽略此消
息// 客户端之间发送消息格式
struct stP2PMessage
{int iMessageType;int iStringLen;         // or IP addressunsigned short Port;
};using namespace std;
typedef list<stUserListNode *> UserList;
/************************** proto.h **********************/************************** Exception.h ******************
#ifndef __HZH_Exception__
#define __HZH_Exception__#define EXCEPTION_MESSAGE_MAXLEN 256
#include "string.h"class Exception
{
private:char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];
public:Exception(char *msg){strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);}char *GetMessage(){return m_ExceptionMessage;}
};#endif
/************************** Exception.h ******************/
/* P2P 程序客户端* * 文件名:P2PClient.c** 日期:2004-5-21** 作者:shootingstars(zhouhuis22@sina.com)**/#pragma comment(lib,"ws2_32.lib")#include "windows.h"
#include "..\proto.h"
#include "..\Exception.h"
#include <iostream>
using namespace std;UserList ClientList;#define COMMANDMAXC 256
#define MAXRETRY    5SOCKET PrimaryUDP;
char UserName[10];
char ServerIP[20];bool RecvedACK;void InitWinSock()
{WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){printf("Windows sockets 2.2 startup");throw Exception("");}else{printf("Using %s (Status: %s)\n",wsaData.szDescription, wsaData.szSystemStatus);printf("with API versions %d.%d to %d.%d\n\n",LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));}
}SOCKET mksock(int type)
{SOCKET sock = socket(AF_INET, type, 0);if (sock < 0){printf("create socket error");throw Exception("");}return sock;
}stUserListNode GetUser(char *username)
{for(UserList::iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator){if( strcmp( ((*UserIterator)->userName), username) == 0 )return *(*UserIterator);}throw Exception("not find this user");
}void BindSock(SOCKET sock)
{sockaddr_in sin;sin.sin_addr.S_un.S_addr = INADDR_ANY;sin.sin_family = AF_INET;sin.sin_port = 0;if (bind(sock, (struct sockaddr*)&sin, sizeof(sin)) < 0)throw Exception("bind error");
}void ConnectToServer(SOCKET sock,char *username, char *serverip)
{sockaddr_in remote;remote.sin_addr.S_un.S_addr = inet_addr(serverip);remote.sin_family = AF_INET;remote.sin_port = htons(SERVER_PORT);stMessage sendbuf;sendbuf.iMessageType = LOGIN;strncpy(sendbuf.message.loginmember.userName, username, 10);sendto(sock, (const char*)&sendbuf, sizeof(sendbuf), 0, (const
sockaddr*)&remote,sizeof(remote));int usercount;int fromlen = sizeof(remote);int iread = recvfrom(sock, (char *)&usercount, sizeof(int), 0, (sockaddr
*)&remote, &fromlen);if(iread<=0){throw Exception("Login error\n");}// 登录到服务端后,接收服务端发来的已经登录的用户的信息cout<<"Have "<<usercount<<" users logined server:"<<endl;for(int i = 0;i<usercount;i++){stUserListNode *node = new stUserListNode;recvfrom(sock, (char*)node, sizeof(stUserListNode), 0, (sockaddr
*)&remote, &fromlen);ClientList.push_back(node);cout<<"Username:"<<node->userName<<endl;in_addr tmp;tmp.S_un.S_addr = htonl(node->ip);cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;cout<<"UserPort:"<<node->port<<endl;cout<<""<<endl;}
}void OutputUsage()
{cout<<"You can input you command:\n"<<"Command Type:\"send\",\"exit\",\"getu\"\n"<<"Example : send Username Message\n"<<"          exit\n"<<"          getu\n"<<endl;
}/* 这是主要的函数:发送一个消息给某个用户(C)*流程:直接向某个用户的外网IP发送消息,如果此前没有联系过*      那么此消息将无法发送,发送端等待超时。*      超时后,发送端将发送一个请求信息到服务端,*      要求服务端发送给客户C一个请求,请求C给本机发送打洞消息*      以上流程将重复MAXRETRY次*/
bool SendMessageTo(char *UserName, char *Message)
{char realmessage[256];unsigned int UserIP;unsigned short UserPort;bool FindUser = false;for(UserList::iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator){if( strcmp( ((*UserIterator)->userName), UserName) == 0 ){UserIP = (*UserIterator)->ip;UserPort = (*UserIterator)->port;FindUser = true;}}if(!FindUser)return false;strcpy(realmessage, Message);for(int i=0;i<MAXRETRY;i++){RecvedACK = false;sockaddr_in remote;remote.sin_addr.S_un.S_addr = htonl(UserIP);remote.sin_family = AF_INET;remote.sin_port = htons(UserPort);stP2PMessage MessageHead;MessageHead.iMessageType = P2PMESSAGE;MessageHead.iStringLen = (int)strlen(realmessage)+1;int isend = sendto(PrimaryUDP, (const char *)&MessageHead,
sizeof(MessageHead), 0, (const sockaddr*)&remote, sizeof(remote));isend = sendto(PrimaryUDP, (const char *)&realmessage,
MessageHead.iStringLen, 0, (const sockaddr*)&remote, sizeof(remote));// 等待接收线程将此标记修改for(int j=0;j<10;j++){if(RecvedACK)return true;elseSleep(300);}// 没有接收到目标主机的回应,认为目标主机的端口映射没有// 打开,那么发送请求信息给服务器,要服务器告诉目标主机// 打开映射端口(UDP打洞)sockaddr_in server;server.sin_addr.S_un.S_addr = inet_addr(ServerIP);server.sin_family = AF_INET;server.sin_port = htons(SERVER_PORT);stMessage transMessage;transMessage.iMessageType = P2PTRANS;strcpy(transMessage.message.translatemessage.userName, UserName);sendto(PrimaryUDP, (const char*)&transMessage, sizeof(transMessage),
0, (const sockaddr*)&server, sizeof(server));Sleep(100);// 等待对方先发送信息。}return false;
}// 解析命令,暂时只有exit和send命令
// 新增getu命令,获取当前服务器的所有用户
void ParseCommand(char * CommandLine)
{if(strlen(CommandLine)<4)return;char Command[10];strncpy(Command, CommandLine, 4);Command[4]='\0';if(strcmp(Command,"exit")==0){stMessage sendbuf;sendbuf.iMessageType = LOGOUT;strncpy(sendbuf.message.logoutmember.userName, UserName, 10);sockaddr_in server;server.sin_addr.S_un.S_addr = inet_addr(ServerIP);server.sin_family = AF_INET;server.sin_port = htons(SERVER_PORT);sendto(PrimaryUDP,(const char*)&sendbuf, sizeof(sendbuf), 0, (const
sockaddr *)&server, sizeof(server));shutdown(PrimaryUDP, 2);closesocket(PrimaryUDP);exit(0);}else if(strcmp(Command,"send")==0){char sendname[20];char message[COMMANDMAXC];int i;for(i=5;;i++){if(CommandLine[i]!=' ')sendname[i-5]=CommandLine[i];else{sendname[i-5]='\0';break;}}strcpy(message, &(CommandLine[i+1]));if(SendMessageTo(sendname, message))printf("Send OK!\n");else printf("Send Failure!\n");}else if(strcmp(Command,"getu")==0){int command = GETALLUSER;sockaddr_in server;server.sin_addr.S_un.S_addr = inet_addr(ServerIP);server.sin_family = AF_INET;server.sin_port = htons(SERVER_PORT);sendto(PrimaryUDP,(const char*)&command, sizeof(command), 0, (const
sockaddr *)&server, sizeof(server));}
}// 接受消息线程
DWORD WINAPI RecvThreadProc(LPVOID lpParameter)
{sockaddr_in remote;int sinlen = sizeof(remote);stP2PMessage recvbuf;for(;;){int iread = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(recvbuf),
0, (sockaddr *)&remote, &sinlen);if(iread<=0){printf("recv error\n");continue;}switch(recvbuf.iMessageType){case P2PMESSAGE:{// 接收到P2P的消息char *comemessage= new char[recvbuf.iStringLen];int iread1 = recvfrom(PrimaryUDP, comemessage, 256, 0,
(sockaddr *)&remote, &sinlen);comemessage[iread1-1] = '\0';if(iread1<=0)throw Exception("Recv Message Error\n");else{printf("Recv a Message:%s\n",comemessage);stP2PMessage sendbuf;sendbuf.iMessageType = P2PMESSAGEACK;sendto(PrimaryUDP, (const char*)&sendbuf,
sizeof(sendbuf), 0, (const sockaddr*)&remote, sizeof(remote));}delete []comemessage;break;}case P2PSOMEONEWANTTOCALLYOU:{// 接收到打洞命令,向指定的IP地址打洞printf("Recv p2someonewanttocallyou data\n");sockaddr_in remote;remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen);remote.sin_family = AF_INET;remote.sin_port = htons(recvbuf.Port);// UDP hole punchingstP2PMessage message;message.iMessageType = P2PTRASH;sendto(PrimaryUDP, (const char *)&message, sizeof(message),
0, (const sockaddr*)&remote, sizeof(remote));break;}case P2PMESSAGEACK:{// 发送消息的应答RecvedACK = true;break;}case P2PTRASH:{// 对方发送的打洞消息,忽略掉。//do nothing ...printf("Recv p2ptrash data\n");break;}case GETALLUSER:{int usercount;int fromlen = sizeof(remote);int iread = recvfrom(PrimaryUDP, (char *)&usercount,
sizeof(int), 0, (sockaddr *)&remote, &fromlen);if(iread<=0){throw Exception("Login error\n");}ClientList.clear();cout<<"Have "<<usercount<<" users logined server:"<<endl;for(int i = 0;i<usercount;i++){stUserListNode *node = new stUserListNode;recvfrom(PrimaryUDP, (char*)node, sizeof(stUserListNode),
0, (sockaddr *)&remote, &fromlen);ClientList.push_back(node);cout<<"Username:"<<node->userName<<endl;in_addr tmp;tmp.S_un.S_addr = htonl(node->ip);cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;cout<<"UserPort:"<<node->port<<endl;cout<<""<<endl;}break;}}}
}int main(int argc, char* argv[])
{try{InitWinSock();PrimaryUDP = mksock(SOCK_DGRAM);BindSock(PrimaryUDP);cout<<"Please input server ip:";cin>>ServerIP;cout<<"Please input your name:";cin>>UserName;ConnectToServer(PrimaryUDP, UserName, ServerIP);HANDLE threadhandle = CreateThread(NULL, 0, RecvThreadProc, NULL,
NULL, NULL);CloseHandle(threadhandle);OutputUsage();for(;;){char Command[COMMANDMAXC];gets(Command);ParseCommand(Command);}}catch(Exception &e){printf(e.GetMessage());return 1;}return 0;
}/**********************************/*************************************
/* P2P 程序服务端* * 文件名:P2PServer.c** 日期:2004-5-21** 作者:shootingstars(zhouhuis22@sina.com)**/
#pragma comment(lib, "ws2_32.lib")#include "windows.h"
#include "..\proto.h"
#include "..\Exception.h"UserList ClientList;void InitWinSock()
{WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){printf("Windows sockets 2.2 startup");throw Exception("");}else{printf("Using %s (Status: %s)\n",wsaData.szDescription, wsaData.szSystemStatus);printf("with API versions %d.%d to %d.%d\n\n",LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));}
}SOCKET mksock(int type)
{SOCKET sock = socket(AF_INET, type, 0);if (sock < 0){printf("create socket error");throw Exception("");}return sock;
}stUserListNode GetUser(char *username)
{for(UserList::iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator){if( strcmp( ((*UserIterator)->userName), username) == 0 )return *(*UserIterator);}throw Exception("not find this user");
}int main(int argc, char* argv[])
{try{InitWinSock();SOCKET PrimaryUDP;PrimaryUDP = mksock(SOCK_DGRAM);sockaddr_in local;local.sin_family=AF_INET;local.sin_port= htons(SERVER_PORT); local.sin_addr.s_addr = htonl(INADDR_ANY);int nResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));if(nResult==SOCKET_ERROR)throw Exception("bind error");sockaddr_in sender;stMessage recvbuf;memset(&recvbuf,0,sizeof(stMessage));// 开始主循环.// 主循环负责下面几件事情:// 一:读取客户端登陆和登出消息,记录客户列表// 二:转发客户p2p请求for(;;){int dwSender = sizeof(sender);int ret = recvfrom(PrimaryUDP, (char *)&recvbuf,
sizeof(stMessage), 0, (sockaddr *)&sender, &dwSender);if(ret <= 0){printf("recv error");continue;}else{int messageType = recvbuf.iMessageType;switch(messageType){case LOGIN:{//  将这个用户的信息记录到用户列表中printf("has a user login : %s\n",
recvbuf.message.loginmember.userName);stUserListNode *currentuser = new stUserListNode();strcpy(currentuser->userName,
recvbuf.message.loginmember.userName);currentuser->ip = ntohl(sender.sin_addr.S_un.S_addr);currentuser->port = ntohs(sender.sin_port);ClientList.push_back(currentuser);// 发送已经登陆的客户信息int nodecount = (int)ClientList.size();sendto(PrimaryUDP, (const char*)&nodecount,
sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));for(UserList::iterator
UserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator){sendto(PrimaryUDP, (const char*)(*UserIterator),
sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender)); }break;}case LOGOUT:{// 将此客户信息删除printf("has a user logout : %s\n",
recvbuf.message.logoutmember.userName);UserList::iterator removeiterator = NULL;for(UserList::iterator
UserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator){if( strcmp( ((*UserIterator)->userName),
recvbuf.message.logoutmember.userName) == 0 ){removeiterator = UserIterator;break;}}if(removeiterator != NULL)ClientList.remove(*removeiterator);break;}case P2PTRANS:{// 某个客户希望服务端向另外一个客户发送一个打洞消息printf("%s wants to p2p
%s\n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);stUserListNode node =
GetUser(recvbuf.message.translatemessage.userName);sockaddr_in remote;remote.sin_family=AF_INET;remote.sin_port= htons(node.port); remote.sin_addr.s_addr = htonl(node.ip);in_addr tmp;tmp.S_un.S_addr = htonl(node.ip);printf("the address is %s,and port is
%d\n",inet_ntoa(tmp), node.port);stP2PMessage transMessage;transMessage.iMessageType = P2PSOMEONEWANTTOCALLYOU;transMessage.iStringLen =
ntohl(sender.sin_addr.S_un.S_addr);transMessage.Port = ntohs(sender.sin_port);sendto(PrimaryUDP,(const char*)&transMessage,
sizeof(transMessage), 0, (const sockaddr *)&remote, sizeof(remote));break;}case GETALLUSER:{int command = GETALLUSER;sendto(PrimaryUDP, (const char*)&command,
sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));int nodecount = (int)ClientList.size();sendto(PrimaryUDP, (const char*)&nodecount,
sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));for(UserList::iterator
UserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator){sendto(PrimaryUDP, (const char*)(*UserIterator),
sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender)); }break;}}}}}catch(Exception &e){printf(e.GetMessage());return 1;}return 0;
}

p2p软件如何穿透内网进行通信相关推荐

  1. p2p技术之tcp内网穿透 java实现版

    如今p2p技术已经十分成熟,然而很多人停留在理论层面,在实现过程中遇到这样那样的问题,甚至有的人说tcp打洞无法实现,本文简单阐述tcp穿透的过程,然后附上完整的java代码. 由于32位Ip地址的稀 ...

  2. 穿透内网,连接动态ip,内网ip打洞-----p2p实现原理(转)

    源: 穿透内网,连接动态ip,内网ip打洞-----p2p实现原理 转载于:https://www.cnblogs.com/LittleTiger/p/10107849.html

  3. 口语化讲某些软件如BT,电驴,向日葵等穿透内网原理

    一般如果要访问或远程控制内网的电脑,都需要在路由器或网关服务器上做端口映射处理,但某些软件如BT,电驴,向日葵,teamviewer等无需做端口映射即可穿透内网远程通讯.一般宽带路由器对出站通讯(主动 ...

  4. Linux下穿透内网跨平台远程控制与被控制

    Linux下穿透内网跨平台远程控制与被控制 A) 从别的系统控制Linux. 1. 64位 图形界面 2. 64位 命令行界面 3. 32位 图形界面 4. 32位 命令行界面 B) 从Linux控制 ...

  5. 不用向日葵 teamview 利用云服务器部署FRP穿透内网实现控制远程桌面

    不用向日葵 teamview 利用云服务器部署FRP穿透内网实现控制远程桌面 前言 需要的硬软件 需要的设置 服务端的配置 客户端配置 远程连接 设置开机自启 前言 由于工作原因,不可使用向日葵及te ...

  6. 干货!手把手教你穿透内网

    干货!手把手教你穿透内网 干货!手把手教你穿透内网 cpolar内网穿透使用场景 如何使用cpolar内网穿透? ↓↓ 1. 注册cpolar账号 2. 安装cpolar内网穿透 2.1 Window ...

  7. 穿透内网远程访问群晖NAS

    现代科技日新月异,我们身边的电子设备也在不断更新,日积月累之下,被淘汰的电子设备越来越多,难道就让这些性能不算差的电子设备从此闲置么,这明显不符合我们物尽其用的原则,不少玩家都将闲置的电脑改造成了家庭 ...

  8. 穿透内网群晖NAS实现远程访问【无公网IP】

    现代科技日新月异,我们身边的电子设备也在不断更新,日积月累之下,被淘汰的电子设备越来越多,难道就让这些性能不算差的电子设备从此闲置么,这明显不符合我们物尽其用的原则,不少玩家都将闲置的电脑改造成了家庭 ...

  9. 端口映射软件大-比较|内网端口映射器使用方法

    [转载]端口映射软件大-比较|内网端口映射器使用方法 2010-05-15 13:34 功能多少作为顺序: PortTunnel 2.0.13 简体中文版 优点:功能最为强大,默认为服务模式. 缺点: ...

最新文章

  1. layui表格获取不到多层数据的解决方案
  2. spring4-3-AOP-面向切面编程
  3. IOS之Xcode之快捷键
  4. 求关系模式r的所有候选码_2_1关系数据库的基本概念
  5. Python MySQL删除表
  6. 国家集训队2011 happiness
  7. GRT上线Bancor提案已通过
  8. Java面试宝典 Beta5.0版 (2018年最新公测版)
  9. 关于批判性思维(Critical Thinking)
  10. HTML5+CSS大作业——宝马轿车网页设计(6页) web前端设计与开发期末作品/期末大作业
  11. Qt知识回顾(九)——2D绘画
  12. 什么是索引?索引的作用是什么?Mysql目前主要的几种索引类型
  13. 计算机中的没有文件怎么处理方法,电脑重启后桌面上文件没有了怎么办
  14. 【电赛练习】手写绘图板 2013电赛G题
  15. 苹果服务器怎么配置文件,苹果CMS数据库配置文件在哪
  16. 北大计算机研究生有多神仙,北大考研成绩公布,还上“热搜”,这都是些什么“神仙分数”?...
  17. 你的睡眠时间和睡眠质量达标了么?
  18. 寂然安静到鸿蒙翻译,明道篇
  19. RMQ与SparseTable(ST表)
  20. 国内开源社区有哪些 ?|GitCode

热门文章

  1. Python学习Day1
  2. 利用python进行身份证号码大全_用 Java 撸一个身份证号码识别系统,准确率高达 90%...
  3. linux下使用Stunnel配置与使用方式一例
  4. 快速理解raft协议(附件动画演示)
  5. Redis 最全面试题(2021)
  6. 【MacOS 】Sublime调整字体大小
  7. Easyui学习整理笔记
  8. Swift编译器Crash—Segmentation fault解决方案
  9. mongodb aggregate中,使用$unwind对数组中的字典对象求和
  10. 6-1 计算两数的和与差 (10 分)