跟着BOY 学习COCOS2D-X 网络篇---强联网(采用技术 BSD SOCKET+多线程技术 +protobuf)客户端实战篇

时间:2023-02-08 09:06:23

               如果按照上面的一讲 你如果把环境搭建好了,下面我们就正式开始客户端的搭建 首先我献给大家画一张我的客户端实现的流程图

              我PS 画的大家不要见怪啊 不过流程就是这样的

            跟着BOY 学习COCOS2D-X 网络篇---强联网(采用技术 BSD SOCKET+多线程技术 +protobuf)客户端实战篇

 搭建看到我上面的框架图的时候 就知道我的大概设计思路,

   boy 在这里强调一点 这个是用异步的结构实现  其中线程类 我是参照java 里面的方法。

   好了废话不多 首先先上 BSD SOCKET 这个核心类

   

   

/*
 * define file about portable socket class.
 * description:this sock is suit both windows and linux
 * design:odison
 * e-mail:odison@126.com>
 *
 */

#ifndef _ODSOCKET_H_
#define _ODSOCKET_H_

#ifdef WIN32
	#include <winsock2.h>
	typedef int				socklen_t;
#else
	#include <sys/socket.h>
	#include <netinet/in.h>
	#include <netdb.h>
	#include <fcntl.h>
	#include <unistd.h>
	#include <sys/stat.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	typedef int				SOCKET;

	//#pragma region define win32 const variable in linux
	#define INVALID_SOCKET	-1
	#define SOCKET_ERROR	-1
	//#pragma endregion
#endif


class ODSocket {

public:
	ODSocket(SOCKET sock = INVALID_SOCKET);
	~ODSocket();

	// Create socket object for snd/recv data
	bool Create(int af, int type, int protocol = 0);

	// Connect socket
	bool Connect(const char* ip, unsigned short port);
	//#region server
	// Bind socket
	bool Bind(unsigned short port);

	// Listen socket
	bool Listen(int backlog = 5);

	// Accept socket
	bool Accept(ODSocket& s, char* fromip = NULL);
	//#endregion
	int Select();
	// Send socket
	int Send(const char* buf, int len, int flags = 0);

	// Recv socket
	int Recv(char* buf, int len, int flags = 0);

	// Close socket
	int Close();

	// Get errno
	int GetError();

	//#pragma region just for win32
	// Init winsock DLL
	static int Init();
	// Clean winsock DLL
	static int Clean();
	//#pragma endregion

	// Domain parse
	static bool DnsParse(const char* domain, char* ip);

	ODSocket& operator = (SOCKET s);

	operator SOCKET ();

protected:
	SOCKET m_sock;
	fd_set  fdR;
};

#endif
对于这个类 我主要讲解四个方法  一个就是 Connect 这个方法  这个主要是用来连接的

第二个  Send 方法 这个主要是用来发送数据的 

第三个方法 Recv  这个主要是用来接收数据的、

第四个方法 Select  这个主要用来判断当前socket 的状态,这里我只用了 判断是否有数据回来这个方法。 


这里说明一下  一旦调用recv 这个方法 他会一直等到读取到数据,所以在我们正常的开发游戏过程中。我们都会把它放到单独的线程中来执行。防止我们的主线程卡死。


好下面介绍一下我们的 连接线程类

#pragma once
#include "ODSocket.h"
#include "pthread.h"
class SocketThread
{
public:	
	~SocketThread(void);
	static SocketThread*   GetInstance();
	int start();  
	ODSocket getSocket();
	int state;// 0 表示连接成功 1 表示连接失败
	ODSocket csocket;	
	void stop();//函数中止当前线程。
private:
	pthread_t pid;	
	static void* start_thread(void *);//静态成员函数,相当于C中的全局函数 	
	SocketThread(void);
private:
	static SocketThread* m_pInstance;	
};

这个线程类是用来连接 我们的 服务器的    大家看到我上面的方法就可以才想到  我这个类是一个单利的模式。因为一个游戏中正常情况下只需要一个 套接字即可。所以我这个类采用了单利的模式可以获取一个套接字对象  。

下面贴上实现

  

#include "SocketThread.h"
#include "cocos2d.h"
#include "ResPonseThread.h"
USING_NS_CC;
int SocketThread::start(){    	
	int errCode = 0;
	do{
		pthread_attr_t tAttr;
		errCode = pthread_attr_init(&tAttr);
		CC_BREAK_IF(errCode!=0);
		//但是上面这个函数其他内容则主要为你创建的线程设定为分离式
		errCode = pthread_attr_setdetachstate(&tAttr, PTHREAD_CREATE_DETACHED);
		if (errCode!=0) {
			pthread_attr_destroy(&tAttr);
			break;
		}		
		errCode = pthread_create(&pid,&tAttr,start_thread,this);
	}while (0);
	return errCode;
} 


void* SocketThread::start_thread(void *arg)   {  
	SocketThread* thred=(SocketThread*)arg;
	ODSocket cdSocket;
	cdSocket.Init();	
	bool isok=cdSocket.Create(AF_INET,SOCK_STREAM,0);	
	bool iscon=cdSocket.Connect("127.0.0.1",8888);
	if(iscon){
		thred->state=0;
		ResPonseThread::GetInstance()->start();// 启动响应参数
		CCLOG("conection");
	}else{
		thred->state=1;
	}	
	thred->csocket=cdSocket;
	return NULL;                                                                                    
}
ODSocket SocketThread::getSocket(){
	return this->csocket;
}

SocketThread* SocketThread::m_pInstance=new SocketThread; 
SocketThread* SocketThread::GetInstance(){	
	return m_pInstance;
}

void SocketThread::stop(){
	pthread_cancel(pid);
	pthread_detach(pid); 
}

SocketThread::SocketThread(void)
{

}


SocketThread::~SocketThread(void)
{
	if(m_pInstance!=NULL){
		delete m_pInstance;
	}
}
对于多线程不是很熟悉的同学们可以在 百度 下相关的资料。只要实现了上面的类,我们就可以连接tong服务器了


下面贴出

接收线程类

#pragma once
// 此类主要 处理服务器推送过来的消息
#include "pthread.h"
#include "cocos2d.h"
#include "BaseResponseMsg.h"
typedef void (cocos2d::CCObject::*ResPonseThreadEvent)(BaseResponseMsg*);
#define callFunc_selectormsg(_SELECTOR) (ResPonseThreadEvent)(&_SELECTOR)

#define M_ADDCALLBACKEVENT(varName)\
protected: cocos2d::CCObject* m_##varName##listener;ResPonseThreadEvent varName##selector;\
public: void add##varName##ListenerEvent(ResPonseThreadEvent m_event,cocos2d::CCObject* listener)  { m_##varName##listener=listener;varName##selector=m_event; }

class ResPonseThread
{
public:	
	~ResPonseThread(void);
	static ResPonseThread*   GetInstance(); // 获取该类的单利
	int start (void * =NULL); //函数是线程启动函数,其输入参数是无类型指针。
	void stop();     //函数中止当前线程。
	void sleep (int tesec); //函数让当前线程休眠给定时间,单位为毫秒秒。
	void detach();   //
	void * wait();
    	
private:
	ResPonseThread(void);
	pthread_t handle; 
	bool started;
	bool detached;
	static void * threadFunc(void *);
	static ResPonseThread* m_pInstance;	
	M_ADDCALLBACKEVENT(msg);// 聊天回调函数
	M_ADDCALLBACKEVENT(notcon);//断网回调函数
	
};

这个并不算一个完整的类,因为不同的命令对应的回调函数不一样 当然你也可以弄成一个回调函数,不过我喜欢把东西分开来写

实现代码

#include "ResPonseThread.h"
#include "cocos2d.h"
#include "SocketThread.h"
#include "BaseResponseMsg.h"
ResPonseThread* ResPonseThread::m_pInstance=new ResPonseThread; 
ResPonseThread* ResPonseThread::GetInstance(){	
	return m_pInstance;
}
ResPonseThread::ResPonseThread(void)
{

	this->m_msglistener=NULL;

	started = detached = false;
}


ResPonseThread::~ResPonseThread(void)
{
	stop();
}
int ResPonseThread::start(void * param){    	
	int errCode = 0;
	do{
		pthread_attr_t attributes;
		errCode = pthread_attr_init(&attributes);
		CC_BREAK_IF(errCode!=0);
		//但是上面这个函数其他内容则主要为你创建的线程设定为分离式
		errCode = pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
		if (errCode!=0) {
			pthread_attr_destroy(&attributes);
			break;
		}		
		errCode = pthread_create(&handle, &attributes,threadFunc,this);
		started = true; 
	}while (0);
	return errCode;
} 

void* ResPonseThread::threadFunc(void *arg){
	ResPonseThread* thred=(ResPonseThread*)arg;	
	ODSocket csocket=SocketThread::GetInstance()->getSocket();
	if(SocketThread::GetInstance()->state==0){
		while(true){
			// 表示服务器端 有消息推送过来
			if(csocket.Select()==-2){				
				char recvBuf[8];// 获取请求头的 数据
				int i=	csocket.Recv(recvBuf,8,0);				
				if (i==8){				
					char dc1[2]={recvBuf[1],recvBuf[0]};
					short len = *(short*)&dc1[0];
					char dc2[2]={recvBuf[3],recvBuf[2]};
					short code = *(short*)&dc2[0];
					char dc3[4]={recvBuf[7],recvBuf[6],recvBuf[5],recvBuf[4]};
					int playId=*(int*)&dc3[0];
					CCLOG("%d",playId);
					char* messbody=NULL;
					int myl=0;
					if(len>8){							
						myl=len-8;
						messbody=new char[myl];						
						csocket.Recv(messbody,myl,0);					  
					}
					//	//1001 = com.lx.command.player.LoginCmd
					//1002 = com.lx.command.player.RegisterCmd
					//1003 = com.lx.command.player.HeartBeatCmd
					// 登陆

					BaseResponseMsg* basmsg=new BaseResponseMsg();
					basmsg->code=code;
					basmsg->len=len;
					basmsg->playerId=playId;
					// 表示服务器推动过来的消息
					if(code==1000){
						if(thred->m_msglistener){
						basmsg->setStringToMsg(messbody,myl);
						(thred->m_msglistener->*(thred->msgselector))(basmsg);
						}
					}
					else {
						CCLOG("%d",code);
					}
				}else {
					if(thred->m_notconlistener){
					 BaseResponseMsg* basmsg=new BaseResponseMsg();
					 basmsg->state=1;// 连接断开
					(thred->m_notconlistener->*(thred->notconselector))(basmsg);
					}
					break;
				}

			}
		}
	}
	return NULL;
}


void ResPonseThread::stop(){
	if (started && !detached) { 
		pthread_cancel(handle);
		pthread_detach(handle); 
		detached = true; 
	}
}

void * ResPonseThread::wait(){
	void * status = NULL;
	if (started && !detached) { 
		pthread_join(handle, &status); 
	}
	return status;
}
void ResPonseThread::sleep(int secstr){
	timeval timeout = { secstr/1000, secstr%1000}; 
	select(0, NULL, NULL, NULL, &timeout);
}

void ResPonseThread::detach(){
	if (started && !detached) {
		pthread_detach(handle);
	} 
	detached = true;
}

 当大家看到接收代码的时候或许很困惑。这里是引文大小端的问。还有前八个字节是我们规定好的,所以只要解析出前八个字节我们就知道服务器给传送过来的什么。 然后调用响应的回调 通知请求方即可。

   下面贴出 一个消息体组装类

    

#pragma once
#include <string.h>
#include "ConvertEndianUtil.h"
#include "ODSocket.h"
#include "SocketThread.h"
typedef struct messageHead{
	short len;
	short code;
	int   playerid;
} messagehead;
template  <typename  Rquest> class BaseRequestMsg
{
public:
	BaseRequestMsg(void);
	~BaseRequestMsg(void);
	void setRequestMessage(Rquest message);// 设置请求体
	void setMessageHead(short code,int player=0);// 设置 请求头
	bool sendMessage();// 发送信息
private:
	Rquest requestmessage;
	messagehead messageHead;
	char* getSendMessage();	
	short dateLength;
	std::string requestMessage;	
};

template  <typename  Rquest>
BaseRequestMsg<Rquest>::BaseRequestMsg(void){
}
template  <typename  Rquest>
BaseRequestMsg<Rquest>::~BaseRequestMsg(void){
}

template  <typename  Rquest>
void BaseRequestMsg<Rquest>::setRequestMessage(Rquest message){
	std::string data; 
	message.SerializeToString(&data);
	this->requestMessage=data;
}

template  <typename  Rquest>
void BaseRequestMsg<Rquest>::setMessageHead(short code,int player){
	messageHead.code=ConvertEndianUtil::convertEndianShort(code);
	messageHead.playerid=ConvertEndianUtil::convertForInt(player);
}

template  <typename  Rquest>
char* BaseRequestMsg<Rquest>::getSendMessage(){
	short total=8+requestMessage.length();
	dateLength=total;
	messageHead.len=ConvertEndianUtil::convertEndianShort(total);
	char* requestmessage=new char[total];
	char* requestmessagehead=(char*)&messageHead;	
	int i=sizeof(messageHead);
	int len=sizeof(requestMessage.c_str())/sizeof(char);
	memcpy(requestmessage,requestmessagehead,8);
	memcpy(&requestmessage[8],requestMessage.c_str(),requestMessage.length());
	return requestmessage;
}

template  <typename  Rquest>
bool BaseRequestMsg<Rquest>::sendMessage(){	
	ODSocket cSocket=SocketThread::GetInstance()->getSocket();
	char* dd=this->getSendMessage();
	int cout=cSocket.Send(this->getSendMessage(),this->dateLength,0);
	if(cout==this->dateLength){
		return true;
	}else {
		return false;
	}
}
 这里采用了C++ 类模板  这样可以让我们的程序更通用一点。 这个主要是针对protobuf 协议体的组装。

 下面给大家贴上一段调用代码

	BaseRequestMsg<zzboy::protobuf::ChatMsgReq>* baserlong=new BaseRequestMsg<zzboy::protobuf::ChatMsgReq>();
	zzboy::protobuf::ChatMsgReq req;
	req.set_msgtype(1);
	req.set_message("ddddd");
	baserlong->setMessageHead((short)1000,(int)1);
	baserlong->setRequestMessage(req);
	baserlong->sendMessage();
这就是一个发送消息的方法,哈哈看起来很简单吧。

   这里我做的demo 是 聊天系统。在很多游戏里面都有聊天。这里就是一个简单的实现。不过整体的思路应该是这样

   跟着BOY 学习COCOS2D-X 网络篇---强联网(采用技术 BSD SOCKET+多线程技术 +protobuf)客户端实战篇

   

红色区域内 1 表示我自己说的话  4545 是别人说的话。其实只要服务器通知我 有人给我发送消息。都会在这里展示出来。

在这里我遇见一个BUG  就是 CCLabelTTF* lable = CCLabelTTF::create(tem, "Arial", 24); 这个标签的创建 在线程的回调函数中我始终穿件不成功。导致我用了另外的一个办法解决。这里如果谁知道这个BUG  为什么 请私信给我活着留言给我。咱们共同进步


                  哈哈 写到这里网络连接着一块就完了,其实感觉也没什么,最重要的就是你解析过数据之后要干什么。大家发现BOY 是不是全才 服务器和客户端都会 。我感觉如果时间允许我自己可以做一个小型的多人在线网络游戏。 哈哈。 

                   关于上面讲到的可能有些人还是不明白。可以留言给我或者在码农哥的群里给我说,如果我看到了都会给大家讲解。这两天这是忍着病给大家写的 有哪些写的不好请见谅。

     客户端源码下载