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關閉時,連線斷開。