1. 程式人生 > >Cocos2dx3.x使用socket建立服務端和客戶端改進

Cocos2dx3.x使用socket建立服務端和客戶端改進

轉自:小小的沮喪 的部落格

由於一個網友使用筆者寫的SocketClient作為遊戲客戶端網路資料接收類,出現了一些問題 
這個問題就是因為當執行onRecv時建立了一個Sprite(Sprite::create(“1.png”)),而建立完成後sprite的資料混亂,或者MoveTo時返回的也是混亂資料。原因在於在多執行緒申請記憶體,在主執行緒使用就會出現問題。為了解決這個問題,特意看了cocos2dx的WebSocket的實現方式,發現當接收到資料時並不是立即呼叫回撥函式,而是將資料資訊加入到訊息佇列,當主執行緒更新時檢查訊息佇列,來執行相應的回撥函式,為此就對SocketClient和SocketServer做了一些改進,當然使用方法沒有太大改變,同時解決了子執行緒申請記憶體出現的問題。 
SocketBase.h 增加列舉,及SocketMessage來儲存接收到訊息到訊息佇列

enum MessageType
{
    DISCONNECT,
    RECEIVE,
    NEW_CONNECTION
};

class SocketMessage
{
private:
    MessageType msgType;    // 訊息型別
    Data* msgData;          // 訊息資料
public:
    SocketMessage(MessageType type, unsigned char* data, int dataLen)
    {
        msgType = type;
        msgData = new
Data; msgData->copy(data, dataLen); } SocketMessage(MessageType type) { msgType = type; msgData = nullptr; } Data* getMsgData() { return msgData; } MessageType getMsgType() { return msgType; } ~SocketMessage() { if (msgData) CC_SAFE_DELETE(msgData); } };
  • 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
  • 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

增加兩個成員變數,作為處理接收的訊息

std::list<SocketMessage*> _UIMessageQueue;  // 儲存訊息的list
std::mutex   _UIMessageQueueMutex;      // 處理訊息的互斥變數
  • 1
  • 2
  • 1
  • 2

當接收到訊息時將訊息加入佇列, 仿照cocos2dx的WebSocket

if (ret > 0 && onRecv != nullptr)
{
    std::lock_guard<std::mutex> lk(_UIMessageQueueMutex);       // 互斥
    SocketMessage * msg = new SocketMessage(RECEIVE, (unsigned char*)recvBuf, ret);
    _UIMessageQueue.push_back(msg); // 加入訊息佇列
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

當在初始化客戶端時initClient,設定排程,讓UI每幀都檢查是否有訊息 
Director::getInstance()->getScheduler()->scheduleUpdate(this, 0, false); 
更新函式

void SocketClient::update(float dt)
{
    if (_UIMessageQueue.size() == 0)        // 如果沒有訊息就退出
    {
        return;
    }

    _UIMessageQueueMutex.lock();        // 第一次檢查有訊息,設定互斥

// 第二次檢查,如果已經沒有訊息就釋放互斥,要檢查兩次,舉個例子
如果有兩個排程update,第一個執行上面的檢查_UIMessageQueue.size() !=0則會互斥鎖住,這時第二個也去檢查UIMessageQueue.size() !=0,也鎖住這時要等待第一個_UIMessageQueueMutex.unlock(),第一個執行完後沒有訊息,那麼第二個執行下面的檢查,結果沒有訊息,一定要unlock,這樣才能不出錯,兩次檢查保證執行緒不互鎖。

    if (_UIMessageQueue.size() == 0)        
    {
        _UIMessageQueueMutex.unlock();
        return;
    }

    SocketMessage *msg = *(_UIMessageQueue.begin());        // 獲取第一個進入佇列的訊息,先到先服務,當然也可以用優先順序佇列,先執行優先順序高的訊息
    _UIMessageQueue.pop_front();        // 記得從佇列刪除訊息

    switch (msg->getMsgType())          // 根據訊息型別執行相應的回撥函式
    {
    case DISCONNECT:
        if (onDisconnect)
            this->onDisconnect();
        break;
    case RECEIVE:
        if (onRecv)
        {
            this->onRecv((const char*)msg->getMsgData()->getBytes(), msg->getMsgData()->getSize());
        }
        break;
    default:
        break;
    }

    CC_SAFE_DELETE(msg);            // 刪除訊息,因為儲存訊息是用的new,所以這裡要刪除
    _UIMessageQueueMutex.unlock();  // 互斥解鎖
}
  • 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
  • 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

同時為了操作方便,保證使用SocketClient時不出現new SocketClient delete SocketClient, 將建構函式和解構函式設定為私有的,看過設計模式的同學應該都知道這樣做的目的, 
提供construct建立SocketClient,和destroy銷燬SocketClient。

SocketClient* SocketClient::construct()
{
    SocketClient* client = new SocketClient;
    return client;
}

void SocketClient::destroy()
{
    delete this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在解構函式刪除相應的東西

SocketClient::~SocketClient(void)
{
    this->clear();
}

void SocketClient::clear()
{
    if (_socektClient != 0) // 關閉
    {
        _mutex.lock();
        this->closeConnect(_socektClient);
        _mutex.unlock();
    }

    for (auto msg : _UIMessageQueue)    // 刪除訊息,不對訊息進行處理
    {
        CC_SAFE_DELETE(msg);
    }
    _UIMessageQueue.clear();

    Director::getInstance()->getScheduler()->unscheduleAllForTarget(this);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

SocketServer 當有新連線請求時,也將訊息儲存在訊息佇列

if (onNewConnection)
    {
        std::lock_guard<std::mutex> lk(_UIMessageQueueMutex);
        SocketMessage * msg = new SocketMessage(NEW_CONNECTION, (unsigned char*)&socket, sizeof(HSocket));
        _UIMessageQueue.push_back(msg);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

對接收訊息做了一些改變, 
由於接收到的訊息要確定是哪個client發來的,要儲存相應的client的socket

struct RecvData
{
    HSocket socketClient;
    int dataLen;
    char data[1024];
};
if (ret > 0 && onRecv != nullptr)
{
    std::lock_guard<std::mutex> lk(_UIMessageQueueMutex);
    RecvData recvData;          // 儲存socket資訊
    recvData.socketClient = socket;
    memcpy(recvData.data, buff, ret);
    recvData.dataLen = ret;
    SocketMessage * msg = new SocketMessage(RECEIVE, (unsigned char*)&recvData, sizeof(RecvData));
    _UIMessageQueue.push_back(msg);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

同時在update時處理訊息

switch (msg->getMsgType())
    {
    case NEW_CONNECTION:
        if (onNewConnection)
        {
            this->onNewConnection(*(HSocket*)msg->getMsgData()->getBytes());
        }
        break;
    case DISCONNECT:
        if (onDisconnect)
        {
            this->onDisconnect(*(HSocket*)msg->getMsgData()->getBytes());
        }
        break;
    case RECEIVE:
        if (onRecv)
        {
            RecvData* recvData = (RecvData*)msg->getMsgData()->getBytes();
            this->onRecv(recvData->socketClient, (const char*)recvData->data, recvData->dataLen);
        }
        break;
    default:
        break;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

對服務端使用了單例模式

SocketServer* SocketServer::getInstance()
{
    if (s_server == nullptr)
    {
        s_server = new SocketServer;
    }

    return s_server;
}

void SocketServer::destroyInstance()
{
    CC_SAFE_DELETE(s_server);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

為了測試修改的正確性,特地做了一個demo,demo很簡單,啟動後選擇Server還是Client 
在Server點選任意位置就會看到一個enemy走向指定位置,如果有客戶端連線,客戶端同樣有enemy根據Server發出的訊息執行相應的命令,由於只是一個簡單的demo,並沒左太多的同步處理。 
效果如下: 
這裡寫圖片描述 
三個圖 
最上面的作為Server 
左下方在Server未啟動時連線失敗, 
右下方成功連線並接受Server控制,Server關閉時,連線斷開。