本文从以下四个部分开始分析讲解,客户端与服务端的源码在文章末尾链接
一、P2P打洞的原理
二、P2P服务器的实现
三、P2P客户端的实现
四、数据包格式
一、P2P打洞原理
#####1、打洞解决了什么问题?#####
我们平常使用的一般都为私有ip,但是私有ip之间是不能直接通信的,如果要进行通信只能通过公网上的服务器进行数据的转发,难道我们每次发送数据都要经过公网上的服务器转发吗?也不是不可以,但是服务器的承受能力就会大大增加。此时就需要我们的打洞技术的出现了,打洞的出现解决了私有ip之间直接通信的问题(还是需要经过一次公网服务器)
例如:QQ中的聊天就广泛的使用到了打洞技术,不然服务器的承受能力会大大增加,而且会影响聊天的效率。
#####2、打洞的实现过程与原理#####
私有ip的数据都要经过路由器的转发,路由器上有一张NAPT表(IP端口映射表),NAPT表记录的是【私有IP:端口】与【公有IP:端口】的映射关系(就是一一对应关系),本文讲到的路由均是以NAPT为工作模式,这并不影响对打洞。实际中的数据实际发送给的都是路由器的【公有IP:端口】,然后经过路由器进过查询路由表后再转发给【私有的IP:端口】的。
举个示例:
用户A
电脑IP:192.168.1.101
桌面上有个客户端程序采用的网络端口:10000
路由器的公有IP:120.78.201.201(实际中常常为多级路由,这里以最简单的一层路由举例)
NAPT路由器的NAPT表的其中一条记录为:【120.78.201.201:20202】-【192.168.1.101:10000】
用户B
电脑IP:192.168.2.202
桌面上有个客户端程序采用的网络端口:22222
路由器的公有IP:120.78.202.202
NAPT路由器的NAPT表的其中一条记录为:【120.78.202.202:20000】-【192.168.2.202:22222】
打洞服务器P2Pserver
IP:120.78.202.100
port:20000
此时用户A的电脑发给了服务器一条数据,服务器收到用户A的IP与端口是多少呢?当然为120.78.201.201:20202,数据包经过路由的时候进行了重新的封包。如果服务器此时发一条数据给用户A,发往的IP与端口是什么呢?当然为120.78.201.201:20202,此时路由器收到这个数据包后,进行查询NAPT表中120.78.201.201:20202对应的IP与端口信息,发现是192.168.1.101:10000,然后路由器就转发给IP为192.168.1.101:10000的电脑,然后电脑上的应用程序就收到这条信息了。
既然如此,我们私有IP虽然不能直接通信,但是我们能够发给公有IP!如果用户B需要给用户A发一条信息时,用户B直接将数据发往目的IP、端口为120.78.201.201:20202的地方不就行了?
这里有两个问题:
第一,用户B怎么知道用户A在路由上映射的IP与端口;
第二,用户B直接将数据包发往120.78.201.201:20202,路由器是会将用户B的数据包丢弃的,因为路由器里面没有关于用户B120.78.202.202的路由信息(路由器里面还有个路由表,用于路由),无法进行路由,所以将会进行丢弃。
如何解决第一个问题?
通过打洞服务器,将用户A映射的IP、端口信息告诉用户B即可。
如何解决第二个问题?
如果打洞服务器首先告诉用户A先发一条信息给用户B(用户A得知用户B的地址信息也是通过打洞服务器),注意此时用户B是收不到的,用户B的路由同样会进行丢弃,但是这并不要紧,因为用户A发了这条信息后,用户A的路由就会记录关于用户B的路由信息(该信息记录的是将用户B的IP信息路由到用户A电脑),然后此时用户B再发给用户A一条信息,就不会进行丢弃了,因为用户A的路由里面有用户B的路由信息。
通过解决上面的两个问题后,我们再通过图形示意图来具体了解下打洞的过程
整个过程就是我标的序号1->2->3->4->5->6->7->8>9
过程一:1->2
此过程为用户B向服务器请求向用户A打洞
过程二:3->4
此过程为服务器相应用户B的打洞请求,告诉用户A用户B想与你打洞(数据包中包含用户B的地址信息)。
过程三:5->6
用户A主动发一条信息给用户B,目的是为了使得路由器A中能够有一条关于路由B的IP的路由信息(注意不是用户B,用户B是私有IP),就如图所示,这条信息会被丢弃的,因为路由B的路由表中没有路由A的IP的信息。
过程四:7->8->9
用户B再发一条信息给用户A,因为此时路由A的路由表中有关于路由B的IP的路由信息,此时路由A就能路由给用户A了,至此,用户A就能直接收到用户B发的信息了。注意,此时用户A发给用户B不需要打洞,因为路由B中已经有关于路由A的IP的路由信息了。
还是思路重要,所以有点啰嗦了…本来以为自己对P2P了解的算不错了,但是在书写的时候发现在细枝末节还是半知半解,因此特此去查阅了一些资料。本文难免有一些不足,希望大家指出不足。
二、P2P服务器的实现
服务端的实现:
服务端采用的是基于UDP的IOCP网络模型实现数据的收发。实现了对客户端数据的分发处理,心跳检测是否在线,数据包完整性的CRC校验等
源码过长,就贴下服务端处理数据的工作者类CWorker,源码自行下载
工作者类-Worker.h
#ifndef __WORKER_H__
#define __WORKER_H__
#include "IOCPServer.h"
#include <map>
using namespace std;
typedef list<stUserListNode *> UserList;
typedef map<stUserListNode*,int> UserMap;
class CWorker
{
public:
CWorker(int nPort = SERVER_PORT, HWND hWnd = NULL);
~CWorker();
void ShowServerInfo(); // 显示服务器的相关配置信息
bool OpenHeartThread(); // 开启心跳检测线程
void OnUsers(); // 显示所有在线用户
static CWorker* m_worker;
protected:
static void CALLBACK NotifyProc(LPVOID lParam, OVERLAPPEDPLUS* pContent, DWORD dwSize, UINT nCode);
void OnCMDLogIn(Msg msg, SOCKADDR_IN addr); // 登录信息
void OnCMDLogOut(Msg msg, SOCKADDR_IN addr); // 下线信息
void OnCMDP2Ptrans(Msg msg, SOCKADDR_IN addr); // 打洞请求信息
void OnCMDGetAllUser(Msg msg, SOCKADDR_IN addr); // 获取在线用户信息
bool SendPacketACK(MsgCmd msgCmd, SOCKADDR_IN addr); // 发送确认包
void OnCMDHeartACK(Msg msg, SOCKADDR_IN addr);
static DWORD CALLBACK HeartbeatProc(LPVOID lparam); // 心跳包即时检测客户状态
bool GetUserByName(stUserListNode & userNode, char* username);
HANDLE m_hHeartThread; // 心跳包检测线程句柄
bool m_bExitTherad; // 心跳包检测线程退出标识
CIOCPServer* m_IocpServer; // UDP IOCP服务器
UserList m_userList; // 保存用户登录信息的链表
UserMap m_userMap; // 用户心跳包检测,记录心跳
static CRITICAL_SECTION m_cs;
};
#endif
工作者类-Worker.cpp
#include "Worker.h"
#include "Lock.h"
#define MAXHEART 10 // 最大心跳次数
CWorker* CWorker::m_worker;
CRITICAL_SECTION CWorker::m_cs;
/************************************************************************/
/* 心跳检测,每一秒进行一次探测 */
/************************************************************************/
DWORD CALLBACK CWorker::HeartbeatProc(LPVOID lparam)
{
int addrlen = sizeof(SOCKADDR_IN);
SOCKADDR_IN clientAddr = { 0 };
int msglen = sizeof(Msg);
Msg heartmsg = { 0 };
heartmsg.head.sCmd = CMDHEARTMSG;
heartmsg.head.nContentLen = 0;
heartmsg.head.CRC32 = ::GetCRC32(heartmsg.content.szMsg, heartmsg.head.nContentLen);
while (!(m_worker->m_bExitTherad))
{
if (m_worker->m_userList.size() == 0)
{
Sleep(100);
continue;
}
Sleep(2000);
CLock cs(m_cs, "HeartbeatProc");
for (UserList::iterator it = m_worker->m_userList.begin();
it != m_worker->m_userList.end(); ++it)
{
if (m_worker->m_userMap[*it] == MAXHEART)//达到最大心跳次数,删除用户信息
{
printf("user [%s] logout -> has max heartbeat\n",(*it)->userName);
m_worker->m_userMap.erase(*it);
stUserListNode* tmp = *it;
it = m_worker->m_userList.erase(it);
delete tmp;
if (it == m_worker->m_userList.end())
break;
}
m_worker->m_userMap[*it]++;//心跳加一
ZeroMemory(&clientAddr, addrlen);
clientAddr.sin_family = AF_INET;
clientAddr.sin_port = ntohs((*it)->port);
clientAddr.sin_addr.s_addr = htonl((*it)->ip);
m_worker->m_IocpServer->PostSend(&clientAddr, (LPBYTE)&heartmsg, msglen);
}
}
return 0;
}
void CALLBACK CWorker::NotifyProc(LPVOID lParam, OVERLAPPEDPLUS* pContent, DWORD dwSize, UINT nCode)
{
Msg msg = { 0 };
memcpy_s(&msg, sizeof(Msg), pContent->buf, sizeof(Msg));
SOCKADDR_IN addr = pContent->remoteAddr;
switch (msg.head.sCmd)
{
case CMDLOGIN:
m_worker->SendPacketACK(CMDLOGINACK, addr);
m_worker->OnCMDLogIn(msg, addr);
break;
case CMDLOGOUT:
m_worker->SendPacketACK(CMDLOGOUTACK, addr);
m_worker->OnCMDLogOut(msg, addr);
break;
case CMDP2PTRANS:
m_worker->SendPacketACK(CMDP2PTRANSACK, addr);
m_worker->OnCMDP2Ptrans(msg, addr);
break;
case CMDP2PGETALLUSER:
m_worker->SendPacketACK(CMDP2PGETALLUSERACK, addr);
m_worker->OnCMDGetAllUser(msg, addr);
break;
case CMDHEARTMSGACK:
m_worker->OnCMDHeartACK(msg, addr);
break;
default:
break;
}
}
void CWorker::OnCMDHeartACK(Msg msg, SOCKADDR_IN addr)
{
unsigned int nIp = ntohl(addr.sin_addr.S_un.S_addr);
unsigned short nPort = ntohs(addr.sin_port);
CLock cs(m_cs, "OnCMDHeartACK");
for (UserList::iterator it = m_userList.begin(); it != m_userList.end(); ++ it)
{
if ((*it)->ip == nIp && (*it)->port == nPort &&
strcmp(msg.content.heartmessage.userName,(*it)->userName) == 0)
{
m_userMap[*it] = 0;
return;
}
}
}
void CWorker::OnCMDLogIn(Msg msg, SOCKADDR_IN addr)
{
/*
对于用户登录的密码检验没有
采用 MD5码 进行校验
*/
stUserListNode* userInfo = new stUserListNode;
strcpy_s(userInfo->userName, 10, msg.content.loginmember.userName);
userInfo->ip = ntohl(addr.sin_addr.S_un.S_addr);
userInfo->port = ntohs(addr.sin_port);
// 防止线程同步导致多条相同用户信息被存放到链表中
// 出现的情况是:由于网络阻塞,一瞬间同时收到同一个客户端的10条登录请求,此时10个线程同时工作(同时对链表操作),此时必须避免线程同步的问题!
CLock* cs = new CLock(m_cs, "OnCMDLogIn");
// 检测该用户是否已经存在
bool bExist = false;
for (UserList::iterator it = m_userList.begin(); it != m_userList.end(); ++it)
{
if (strcmp(userInfo->userName,(*it)->userName) == 0 &&
userInfo->ip == (*it)->ip && userInfo->port == (*it)->port)
{
bExist = true;
break;
}
}
// 将新连接的用户,加入链表中
if (!bExist)
{
printf("------user login : %s <-> [%s:%d]\n", userInfo->userName, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
m_userList.push_back(userInfo);
m_userMap[userInfo] = 0;
}
delete cs;// 这里不用在末尾释放已达到提升速度
// 发送已经登录的客户信息给客户端
// 1、发送 登录客户的个数,客户端根据个数进行接收
int usercount = m_userList.size();
Msg usercntmsg = { 0 };
usercntmsg.head.sCmd = CMDP2PGETUSERCNT;
usercntmsg.head.nContentLen = sizeof(int);
usercntmsg.head.CRC32 = ::GetCRC32((char*)&usercount, sizeof(int));
usercntmsg.content.usercount = usercount;
m_IocpServer->PostSend((SOCKADDR_IN*)&addr, (LPBYTE)&usercntmsg, sizeof(Msg));
// 2、开始逐条发送 客户信息
for (UserList::iterator it = m_userList.begin(); it != m_userList.end(); ++it)
{
m_IocpServer->PostSend((SOCKADDR_IN*)&addr, (LPBYTE)(*it), sizeof(stUserListNode));
}
printf("send user list information to : %s <-> [%s:%d]\n", userInfo->userName, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
}
void CWorker::OnCMDLogOut(Msg msg, SOCKADDR_IN addr)
{
CLock cs(m_cs, "OnCMDLogOut");
for (UserList::iterator it = m_userList.begin(); it != m_userList.end(); ++it)
{
if (strcmp((*it)->userName,msg.content.logoutmember.userName) == 0)
{
printf("------user logout : %s <-> [%s:%d]\n", msg.content.logoutmember.userName, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
m_userMap.erase(*it);
m_userList.remove(*it); // 移除链表节点
return;
}
}
}
void CWorker::OnCMDP2Ptrans(Msg msg, SOCKADDR_IN addr)
{
printf("[%s:%d] wants to p2p %s\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), msg.content.requesttransmsg.toName);
stUserListNode userNode;
// 获取用户信息--用户可能已经下线
if (!GetUserByName(userNode,msg.content.requesttransmsg.toName))
{
// 用户下线处理
return;
}
// 1、获取被打洞端的信息
SOCKADDR_IN remote = { 0 };
remote.sin_family = AF_INET;
remote.sin_port = htons(userNode.port);
remote.sin_addr.s_addr = htonl(userNode.ip);
printf("tell %s[%s:%d] to send p2ptrans message to: ", msg.content.requesttransmsg.toName,inet_ntoa(remote.sin_addr), ntohs(remote.sin_port));
printf("%s[%s:%d]\n", msg.content.requesttransmsg.requestName, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
// 2、打洞数据包封装
Msg transmsg = { 0 };
transmsg.content.servtransmsg.nIP = ntohl(addr.sin_addr.S_un.S_addr);
transmsg.content.servtransmsg.nPort = ntohs(addr.sin_port);
strcpy_s(transmsg.content.servtransmsg.userName, USERNAMELEN, msg.content.requesttransmsg.requestName);
transmsg.head.sCmd = CMDREQUESTP2P;
transmsg.head.nContentLen = sizeof(stP2PTranslate);
transmsg.head.CRC32 = ::GetCRC32(transmsg.content.szMsg, transmsg.head.nContentLen);
// 3、发送打洞数据包给 被打洞方,要求 被打洞方 主动更新路由器表信息
m_IocpServer->PostSend(&remote, (LPBYTE)&transmsg, sizeof(Msg));
}
void CWorker::OnCMDGetAllUser(Msg msg, SOCKADDR_IN addr)
{
// 1、发送客户个数
msg.content.usercount = m_userList.size();
msg.head.sCmd = CMDP2PGETUSERCNT;
msg.head.nContentLen = sizeof(int);
msg.head.CRC32 = ::GetCRC32(msg.content.szMsg, msg.head.nContentLen);
m_IocpServer->PostSend(&addr, (LPBYTE)&msg, sizeof(Msg));
// 2、循环发送客户信息
for (UserList::iterator it = m_userList.begin(); it != m_userList.end(); ++it)
{
m_IocpServer->PostSend(&addr, (LPBYTE)(*it), sizeof(stUserListNode));
}
printf("send user list information to : [%s:%d]\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
}
bool CWorker::SendPacketACK(MsgCmd msgCmd, SOCKADDR_IN addr)
{
Msg msgACK = { 0 };
msgACK.head.sCmd = msgCmd;
msgACK.head.nContentLen = 0;
msgACK.head.CRC32 = ::GetCRC32(msgACK.content.szMsg, 0);
m_IocpServer->PostSend(&addr, (LPBYTE)&msgACK, sizeof(Msg));
return true;
}
bool CWorker::GetUserByName(stUserListNode & userNode, char* username)
{
CLock cs(m_cs, "GetUserByName");
for (UserList::iterator it = m_userList.begin(); it != m_userList.end(); ++it)
{
if (strcmp((*it)->userName,username) == 0)
{
userNode = **it;
return true;
}
}
return false;
}
CWorker::CWorker(int nPort, HWND hWnd)
:m_IocpServer(NULL)
,m_bExitTherad(false)
{
InitializeCriticalSection(&m_cs);
m_IocpServer = new CIOCPServer;
if (!m_IocpServer->Initialize(NotifyProc, hWnd, nPort))
printf("服务端启动失败:%d", GetLastError());
printf("P2P服务器启动成功...\n");
ShowServerInfo();
}
CWorker::~CWorker()
{
try
{
if (m_IocpServer)
{
DeleteCriticalSection(&m_cs);
m_bExitTherad = true;
delete m_IocpServer;
Sleep(100);// 等待资源释放完毕
}
}
catch (...) {}
}
void CWorker::ShowServerInfo()
{
printf("服务器开启的线程数 : %d\n", m_IocpServer->m_nCurrentThreads);
printf("服务器端口号 : %d\n", m_IocpServer->m_nPort);
printf("正在工作的线程数 : %d\n", m_IocpServer->m_nBusyThreads);
printf("内存中的元素个数 : %d\n", m_IocpServer->m_listContexts.size());
printf("服务器发送即时速度 : %d KB/s\n", m_IocpServer->m_nSendKbps);
printf("服务器接收即时速度 : %d KB/s\n", m_IocpServer->m_nRecvKbps);
}
bool CWorker::OpenHeartThread()
{
m_hHeartThread = CreateThread(NULL, NULL, HeartbeatProc, m_worker, NULL, NULL);
if (m_hHeartThread == INVALID_HANDLE_VALUE)
{
printf("CreateThread failed!\n");
return false;
}
return true;
}
void CWorker::OnUsers()
{
CLock cs(m_cs, "OnUsers");
printf("\nHave %d users\n", m_userMap.size());
if (m_userMap.size() == 0)
return;
for (UserMap::iterator it = m_userMap.begin(); it != m_userMap.end(); it++)
{
in_addr tmp;
tmp.S_un.S_addr = htonl(it->first->ip);
printf("Username:%s\tIP:%s\tport:%d\theart:%d\n", it->first->userName, inet_ntoa(tmp), it->first->port,it->second);
}
}
三、P2P客户端的实现
客户端的实现:
客户端采用普通UDP套接字加多线程实现。实现了超时重发,用户登录密码的MD5加密,数据包完整性的CRC校验等
客户端的工作者类CWorker
Worker.h
#ifndef __WORKER_H__
#define __WORKER_H__
#include "Socket.h"
#include "../public/MsgProtocal.h"
#include <list>
#define MAX_RESEND 10 // 最大重发次数
#define MIN_RESEND 5 // 最小重发次数
#define MAX_RECV 10 // 最大接收次数
#define MAX_CONNECT 10 // 连接服务器的次数
typedef std::list<stUserListNode *> UserList;
class CWorker
{
public:
CWorker();
~CWorker();
static CWorker* m_worker;
static DWORD WINAPI RecvThreadProc(LPVOID lparam); // 接收消息的线程
bool IsIP(char* ip);
bool InitNetEnvironment(int nPort=0);
bool OnLogin();
void SuspendRecvThread() const
{
SuspendThread(m_hThread);
}
void ResumeRecvThread() const
{
ResumeThread(m_hThread);
}
/*
** 用户命令响应
*/
bool OnGetU();
/*
** 发送信息给在线用户
*/
bool OnSend(char* sendname, char* message);
/*
** 下线
*/
bool OnExit();
protected:
MsgCmd m_cmdACK; // 确认消息的类型
bool m_bReplyACK; // 是否判断回复的确认消息
UserList m_UserList;
Socket* m_sock;
unsigned int m_serverPort;
HANDLE m_hThread; // 线程句柄
bool m_bExit; // 线程退出标识
char m_ServerIP[20];
char m_UserName[USERNAMELEN];
char m_UserPwd[USERPASSWORDLEN];
bool CheckPackage(Msg msg);
// 丢包重发机制的收发
bool ConnectToServer(char* serverip, char* szName, char* szPwd, int max_time = MAX_CONNECT);
bool SendtoServer(Msg & msg, unsigned short nCmd, char* dstip, unsigned int dstport, int max_time = MAX_RESEND);
// 接收指定文件头的数据包
bool OnCmdRecvFrom(Msg & msg, unsigned short nCmd, char* dstip, unsigned int dstport, int max_time = MAX_RECV);
bool OnGetUsersRecvFrom(void* recvbuf, int recvlen, char* dstip, unsigned int dstport, int max_time = MAX_RECV);
};
#endif
Worker.cpp
#include "Worker.h"
#include "../public/CRC32.h"
#include "../public/MD5.h"
#include <string>
#include <regex>
using namespace std;
CWorker* CWorker::m_worker;
DWORD WINAPI CWorker::RecvThreadProc(LPVOID lparam)
{
CWorker* pThis = (CWorker*)lparam;
Msg recvMsg = { 0 };
int msglen = sizeof(Msg);
char szAddress[32] = { 0 };
unsigned int nPort = 0;
while (!(pThis->m_bExit))
{
// 此处可以改进为使用 接收的消息队列
// 其中一个线程作为接收,放入消息队里中,其中一个线程不停的在消息队列中获取消息,并进行处理!!!
ZeroMemory(&recvMsg, msglen);
ZeroMemory(szAddress, sizeof(szAddress));
int iread = pThis->m_sock->ReceiveFrom(&recvMsg, msglen, szAddress, nPort);
if (iread <= 0 || iread != msglen) // recv error
{
//printf("recvfrom failed : %d\n", GetLastError());
continue;
}
// CRC差错检测
if (recvMsg.head.CRC32 != ::GetCRC32(recvMsg.content.szMsg,recvMsg.head.nContentLen))
continue;
/******************************************************/
/* 根据数据包消息头进行分发处理 */
/*******************************************************/
switch (recvMsg.head.sCmd)
{
case CMDP2PMESSAGEACK:// P2P消息确认包的处理,来自客户端
{
pThis->m_bReplyACK = true;
printf("Recv message ack from %s:%ld\n", szAddress, nPort);
break;
}
case CMDP2PMESSAGE:// 接收到P2P消息的处理,来自客户端
{
printf("Recv Message from %s[%s:%d] -> %s\n", recvMsg.content.p2pmesssage.username,
szAddress, nPort, recvMsg.content.p2pmesssage.message);
ZeroMemory(&recvMsg, msglen);
recvMsg.head.sCmd = CMDP2PMESSAGEACK;
recvMsg.head.nContentLen = 0;
recvMsg.head.CRC32 = ::GetCRC32(recvMsg.content.szMsg, recvMsg.head.nContentLen);
pThis->m_sock->SendTo(&recvMsg, msglen, nPort, szAddress);
printf("Send a Message ACK to %s[%s:%d]\n", recvMsg.content.p2pmesssage.username, szAddress, nPort);
break;
}
case CMDP2PGETALLUSERACK:// getu 获取所有用户信息回复包,来自服务端
{
pThis->m_bReplyACK = true;
printf("Recv getu ack from server\n");
ZeroMemory(&recvMsg, sizeof(Msg));
if (!pThis->OnCmdRecvFrom(recvMsg, CMDP2PGETUSERCNT,pThis->m_ServerIP, pThis->m_serverPort))
{
printf("Get user count from server timeout...\n");
continue;
}
int usercount = recvMsg.content.usercount;
printf("Have %d users logined server\n", usercount);
// 3、开始逐条接收(可能会有丢包出现,可以记录是哪个包丢失,然后进行重新申请)
int nCntRecved = 0;
int nodelen = sizeof(stUserListNode);
pThis->m_UserList.clear();
for (int i = 0; i < usercount; i++)
{
stUserListNode *node = new stUserListNode;
if (!pThis->OnGetUsersRecvFrom(node, nodelen, pThis->m_ServerIP, pThis->m_serverPort))
{
printf("Get user info from server timeout...\n");
continue;
}
pThis->m_UserList.push_back(node);
in_addr tmp;
tmp.S_un.S_addr = htonl(node->ip);
printf("Username:%s\nUserIP:%s\nUserPort:%d\n\n", node->userName, inet_ntoa(tmp), node->port);
nCntRecved++;
}
printf("Has received %d users form server\n", nCntRecved);
break;
}
case CMDP2PTRANSACK:// 主动请求打洞确认包,来自服务端
{
pThis->m_bReplyACK = true;
printf("Recv p2ptransACK from server\n");
break;
}
case CMDREQUESTP2P:// 对方的打洞请求,来自服务端的转发
{
printf("Recv P2P request from %s\n",recvMsg.content.servtransmsg.userName);
printf("Send P2P request ACK to %s\n", recvMsg.content.servtransmsg.userName);
unsigned int toIP = recvMsg.content.servtransmsg.nIP;
unsigned short toPort = recvMsg.content.servtransmsg.nPort;
ZeroMemory(&recvMsg, msglen);
recvMsg.head.sCmd = CMDREQUESTP2PACK;
recvMsg.head.nContentLen = 0;
recvMsg.head.CRC32 = ::GetCRC32(recvMsg.content.szMsg, recvMsg.head.nContentLen);
pThis->m_sock->SendTo2(&recvMsg, msglen, toPort, toIP);
break;
}
case CMDREQUESTP2PACK:// 对方发来的打洞消息,忽略,来自客户端
{
// do nothing
printf("Recv P2P burrow ACK from %s:%d\n", szAddress, nPort);
break;
}
case CMDLOGOUTACK:// 确认下线消息
{
pThis->m_bReplyACK = true;
break;
}
case CMDHEARTMSG:// 心跳包消息
{
//printf("recv one heart packet \n");
ZeroMemory(&recvMsg, msglen);
strcpy_s(recvMsg.content.heartmessage.userName, pThis->m_UserName);
recvMsg.head.sCmd = CMDHEARTMSGACK;
recvMsg.head.nContentLen = strlen(pThis->m_UserName);
recvMsg.head.CRC32 = ::GetCRC32(recvMsg.content.szMsg, recvMsg.head.nContentLen);
pThis->m_sock->SendTo(&recvMsg, msglen, pThis->m_serverPort, pThis->m_ServerIP);
//printf("reply heart ack to server\n");
break;
}
}
}
printf("Worker Thread exit...\n");
return 0;
}
bool CWorker::InitNetEnvironment(int nPort/*=0*/)
{
if (!m_sock->Create(nPort, SOCK_DGRAM))
return false;
return true;
}
bool CWorker::IsIP(char* ip)
{
regex pattern("\\b([1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4])\\.([0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4])\\.([0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4])\\.([0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4])\\b");
return regex_match(ip, pattern);
}
bool CWorker::CheckPackage(Msg msg)
{
if (msg.head.CRC32 == ::GetCRC32(msg.content.szMsg, msg.head.nContentLen))
return true;
return false;
}
bool CWorker::OnLogin()
{
printf("\n");
do
{
ZeroMemory(m_ServerIP, sizeof(m_ServerIP));
printf("Please input server ip:");
scanf_s("%s", m_ServerIP, sizeof(m_ServerIP));
} while (!IsIP(m_ServerIP));
printf("Please input your name:");
ZeroMemory(m_UserName, USERNAMELEN);
scanf_s("%s", m_UserName, sizeof(m_UserName));
printf("Please input your password:");
ZeroMemory(m_UserPwd, USERPASSWORDLEN);
scanf_s("%s", m_UserPwd, sizeof(m_UserPwd));
return ConnectToServer(m_ServerIP, m_UserName, m_UserPwd);
}
bool CWorker::ConnectToServer(char* serverip, char* szName, char* szPwd, int max_time)
{
// 1、对密码进行MD5加密处理
string check = szName + string("#") + ::md5(szPwd);
MsgContent msgcontent = { 0 };
strcpy(msgcontent.loginmember.userName, szName);
strcpy(msgcontent.loginmember.check, check.c_str());
// 2、封装数据包,进行CRC32校验
Msg msg = { 0 };
msg.head.sCmd = CMDLOGIN;
msg.head.nContentLen = strlen(msgcontent.szMsg);
msg.head.CRC32 = GetCRC32(msgcontent.szMsg, msg.head.nContentLen);
memcpy(&msg.content.loginmember, &msgcontent, sizeof(stLoginMessage));
// 3、发送登录数据包 登录服务器
printf("begin to connect server...\n");
if(!SendtoServer(msg,CMDLOGINACK,m_ServerIP,m_serverPort))
{
printf("connect server timeout...\n");
return false;
}
// 4、登录服务器成功,开始接收在线端用户数量
ZeroMemory(&msg, sizeof(Msg));
int usercount = 0;
int iread = 0;
if (!OnCmdRecvFrom(msg, CMDP2PGETUSERCNT, m_ServerIP, m_serverPort))
{
printf("Get user count from server timeout...\n");
return false;
}
usercount = msg.content.usercount;
printf("Have %d users logined server\n", usercount);
// 5、接收在线用户信息
int nCntRecved = 0;
int nodelen = sizeof(stUserListNode);
m_UserList.clear();
for (int i = 0; i < usercount; i++)
{
stUserListNode *node = new stUserListNode;
if (!OnGetUsersRecvFrom(node, nodelen, m_ServerIP, m_serverPort))
{
printf("Get user info from server timeout...\n");
continue;
}
m_UserList.push_back(node);
in_addr tmp;
tmp.S_un.S_addr = htonl(node->ip);
printf("Username:%s\nUserIP:%s\nUserPort:%d\n\n", node->userName, inet_ntoa(tmp), node->port);
nCntRecved++;
}
printf("Has received %d users form server\n", nCntRecved);
m_hThread = CreateThread(NULL, NULL, RecvThreadProc, m_worker, NULL, NULL);
if (m_hThread == INVALID_HANDLE_VALUE)
{
printf("CreateThread Failed!\n");
return false;
}
return true;
}
bool CWorker::OnGetU()
{
int msglen = sizeof(Msg);
Msg msg = { 0 };
msg.head.sCmd = CMDP2PGETALLUSER; // 发送 CMDGETALLUSER 命令,获取用户
msg.head.nContentLen = 0;
msg.head.CRC32 = ::GetCRC32(msg.content.szMsg, 0);
for (int i = 0; i < MAX_RESEND; i++)
{
if (msglen != m_sock->SendTo(&msg, msglen, m_serverPort, m_ServerIP))
{
printf("sendto failed...\n");
continue;
}
for (int j = 0; j < 15; j++)
{
// 是否收到服务端的回复
if (m_bReplyACK)
return true;
Sleep(100);
}
}
printf("get users timeout...\n");
return false;
}
bool CWorker::OnSend(char* sendname, char* message)
{
unsigned int UserIP = 0;
unsigned short UserPort = 0;
bool bFindUser = false;
for (UserList::iterator it = m_UserList.begin(); it != m_UserList.end(); ++it)
{
if (strcmp((*it)->userName,sendname) == 0)
{
UserIP = (*it)->ip;
UserPort = (*it)->port;
bFindUser = true;
}
}
if (!bFindUser)
{
printf("Can't find username!\nSend failed!\n");
return false;
}
Msg p2pmsg = { 0 };
strcpy_s(p2pmsg.content.p2pmesssage.username, USERNAMELEN, m_UserName);
strcpy_s(p2pmsg.content.p2pmesssage.message, MAXMSGLEN, message);
p2pmsg.head.sCmd = CMDP2PMESSAGE;
p2pmsg.head.nContentLen = USERNAMELEN + MAXMSGLEN;
p2pmsg.head.CRC32 = ::GetCRC32(p2pmsg.content.szMsg, p2pmsg.head.nContentLen);
int msglen = sizeof(Msg);
/* 1、
** 发送消息给用户,每1.5秒进行一次重传,重传次数为5次
*/
m_bReplyACK = false;
for (int i = 0; i < MIN_RESEND; i++)
{
if (msglen != m_sock->SendTo2(&p2pmsg, msglen, UserPort, UserIP))
{
printf("sendto() failed:%d\n",GetLastError());
continue;
}
for (int j = 0; j < 15; j++)// 1.5 秒没有收到确认包,进行重传
{
if (m_bReplyACK)
{
printf("Send OK!\n");
return true;
}
Sleep(100);
}
}
/* 2、
** 没有收到目标主机的回应时,认为与目标主机之间没有进行穿透
** 发送穿透信息给服务器,请求打洞穿透
*/
printf("Prepare p2p to %s by P2Pserver...\n",sendname);
Msg transmsg = { 0 };
strcpy_s(transmsg.content.requesttransmsg.requestName, USERNAMELEN, m_UserName);
strcpy_s(transmsg.content.requesttransmsg.toName, USERNAMELEN, sendname);
transmsg.head.sCmd = CMDP2PTRANS;
transmsg.head.nContentLen = sizeof(stP2PClientRequestTrans);
transmsg.head.CRC32 = ::GetCRC32(transmsg.content.szMsg, transmsg.head.nContentLen);
m_bReplyACK = false;
for (int i = 0; i < MIN_RESEND; i++)
{
if(msglen != m_sock->SendTo(&transmsg, msglen, m_serverPort, m_ServerIP))
{
printf("sendto() failed:%d\n", GetLastError());
continue;
}
for (int j = 0; j < 15; j++)
{
if (m_bReplyACK)
break;
Sleep(100);
}
if (m_bReplyACK)
break;
}
/* 3、
** 再次 发送消息给用户,每1.5秒进行一次重传,重传次数为5次
*/
m_bReplyACK = false;
for (int i = 0; i < MIN_RESEND; i++)
{
if (msglen != m_sock->SendTo2(&p2pmsg, msglen, UserPort, UserIP))
{
printf("sendto() failed:%d\n", GetLastError());
continue;
}
for (int j = 0; j < 15; j++)// 1.5 秒没有收到确认包,进行重传
{
if (m_bReplyACK)
{
printf("Send OK!\n");
return true;
}
Sleep(100);
}
}
printf("Send failed!\n");
return false;
}
bool CWorker::OnExit()
{
Msg exitmsg = { 0 };
strcpy_s(exitmsg.content.logoutmember.userName, USERNAMELEN, m_UserName);
exitmsg.head.sCmd = CMDLOGOUT;
exitmsg.head.nContentLen = strlen(m_UserName);
exitmsg.head.CRC32 = ::GetCRC32(exitmsg.content.szMsg, exitmsg.head.nContentLen);
int msglen = sizeof(Msg);
m_bReplyACK = false;
for (int i = 0; i < MAX_RESEND; i++)
{
if (msglen != m_sock->SendTo(&exitmsg,msglen,m_serverPort,m_ServerIP))
continue;
for (int j = 0; j < 15; j++)
{
if (m_bReplyACK)
return true;
Sleep(100);
}
}
return false;
}
bool CWorker::OnCmdRecvFrom(Msg & msg, unsigned short nCmd, char* dstip, unsigned int dstport, int max_time)
{
SOCKET sock = m_sock->GetSOCKET();
int msglen = sizeof(Msg);
fd_set readfds = { 0 };
timeval tv;
tv.tv_sec = 1;// 1秒超时
tv.tv_usec = 0;
while (max_time--)
{
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
int nRet = select(0, &readfds, 0, 0, &tv);
if (nRet == 0)
{
continue;
}
if (nRet != SOCKET_ERROR)
{
if (FD_ISSET(sock,&readfds))
{
int iread = m_sock->ReceiveFrom(&msg, msglen, dstip, dstport);
if (iread <= 0)
continue;
// 进行对数据包 CRC 校验
if (msg.head.sCmd != nCmd || msg.head.CRC32 != ::GetCRC32(msg.content.szMsg,msg.head.nContentLen))
continue;
return true;
}
}
else
{
printf("select failed!\n");
}
}
return false;
}
bool CWorker::OnGetUsersRecvFrom(void* recvbuf, int recvlen, char* dstip, unsigned int dstport, int max_time /*= MAX_RECV*/)
{
SOCKET sock = m_sock->GetSOCKET();
fd_set readfds = { 0 };
timeval tv;
tv.tv_sec = 1;// 1秒超时
tv.tv_usec = 0;
while (max_time--)
{
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
int nRet = select(0, &readfds, NULL, NULL, &tv);
if (nRet == 0)
{
continue;
}
else if (nRet != SOCKET_ERROR)
{
if (FD_ISSET(sock, &readfds))
{
int iread = m_sock->ReceiveFrom(recvbuf, recvlen, dstip, dstport);
if (iread <= 0 || iread != recvlen)
{
//printf("Recvfrom error!\n");
continue;
}
return true;
}
}
else
{
printf("select failed!\n");
}
}
return false;
}
bool CWorker::SendtoServer(Msg & msg, unsigned short nCmd, char* dstip, unsigned int dstport, int max_time)
{
SOCKET sock = m_sock->GetSOCKET();
int msglen = sizeof(Msg);
fd_set readfds = { 0 };
timeval tv;
tv.tv_sec = 1;// 1秒超时
tv.tv_usec = 0;
while (max_time--)
{
if (msglen != m_sock->SendTo(&msg, msglen, dstport, dstip))
{
printf("Unknown sendto error\n");
continue;
}
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
int nRet = select(0, &readfds, 0, 0, &tv);
if (nRet == 0)
{
continue;
}
else if (nRet != SOCKET_ERROR)
{
if (FD_ISSET(sock, &readfds))
{
// 服务端的回复包(空的数据包)
Msg recvMsg = { 0 };
int iread = m_sock->ReceiveFrom(&recvMsg, msglen, dstip, dstport);
if(iread <= 0)
continue;
if (recvMsg.head.sCmd != nCmd || recvMsg.head.CRC32 != ::GetCRC32(recvMsg.content.szMsg, recvMsg.head.nContentLen))
continue;
return true;
}
}
else
{
printf("select failed!\n");
}
}
return false;
}
CWorker::CWorker()
{
ZeroMemory(m_ServerIP, sizeof(m_ServerIP));
ZeroMemory(m_UserName, sizeof(m_UserName));
ZeroMemory(m_UserPwd, sizeof(m_UserPwd));
m_sock = new Socket;
m_serverPort = SERVER_PORT;
m_bExit = false;
m_hThread = INVALID_HANDLE_VALUE;
m_cmdACK = nocmd;
m_bReplyACK = false;
}
CWorker::~CWorker()
{
m_bExit = true;
Sleep(10);// 等待线程退出完毕
delete m_sock;
}
四、数据包格式
#ifndef __MSGPROTOCAL_H__
#define __MSGPROTOCAL_H__
#include <windows.h>
#define SERVER_PORT 0x9870
#define MAXMSGLEN 200
#define USERNAMELEN 20
#define USERPASSWORDLEN 20
typedef enum _MsgCmd
{
CMDLOGIN = 0x1, // 登录,发送给服务端
CMDLOGINACK,
CMDLOGOUT, // 断开连接,发送给服务端
CMDLOGOUTACK,
CMDP2PTRANS, // 发送给服务端的请求打洞消息
CMDP2PTRANSACK,
CMDP2PGETALLUSER, // 发送给服务端请求获取所有在线用户的信息
CMDP2PGETALLUSERACK,
CMDP2PGETUSERCNT, // 服务端将在线的用户人数发送给客户端
CMDP2PMESSAGE, // P2P之间的通信信息,客户端之间的消息
CMDP2PMESSAGEACK,
CMDREQUESTP2P, // 服务端转发给客户端的请求打洞数据包
CMDREQUESTP2PACK,
CMDHEARTMSG, // 服务端发出的心跳探测包
CMDHEARTMSGACK, // 客户端回复的心跳探测包
nocmd,
}MsgCmd, *pMsgCmd;
// Client登录时向服务器发送的消息
struct stLoginMessage
{
char check[64];
char userName[USERNAMELEN];
};
// Client注销时发送的消息
struct stLogoutMessage
{
char userName[USERNAMELEN];
};
// Client的打洞消息
struct stP2PTranslate
{
unsigned int nIP; // ip
unsigned short nPort; // port
char userName[USERNAMELEN]; // name
};
struct stP2PClientRequestTrans
{
char requestName[USERNAMELEN];
char toName[USERNAMELEN];
};
// Client 之间发送的P2P信息
struct stP2PMessage
{
char username[USERNAMELEN];
char message[MAXMSGLEN];
};
// 客户节点信息
struct stUserListNode
{
char userName[USERNAMELEN];
unsigned int ip;
unsigned short port;
};
struct stHeartPacket
{
char userName[USERNAMELEN];
};
// 消息内容
typedef union _MsgContent
{
char szMsg[MAXMSGLEN];
stLoginMessage loginmember;
stLogoutMessage logoutmember;
stP2PTranslate servtransmsg;
stP2PClientRequestTrans requesttransmsg;
stUserListNode servermessage;
stP2PMessage p2pmesssage;
stHeartPacket heartmessage;
unsigned int usercount;
}MsgContent, *pMsgContent;
typedef struct _MsgHead
{
unsigned short sCmd; // 消息命令字
unsigned int nContentLen; // 消息数据长度
unsigned int CRC32; // 消息的CRC完整校验
}MsgHead, *pMsgHead;
// 服务端与客户端之间的消息结构
typedef struct _Msg
{
MsgHead head;
MsgContent content;
}Msg, *pMsg;
// 每接受到一个消息,必须检查消息的总长度,如果有消息数据,则必须将消息头原样数据发送回去以确认收到数据
#endif
2019/3/6 补充
为了提升打洞的效率,以及近来用golang的一些开发积累。源码暂不私有
参考文献:
http://xdxd.love/2016/10/18/对称NAT穿透的一种新方法/
链接:https://pan.baidu.com/s/1-DkDrzdkuh5uzgAGRTCcWA
提取码:214r