#ifdef WIN32 #include <windows.h> #include <WinSock.h> #else #include <sys/socket.h> #include <fcntl.h> #include <errno.h> #include <netinet/in.h> #include <arpa/inet.h> #define SOCKET int #define SOCKET_ERROR -1 #define INVALID_SOCKET -1 #endif #ifndef CHECKF #define CHECKF(x) \ do \ { \ if (!(x)) { \ log_msg("CHECKF", #x, __FILE__, __LINE__); \ return 0; \ } \ } while (0) #endif #define _MAX_MSGSIZE 16 * 1024 // 暫定一個消息最大為16k #define BLOCKSECONDS 30 // INIT函數阻塞時間 #define INBUFSIZE (64*1024) //? 具體尺寸根據剖面報告調整 接收數據的緩存 #define OUTBUFSIZE (8*1024) //? 具體尺寸根據剖面報告調整。 發送數據的緩存,當不超過8K時,FLUSH只需要SEND一次 class CGameSocket { public: CGameSocket(void); bool Create(const char* pszServerIP, int nServerPort, int nBlockSec = BLOCKSECONDS, bool bKeepAlive = false); bool SendMsg(void* pBuf, int nSize); bool ReceiveMsg(void* pBuf, int& nSize); bool Flush(void); bool Check(void); void Destroy(void); SOCKET GetSocket(void) const { return m_sockClient; } private: bool recvFromSock(void); // 從網絡中讀取盡可能多的數據 bool hasError(); // 是否發生錯誤,注意,異步模式未完成非錯誤 void closeSocket(); SOCKET m_sockClient; // 發送數據緩沖 char m_bufOutput[OUTBUFSIZE]; //? 可優化為指針數組 int m_nOutbufLen; // 環形緩沖區 char m_bufInput[INBUFSIZE]; int m_nInbufLen; int m_nInbufStart; // INBUF使用循環式隊列,該變量為隊列起點,0 - (SIZE-1) };
#include "stdafx.h" #include "Socket.h" CGameSocket::CGameSocket() { // 初始化 memset(m_bufOutput, 0, sizeof(m_bufOutput)); memset(m_bufInput, 0, sizeof(m_bufInput)); } void CGameSocket::closeSocket() { #ifdef WIN32 closesocket(m_sockClient); WSACleanup(); #else close(m_sockClient); #endif } bool CGameSocket::Create(const char* pszServerIP, int nServerPort, int nBlockSec, bool bKeepAlive /*= FALSE*/) { // 檢查參數 if(pszServerIP == 0 || strlen(pszServerIP) > 15) { return false; } #ifdef WIN32 WSADATA wsaData; WORD version = MAKEWORD(2, 0); int ret = WSAStartup(version, &wsaData);//win sock start up if (ret != 0) { return false; } #endif // 創建主套接字 m_sockClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(m_sockClient == INVALID_SOCKET) { closeSocket(); return false; } // 設置SOCKET為KEEPALIVE if(bKeepAlive) { int optval=1; if(setsockopt(m_sockClient, SOL_SOCKET, SO_KEEPALIVE, (char *) &optval, sizeof(optval))) { closeSocket(); return false; } } #ifdef WIN32 DWORD nMode = 1; int nRes = ioctlsocket(m_sockClient, FIONBIO, &nMode); if (nRes == SOCKET_ERROR) { closeSocket(); return false; } #else // 設置為非阻塞方式 fcntl(m_sockClient, F_SETFL, O_NONBLOCK); #endif unsigned long serveraddr = inet_addr(pszServerIP); if(serveraddr == INADDR_NONE) // 檢查IP地址格式錯誤 { closeSocket(); return false; } sockaddr_in addr_in; memset((void *)&addr_in, 0, sizeof(addr_in)); addr_in.sin_family = AF_INET; addr_in.sin_port = htons(nServerPort); addr_in.sin_addr.s_addr = serveraddr; if(connect(m_sockClient, (sockaddr *)&addr_in, sizeof(addr_in)) == SOCKET_ERROR) { if (hasError()) { closeSocket(); return false; } else // WSAWOLDBLOCK { timeval timeout; timeout.tv_sec = nBlockSec; timeout.tv_usec = 0; fd_set writeset, exceptset; FD_ZERO(&writeset); FD_ZERO(&exceptset); FD_SET(m_sockClient, &writeset); FD_SET(m_sockClient, &exceptset); int ret = select(FD_SETSIZE, NULL, &writeset, &exceptset, &timeout); if (ret == 0 || ret < 0) { closeSocket(); return false; } else // ret > 0 { ret = FD_ISSET(m_sockClient, &exceptset); if(ret) // or (!FD_ISSET(m_sockClient, &writeset) { closeSocket(); return false; } } } } m_nInbufLen = 0; m_nInbufStart = 0; m_nOutbufLen = 0; struct linger so_linger; so_linger.l_onoff = 1; so_linger.l_linger = 500; setsockopt(m_sockClient, SOL_SOCKET, SO_LINGER, (const char*)&so_linger, sizeof(so_linger)); return true; } bool CGameSocket::SendMsg(void* pBuf, int nSize) { if(pBuf == 0 || nSize <= 0) { return false; } if (m_sockClient == INVALID_SOCKET) { return false; } // 檢查通訊消息包長度 int packsize = 0; packsize = nSize; // 檢測BUF溢出 if(m_nOutbufLen + nSize > OUTBUFSIZE) { // 立即發送OUTBUF中的數據,以清空OUTBUF。 Flush(); if(m_nOutbufLen + nSize > OUTBUFSIZE) { // 出錯了 Destroy(); return false; } } // 數據添加到BUF尾 memcpy(m_bufOutput + m_nOutbufLen, pBuf, nSize); m_nOutbufLen += nSize; return true; } bool CGameSocket::ReceiveMsg(void* pBuf, int& nSize) { //檢查參數 if(pBuf == NULL || nSize <= 0) { return false; } if (m_sockClient == INVALID_SOCKET) { return false; } // 檢查是否有一個消息(小於2則無法獲取到消息長度) if(m_nInbufLen < 2) { // 如果沒有請求成功 或者 如果沒有數據則直接返回 if(!recvFromSock() || m_nInbufLen < 2) { // 這個m_nInbufLen更新了 return false; } } // 計算要拷貝的消息的大小(一個消息,大小為整個消息的第一個16字節),因為環形緩沖區,所以要分開計算 int packsize = (unsigned char)m_bufInput[m_nInbufStart] + (unsigned char)m_bufInput[(m_nInbufStart + 1) % INBUFSIZE] * 256; // 注意字節序,高位+低位 // 檢測消息包尺寸錯誤 暫定最大16k if (packsize <= 0 || packsize > _MAX_MSGSIZE) { m_nInbufLen = 0; // 直接清空INBUF m_nInbufStart = 0; return false; } // 檢查消息是否完整(如果將要拷貝的消息大於此時緩沖區數據長度,需要再次請求接收剩余數據) if (packsize > m_nInbufLen) { // 如果沒有請求成功 或者 依然無法獲取到完整的數據包 則返回,直到取得完整包 if (!recvFromSock() || packsize > m_nInbufLen) { // 這個m_nInbufLen已更新 return false; } } // 復制出一個消息 if(m_nInbufStart + packsize > INBUFSIZE) { // 如果一個消息有回卷(被拆成兩份在環形緩沖區的頭尾) // 先拷貝環形緩沖區末尾的數據 int copylen = INBUFSIZE - m_nInbufStart; memcpy(pBuf, m_bufInput + m_nInbufStart, copylen); // 再拷貝環形緩沖區頭部的剩余部分 memcpy((unsigned char *)pBuf + copylen, m_bufInput, packsize - copylen); nSize = packsize; } else { // 消息沒有回卷,可以一次拷貝出去 memcpy(pBuf, m_bufInput + m_nInbufStart, packsize); nSize = packsize; } // 重新計算環形緩沖區頭部位置 m_nInbufStart = (m_nInbufStart + packsize) % INBUFSIZE; m_nInbufLen -= packsize; return true; } bool CGameSocket::hasError() { #ifdef WIN32 int err = WSAGetLastError(); if(err != WSAEWOULDBLOCK) { #else int err = errno; if(err != EINPROGRESS && err != EAGAIN) { #endif return true; } return false; } // 從網絡中讀取盡可能多的數據,實際向服務器請求數據的地方 bool CGameSocket::recvFromSock(void) { if (m_nInbufLen >= INBUFSIZE || m_sockClient == INVALID_SOCKET) { return false; } // 接收第一段數據 int savelen, savepos; // 數據要保存的長度和位置 if(m_nInbufStart + m_nInbufLen < INBUFSIZE) { // INBUF中的剩余空間有回繞 savelen = INBUFSIZE - (m_nInbufStart + m_nInbufLen); // 后部空間長度,最大接收數據的長度 } else { savelen = INBUFSIZE - m_nInbufLen; } // 緩沖區數據的末尾 savepos = (m_nInbufStart + m_nInbufLen) % INBUFSIZE; CHECKF(savepos + savelen <= INBUFSIZE); int inlen = recv(m_sockClient, m_bufInput + savepos, savelen, 0); if(inlen > 0) { // 有接收到數據 m_nInbufLen += inlen; if (m_nInbufLen > INBUFSIZE) { return false; } // 接收第二段數據(一次接收沒有完成,接收第二段數據) if(inlen == savelen && m_nInbufLen < INBUFSIZE) { int savelen = INBUFSIZE - m_nInbufLen; int savepos = (m_nInbufStart + m_nInbufLen) % INBUFSIZE; CHECKF(savepos + savelen <= INBUFSIZE); inlen = recv(m_sockClient, m_bufInput + savepos, savelen, 0); if(inlen > 0) { m_nInbufLen += inlen; if (m_nInbufLen > INBUFSIZE) { return false; } } else if(inlen == 0) { Destroy(); return false; } else { // 連接已斷開或者錯誤(包括阻塞) if (hasError()) { Destroy(); return false; } } } } else if(inlen == 0) { Destroy(); return false; } else { // 連接已斷開或者錯誤(包括阻塞) if (hasError()) { Destroy(); return false; } } return true; } bool CGameSocket::Flush(void) //? 如果 OUTBUF > SENDBUF 則需要多次SEND() { if (m_sockClient == INVALID_SOCKET) { return false; } if(m_nOutbufLen <= 0) { return true; } // 發送一段數據 int outsize; outsize = send(m_sockClient, m_bufOutput, m_nOutbufLen, 0); if(outsize > 0) { // 刪除已發送的部分 if(m_nOutbufLen - outsize > 0) { memcpy(m_bufOutput, m_bufOutput + outsize, m_nOutbufLen - outsize); } m_nOutbufLen -= outsize; if (m_nOutbufLen < 0) { return false; } } else { if (hasError()) { Destroy(); return false; } } return true; } bool CGameSocket::Check(void) { // 檢查狀態 if (m_sockClient == INVALID_SOCKET) { return false; } char buf[1]; int ret = recv(m_sockClient, buf, 1, MSG_PEEK); if(ret == 0) { Destroy(); return false; } else if(ret < 0) { if (hasError()) { Destroy(); return false; } else { // 阻塞 return true; } } else { // 有數據 return true; } return true; } void CGameSocket::Destroy(void) { // 關閉 struct linger so_linger; so_linger.l_onoff = 1; so_linger.l_linger = 500; int ret = setsockopt(m_sockClient, SOL_SOCKET, SO_LINGER, (const char*)&so_linger, sizeof(so_linger)); closeSocket(); m_sockClient = INVALID_SOCKET; m_nInbufLen = 0; m_nInbufStart = 0; m_nOutbufLen = 0; memset(m_bufOutput, 0, sizeof(m_bufOutput)); memset(m_bufInput, 0, sizeof(m_bufInput)); }
// 發送消息 bSucSend = m_pSocket->SendMsg(buf, nLen); // 接收消息處理(放到游戲主循環中,每幀處理) if (!m_pSocket) { return; } if (!m_pSocket->Check()) { m_pSocket = NULL; // 掉線了 onConnectionAbort(); return; } // 發送數據(向服務器發送消息) m_pSocket->Flush(); // 接收數據(取得緩沖區中的所有消息,直到緩沖區為空) while (true) { char buffer[_MAX_MSGSIZE] = { 0 }; int nSize = sizeof(buffer); char* pbufMsg = buffer; if(m_pSocket == NULL) { break; } if (!m_pSocket->ReceiveMsg(pbufMsg, nSize)) { break; } while (true) { MsgHead* pReceiveMsg = (MsgHead*)(pbufMsg); uint16 dwCurMsgSize = pReceiveMsg->usSize; // CCLOG("msgsize: %d", dwCurMsgSize); if((int)dwCurMsgSize > nSize || dwCurMsgSize <= 0) { // broken msg break; } CMessageSubject::instance().OnMessage((const char*)pReceiveMsg, pReceiveMsg->usSize); pbufMsg += dwCurMsgSize; nSize -= dwCurMsgSize; if(nSize <= 0) { break; } } }
這樣的一個Socket封裝,適用於windows mac ios android等平台, Socket處理是異步非阻塞的,所以可以放心的放到主線程處理消息, 最大支持64k的接收消息緩沖(一般一個消息不可能大於3k)。
這里展示這個,目的並不是說這個封裝有多么優異,多么高科技,多么牛x。 恰恰是想表達它的簡單。 這個簡單的封裝完全可以勝任一個mmo客戶端的消息底層(注意是客戶端,服務器對消息底層的性能要求要遠遠大於客戶端),甚至是魔獸世界這類的大型mmo都可以用這么一個小的封裝來做消息底層。
對於游戲客戶端消息底層的要求非常簡單,根本不需要boost::asio什么的開源庫。
1、非阻塞模型,這樣我才放心把消息處理放到主線程,多線程處理消息其實很浪費。不知道得多大型的mmo才會用到。
2、消息接收緩存處理,避免大消息被截掉。
3、沒了,剩下的一些特殊處理應該是上層邏輯來考慮的。比如掉線重連等。
轉載地址:http://blog.csdn.net/langresser_king/article/details/8646088