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

发表于2016-05-26
评论1 2k浏览
  下面我们就正式开始客户端的搭建 首先我献给大家画一张我的客户端实现的流程图
  我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 是不是全才 服务器和客户端都会 。我感觉如果时间允许我自己可以做一个小型的多人在线网络游戏。 哈哈。 
  关于上面讲到的可能有些人还是不明白。可以留言给我或者在码农哥的群里给我说,如果我看到了都会给大家讲解。这两天这是忍着病给大家写的 有哪些写的不好请见谅。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引