下面是关键的部分了。 看看下面的情况: ServerS1 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:62000v | 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)又接着向另外一个ServerS2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢? 这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做SymmetricNAT,后一种叫做ConeNAT。我们期望我们的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 HolePunching的技术)以后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)
首先,ClientA登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的ClientA的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NATB给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。 此时,ClientA与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从ServerS那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息ClientB就能收到了呢?答案是不行,因为如果这样发送信息,NATB会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NATB上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么ClientA发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是ServerS。 总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求ServerS命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢,然后ClientA就可以通过Client B的外网地址与Client B通信了。 注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向ClientA打洞的端口已经重新分配了,Client B将无法知道这个端口(如果SymmetricNAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。 下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopbacktranslation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send usernamemessage的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。 程序现在支持三个命令:send , getu , exit send格式:sendusername message 功能:发送信息给username getu格式:getu 功能:获得当前服务器用户列表 exit格式:exit 功能:注销与服务器的连接(服务器不会自动监测客户是否吊线) 代码很短,相信很容易懂,如果有什么问题,可以给我发邮件zhouhuis22@sina.com 或者在上发送短消息。同时,欢迎转发此文,但希望保留作者版权8-)。 最后感谢网友PiggyXP 和 Seilfer的测试帮助
P2PServer.c
[code='C'] #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::iteratorUserIterator=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); intnResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr)); if(nResult==SOCKET_ERROR) throw Exception("binderror");
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("recverror"); continue; } else { intmessageType = recvbuf.iMessageType; switch(messageType){ caseLOGIN: { // 将这个用户的信息记录到用户列表中 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, (constsockaddr*)&sender, sizeof(sender)); }
break; } caseLOGOUT: { // 将此客户信息删除 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; } caseP2PTRANS: { // 某个客户希望服务端向另外一个客户发送一个打洞消息 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; } caseGETALLUSER: { 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, (constsockaddr*)&sender, sizeof(sender)); } break; } } } }
} catch(Exception &e) { printf(e.GetMessage()); return 1; }
return 0; }
#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 #defineMAXRETRY 5
SOCKET 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::iteratorUserIterator=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, (structsockaddr*)&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, (constchar*)&sendbuf, sizeof(sendbuf), 0, (constsockaddr*)&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 caninput you command:\n" <<"CommandType:\"send\",\"exit\",\"getu\"\n" <<"Example : sendUsername Message\n" <<" exit\n" <<" getu\n" <<endl; }
bool SendMessageTo(char *UserName, char *Message) { char realmessage[256]; unsigned int UserIP; unsigned short UserPort; bool FindUser = false; for(UserList::iteratorUserIterator=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, (constsockaddr*)&remote, sizeof(remote)); isend = sendto(PrimaryUDP, (const char*)&realmessage, MessageHead.iStringLen, 0, (constsockaddr*)&remote, sizeof(remote)); // 等待接收线程将此标记修改 for(int j=0;j<10;j++) { if(RecvedACK) returntrue; else Sleep(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, (constchar*)&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,(constchar*)&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("SendFailure!\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,(constchar*)&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("Recvp2someonewanttocallyou data\n"); sockaddr_inremote; remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen); remote.sin_family = AF_INET; remote.sin_port = htons(recvbuf.Port);
// UDPhole punching stP2PMessagemessage; 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("Recvp2ptrash data\n"); break; } case GETALLUSER: { intusercount; 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<<"Pleaseinput server ip:"; cin>>ServerIP;
cout<<"Pleaseinput your name:"; cin>>UserName;
ConnectToServer(PrimaryUDP, UserName,ServerIP);
HANDLE threadhandle = CreateThread(NULL, 0,RecvThreadProc, NULL, NULL, NULL); CloseHandle(threadhandle); OutputUsage();
for(;;) { charCommand[COMMANDMAXC]; gets(Command); ParseCommand(Command); } } catch(Exception &e) { printf(e.GetMessage()); return 1; } return 0; }
#ifndef __HZH_Exception__ #define __HZH_Exception__
#define EXCEPTION_MESSAGE_MAXLEN 256 #include "string.h"
class Exception { private: charm_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN]; public: Exception(char *msg) { strncpy(m_ExceptionMessage, msg,EXCEPTION_MESSAGE_MAXLEN); }
char *GetMessage() { return m_ExceptionMessage; } };
#endif
#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 P2PMESSAGE100 // 发送消息 #define P2PMESSAGEACK101 // 收到消息的应答 #define P2PSOMEONEWANTTOCALLYOU 102 //服务器向客户端发送的消息 // 希望此客户端发送一个UDP打洞包 #defineP2PTRASH 103 // 客户端发送的打洞包,接收端应该忽略此消息
// 客户端之间发送消息格式 struct stP2PMessage { int iMessageType; intiStringLen; // or IP address unsigned short Port; };[/code] using namespace std; typedef list<stUserListNode *>UserList;