Cocos2dx3.x使用socket建立服務端和用戶端改進,cocos2dx3.xsocket
由於一個網友使用筆者寫的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); }};
增加兩個成員變數,作為處理接收的訊息
std::list<SocketMessage*> _UIMessageQueue; // 儲存訊息的liststd::mutex _UIMessageQueueMutex; // 處理訊息的互斥變數
當接收到訊息時將訊息排入佇列, 仿照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); // 加入訊息佇列}
當在初始化用戶端時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(); // 互斥解鎖}
同時為了操作方便,保證使用SocketClient時不出現new SocketClient delete SocketClient, 將建構函式和解構函式設定為私人的,看過設計模式的同學應該都知道這樣做的目的,
提供construct建立SocketClient,和destroy銷毀SocketClient。
SocketClient* SocketClient::construct(){ SocketClient* client = new SocketClient; return client;}void SocketClient::destroy(){ delete this;}
在解構函式刪除相應的東西
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);}
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); }
對接收訊息做了一些改變,
由於接收到的訊息要確定是哪個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);}
同時在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; }
對服務端使用了單例模式
SocketServer* SocketServer::getInstance(){ if (s_server == nullptr) { s_server = new SocketServer; } return s_server;}void SocketServer::destroyInstance(){ CC_SAFE_DELETE(s_server);}
為了測試修改的正確性,特地做了一個demo,demo很簡單,啟動後選擇Server還是Client
在Server點擊任意位置就會看到一個enemy走向指定位置,如果有用戶端串連,用戶端同樣有enemy根據Server發出的訊息執行相應的命令,由於只是一個簡單的demo,並沒左太多的同步處理。
效果如下:
三個圖
最上面的作為Server
左下方在Server未啟動時串連失敗,
右下方成功串連並接受Server控制,Server關閉時,串連斷開。
源碼及資源下載