COCOS2D-X 网络篇---强联网(采用技术 BSD SOCKET+多线程技术 +protobuf)客户端实战篇
发表于2016-05-26
下面我们就正式开始客户端的搭建 首先我献给大家画一张我的客户端实现的流程图
我PS 画的大家不要见怪啊 不过流程就是这样的
搭建看到我上面的框架图的时候 就知道我的大概设计思路,
boy 在这里强调一点 这个是用异步的结构实现 其中线程类 我是参照Java 里面的方法。
好了废话不多 首先先上 BSD SOCKET 这个核心类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | /* * 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 typedef int socklen_t; #else #include #include #include #include #include #include #include #include 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 这个方法 他会一直等到读取到数据,所以在我们正常的开发游戏过程中。我们都会把它放到单独的线程中来执行。防止我们的主线程卡死。
好下面介绍一下我们的 连接线程类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #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; }; |
这个线程类是用来连接 我们的 服务器的 大家看到我上面的方法就可以才想到 我这个类是一个单利的模式。因为一个游戏中正常情况下只需要一个 套接字即可。所以我这个类采用了单利的模式可以获取一个套接字对象 。
下面贴上实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | #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服务器了
下面贴出
接收线程类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #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); //断网回调函数 }; |
这个并不算一个完整的类,因为不同的命令对应的回调函数不一样 当然你也可以弄成一个回调函数,不过我喜欢把东西分开来写
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | #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 // 登陆 |
当大家看到接收代码的时候或许很困惑。这里是引文大小端的问。还有前八个字节是我们规定好的,所以只要解析出前八个字节我们就知道服务器给传送过来的什么。 然后调用响应的回调 通知请求方即可。
下面贴出 一个消息体组装类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | #pragma once #include #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::BaseRequestMsg( void ){ } template < typename rquest= "" > BaseRequestMsg::~BaseRequestMsg( void ){ } template < typename rquest= "" > void BaseRequestMsg::setRequestMessage(Rquest message){ std::string data; message.SerializeToString(&data); this ->requestMessage=data; } template < typename rquest= "" > void BaseRequestMsg::setMessageHead( short code, int player){ messageHead.code=ConvertEndianUtil::convertEndianShort(code); messageHead.playerid=ConvertEndianUtil::convertForInt(player); } template < typename rquest= "" > char * BaseRequestMsg::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::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 ; } } typename > typename > typename > typename > typename > typename > typename > |
这里采用了C++ 类模板 这样可以让我们的程序更通用一点。 这个主要是针对protobuf 协议体的组装。
下面给大家贴上一段调用代码
1 2 3 4 5 6 7 | BaseRequestMsg* baserlong= new BaseRequestMsg(); zzboy::protobuf::ChatMsgReq req; req.set_msgtype(1); req.set_message( "ddddd" ); baserlong->setMessageHead(( short )1000,( int )1); baserlong->setRequestMessage(req); baserlong->sendMessage(); |
这就是一个发送消息的方法,哈哈看起来很简单吧。
这里我做的demo 是 聊天系统。在很多游戏里面都有聊天。这里就是一个简单的实现。不过整体的思路应该是这样
红色区域内 1 表示我自己说的话 4545 是别人说的话。其实只要服务器通知我 有人给我发送消息。都会在这里展示出来。
在这里我遇见一个BUG 就是 CCLabelTTF* lable = CCLabelTTF::create(tem, "Arial", 24); 这个标签的创建 在线程的回调函数中我始终穿件不成功。导致我用了另外的一个办法解决。这里如果谁知道这个BUG 为什么 请私信给我活着留言给我。咱们共同进步
哈哈 写到这里网络连接着一块就完了,其实感觉也没什么,最重要的就是你解析过数据之后要干什么。大家发现BOY 是不是全才 服务器和客户端都会 。我感觉如果时间允许我自己可以做一个小型的多人在线网络游戏。 哈哈。
关于上面讲到的可能有些人还是不明白。可以留言给我或者在码农哥的群里给我说,如果我看到了都会给大家讲解。这两天这是忍着病给大家写的 有哪些写的不好请见谅。