P2P打洞服务器与客户端

本文从以下四个部分开始分析讲解,客户端与服务端的源码在文章末尾链接

一、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

服务端以及客户端源码(编译器VS2015)

2019/3/6 补充
为了提升打洞的效率,以及近来用golang的一些开发积累。源码暂不私有
参考文献:
http://xdxd.love/2016/10/18/对称NAT穿透的一种新方法/

链接:https://pan.baidu.com/s/1-DkDrzdkuh5uzgAGRTCcWA
提取码:214r

  • 15
    点赞
  • 106
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值