跨平臺的遊戲客戶端Socket封裝

原帖: http://blog.csdn.net/langresser_king/article/details/8646088


依照慣例,先上代碼:

  1. #pragma once  
  2.   
  3. #ifdef WIN32  
  4. #include <windows.h>  
  5. #include <WinSock.h>  
  6. #else  
  7. #include <sys/socket.h>  
  8. #include <fcntl.h>  
  9. #include <errno.h>  
  10. #include <netinet/in.h>  
  11. #include <arpa/inet.h>  
  12.   
  13. #define SOCKET int  
  14. #define SOCKET_ERROR -1  
  15. #define INVALID_SOCKET -1  
  16.   
  17. #endif  
  18.   
  19. #ifndef CHECKF  
  20. #define CHECKF(x) \  
  21.     do \  
  22. { \  
  23.     if (!(x)) { \  
  24.     log_msg("CHECKF", #x, __FILE__, __LINE__); \  
  25.     return 0; \  
  26.     } \  
  27. while (0)  
  28. #endif  
  29.   
  30. #define _MAX_MSGSIZE 16 * 1024      // 暫定一個消息最大爲16k  
  31. #define BLOCKSECONDS    30          // INIT函數阻塞時間  
  32. #define INBUFSIZE   (64*1024)       //? 具體尺寸根據剖面報告調整  接收數據的緩存  
  33. #define OUTBUFSIZE  (8*1024)        //? 具體尺寸根據剖面報告調整。 發送數據的緩存,當不超過8K時,FLUSH只需要SEND一次  
  34.   
  35. class CGameSocket {  
  36. public:  
  37.     CGameSocket(void);  
  38.     bool    Create(const char* pszServerIP, int nServerPort, int nBlockSec = BLOCKSECONDS, bool bKeepAlive = false);  
  39.     bool    SendMsg(void* pBuf, int nSize);  
  40.     bool    ReceiveMsg(void* pBuf, int& nSize);  
  41.     bool    Flush(void);  
  42.     bool    Check(void);  
  43.     void    Destroy(void);  
  44.     SOCKET  GetSocket(voidconst { return m_sockClient; }  
  45. private:  
  46.     bool    recvFromSock(void);     // 從網絡中讀取儘可能多的數據  
  47.     bool    hasError();         // 是否發生錯誤,注意,異步模式未完成非錯誤  
  48.     void    closeSocket();  
  49.   
  50.     SOCKET  m_sockClient;  
  51.   
  52.     // 發送數據緩衝  
  53.     char    m_bufOutput[OUTBUFSIZE];    //? 可優化爲指針數組  
  54.     int     m_nOutbufLen;  
  55.   
  56.     // 環形緩衝區  
  57.     char    m_bufInput[INBUFSIZE];  
  58.     int     m_nInbufLen;  
  59.     int     m_nInbufStart;              // INBUF使用循環式隊列,該變量爲隊列起點,0 - (SIZE-1)  
  60. };  

  1. #include "stdafx.h"  
  2. #include "Socket.h"  
  3.   
  4. CGameSocket::CGameSocket()  
  5. {   
  6.     // 初始化  
  7.     memset(m_bufOutput, 0, sizeof(m_bufOutput));  
  8.     memset(m_bufInput, 0, sizeof(m_bufInput));  
  9. }  
  10.   
  11. void CGameSocket::closeSocket()  
  12. {  
  13. #ifdef WIN32  
  14.     closesocket(m_sockClient);  
  15.     WSACleanup();  
  16. #else  
  17.     close(m_sockClient);  
  18. #endif  
  19. }  
  20.   
  21. bool CGameSocket::Create(const char* pszServerIP, int nServerPort, int nBlockSec, bool bKeepAlive /*= FALSE*/)  
  22. {  
  23.     // 檢查參數  
  24.     if(pszServerIP == 0 || strlen(pszServerIP) > 15) {  
  25.         return false;  
  26.     }  
  27.   
  28. #ifdef WIN32  
  29.     WSADATA wsaData;  
  30.     WORD version = MAKEWORD(2, 0);  
  31.     int ret = WSAStartup(version, &wsaData);//win sock start up  
  32.     if (ret != 0) {  
  33.         return false;  
  34.     }  
  35. #endif  
  36.   
  37.     // 創建主套接字  
  38.     m_sockClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
  39.     if(m_sockClient == INVALID_SOCKET) {  
  40.         closeSocket();  
  41.         return false;  
  42.     }  
  43.   
  44.     // 設置SOCKET爲KEEPALIVE  
  45.     if(bKeepAlive)  
  46.     {  
  47.         int     optval=1;  
  48.         if(setsockopt(m_sockClient, SOL_SOCKET, SO_KEEPALIVE, (char *) &optval, sizeof(optval)))  
  49.         {  
  50.             closeSocket();  
  51.             return false;  
  52.         }  
  53.     }  
  54.   
  55. #ifdef WIN32  
  56.     DWORD nMode = 1;  
  57.     int nRes = ioctlsocket(m_sockClient, FIONBIO, &nMode);  
  58.     if (nRes == SOCKET_ERROR) {  
  59.         closeSocket();  
  60.         return false;  
  61.     }  
  62. #else  
  63.     // 設置爲非阻塞方式  
  64.     fcntl(m_sockClient, F_SETFL, O_NONBLOCK);  
  65. #endif  
  66.   
  67.     unsigned long serveraddr = inet_addr(pszServerIP);  
  68.     if(serveraddr == INADDR_NONE)   // 檢查IP地址格式錯誤  
  69.     {  
  70.         closeSocket();  
  71.         return false;  
  72.     }  
  73.   
  74.     sockaddr_in addr_in;  
  75.     memset((void *)&addr_in, 0, sizeof(addr_in));  
  76.     addr_in.sin_family = AF_INET;  
  77.     addr_in.sin_port = htons(nServerPort);  
  78.     addr_in.sin_addr.s_addr = serveraddr;  
  79.       
  80.     if(connect(m_sockClient, (sockaddr *)&addr_in, sizeof(addr_in)) == SOCKET_ERROR) {  
  81.         if (hasError()) {  
  82.             closeSocket();  
  83.             return false;  
  84.         }  
  85.         else    // WSAWOLDBLOCK  
  86.         {  
  87.             timeval timeout;  
  88.             timeout.tv_sec  = nBlockSec;  
  89.             timeout.tv_usec = 0;  
  90.             fd_set writeset, exceptset;  
  91.             FD_ZERO(&writeset);  
  92.             FD_ZERO(&exceptset);  
  93.             FD_SET(m_sockClient, &writeset);  
  94.             FD_SET(m_sockClient, &exceptset);  
  95.   
  96.             int ret = select(FD_SETSIZE, NULL, &writeset, &exceptset, &timeout);  
  97.             if (ret == 0 || ret < 0) {  
  98.                 closeSocket();  
  99.                 return false;  
  100.             } else  // ret > 0  
  101.             {  
  102.                 ret = FD_ISSET(m_sockClient, &exceptset);  
  103.                 if(ret)     // or (!FD_ISSET(m_sockClient, &writeset)  
  104.                 {  
  105.                     closeSocket();  
  106.                     return false;  
  107.                 }  
  108.             }  
  109.         }  
  110.     }  
  111.   
  112.     m_nInbufLen     = 0;  
  113.     m_nInbufStart   = 0;  
  114.     m_nOutbufLen    = 0;  
  115.   
  116.     struct linger so_linger;  
  117.     so_linger.l_onoff = 1;  
  118.     so_linger.l_linger = 500;  
  119.     setsockopt(m_sockClient, SOL_SOCKET, SO_LINGER, (const char*)&so_linger, sizeof(so_linger));  
  120.   
  121.     return true;  
  122. }  
  123.   
  124. bool CGameSocket::SendMsg(void* pBuf, int nSize)  
  125. {  
  126.     if(pBuf == 0 || nSize <= 0) {  
  127.         return false;  
  128.     }  
  129.   
  130.     if (m_sockClient == INVALID_SOCKET) {  
  131.         return false;  
  132.     }  
  133.   
  134.     // 檢查通訊消息包長度  
  135.     int packsize = 0;  
  136.     packsize = nSize;  
  137.   
  138.     // 檢測BUF溢出  
  139.     if(m_nOutbufLen + nSize > OUTBUFSIZE) {  
  140.         // 立即發送OUTBUF中的數據,以清空OUTBUF。  
  141.         Flush();  
  142.         if(m_nOutbufLen + nSize > OUTBUFSIZE) {  
  143.             // 出錯了  
  144.             Destroy();  
  145.             return false;  
  146.         }  
  147.     }  
  148.     // 數據添加到BUF尾  
  149.     memcpy(m_bufOutput + m_nOutbufLen, pBuf, nSize);  
  150.     m_nOutbufLen += nSize;  
  151.     return true;  
  152. }  
  153.   
  154. bool CGameSocket::ReceiveMsg(void* pBuf, int& nSize)  
  155. {  
  156.     //檢查參數  
  157.     if(pBuf == NULL || nSize <= 0) {  
  158.         return false;  
  159.     }  
  160.       
  161.     if (m_sockClient == INVALID_SOCKET) {  
  162.         return false;  
  163.     }  
  164.   
  165.     // 檢查是否有一個消息(小於2則無法獲取到消息長度)  
  166.     if(m_nInbufLen < 2) {  
  167.         //  如果沒有請求成功  或者   如果沒有數據則直接返回  
  168.         if(!recvFromSock() || m_nInbufLen < 2) {     // 這個m_nInbufLen更新了  
  169.             return false;  
  170.         }  
  171.     }  
  172.   
  173.     // 計算要拷貝的消息的大小(一個消息,大小爲整個消息的第一個16字節),因爲環形緩衝區,所以要分開計算  
  174.     int packsize = (unsigned char)m_bufInput[m_nInbufStart] +  
  175.         (unsigned char)m_bufInput[(m_nInbufStart + 1) % INBUFSIZE] * 256; // 注意字節序,高位+低位  
  176.   
  177.     // 檢測消息包尺寸錯誤 暫定最大16k  
  178.     if (packsize <= 0 || packsize > _MAX_MSGSIZE) {  
  179.         m_nInbufLen = 0;        // 直接清空INBUF  
  180.         m_nInbufStart = 0;  
  181.         return false;  
  182.     }  
  183.   
  184.     // 檢查消息是否完整(如果將要拷貝的消息大於此時緩衝區數據長度,需要再次請求接收剩餘數據)  
  185.     if (packsize > m_nInbufLen) {  
  186.         // 如果沒有請求成功   或者    依然無法獲取到完整的數據包  則返回,直到取得完整包  
  187.         if (!recvFromSock() || packsize > m_nInbufLen) { // 這個m_nInbufLen已更新  
  188.             return false;  
  189.         }  
  190.     }  
  191.   
  192.     // 複製出一個消息  
  193.     if(m_nInbufStart + packsize > INBUFSIZE) {  
  194.         // 如果一個消息有回捲(被拆成兩份在環形緩衝區的頭尾)  
  195.         // 先拷貝環形緩衝區末尾的數據  
  196.         int copylen = INBUFSIZE - m_nInbufStart;  
  197.         memcpy(pBuf, m_bufInput + m_nInbufStart, copylen);  
  198.   
  199.         // 再拷貝環形緩衝區頭部的剩餘部分  
  200.         memcpy((unsigned char *)pBuf + copylen, m_bufInput, packsize - copylen);  
  201.         nSize = packsize;  
  202.     } else {  
  203.         // 消息沒有回捲,可以一次拷貝出去  
  204.         memcpy(pBuf, m_bufInput + m_nInbufStart, packsize);  
  205.         nSize = packsize;  
  206.     }  
  207.   
  208.     // 重新計算環形緩衝區頭部位置  
  209.     m_nInbufStart = (m_nInbufStart + packsize) % INBUFSIZE;  
  210.     m_nInbufLen -= packsize;  
  211.     return  true;  
  212. }  
  213.   
  214. bool CGameSocket::hasError()  
  215. {  
  216. #ifdef WIN32  
  217.     int err = WSAGetLastError();  
  218.     if(err != WSAEWOULDBLOCK) {  
  219. #else  
  220.     int err = errno;  
  221.     if(err != EINPROGRESS && err != EAGAIN) {  
  222. #endif  
  223.         return true;  
  224.     }  
  225.   
  226.     return false;  
  227. }  
  228.   
  229. // 從網絡中讀取儘可能多的數據,實際向服務器請求數據的地方  
  230. bool CGameSocket::recvFromSock(void)  
  231. {  
  232.     if (m_nInbufLen >= INBUFSIZE || m_sockClient == INVALID_SOCKET) {  
  233.         return false;  
  234.     }  
  235.   
  236.     // 接收第一段數據  
  237.     int savelen, savepos;           // 數據要保存的長度和位置  
  238.     if(m_nInbufStart + m_nInbufLen < INBUFSIZE)  {   // INBUF中的剩餘空間有迴繞  
  239.         savelen = INBUFSIZE - (m_nInbufStart + m_nInbufLen);        // 後部空間長度,最大接收數據的長度  
  240.     } else {  
  241.         savelen = INBUFSIZE - m_nInbufLen;  
  242.     }  
  243.   
  244.     // 緩衝區數據的末尾  
  245.     savepos = (m_nInbufStart + m_nInbufLen) % INBUFSIZE;  
  246.     CHECKF(savepos + savelen <= INBUFSIZE);  
  247.     int inlen = recv(m_sockClient, m_bufInput + savepos, savelen, 0);  
  248.     if(inlen > 0) {  
  249.         // 有接收到數據  
  250.         m_nInbufLen += inlen;  
  251.           
  252.         if (m_nInbufLen > INBUFSIZE) {  
  253.             return false;  
  254.         }  
  255.   
  256.         // 接收第二段數據(一次接收沒有完成,接收第二段數據)  
  257.         if(inlen == savelen && m_nInbufLen < INBUFSIZE) {  
  258.             int savelen = INBUFSIZE - m_nInbufLen;  
  259.             int savepos = (m_nInbufStart + m_nInbufLen) % INBUFSIZE;  
  260.             CHECKF(savepos + savelen <= INBUFSIZE);  
  261.             inlen = recv(m_sockClient, m_bufInput + savepos, savelen, 0);  
  262.             if(inlen > 0) {  
  263.                 m_nInbufLen += inlen;  
  264.                 if (m_nInbufLen > INBUFSIZE) {  
  265.                     return false;  
  266.                 }     
  267.             } else if(inlen == 0) {  
  268.                 Destroy();  
  269.                 return false;  
  270.             } else {  
  271.                 // 連接已斷開或者錯誤(包括阻塞)  
  272.                 if (hasError()) {  
  273.                     Destroy();  
  274.                     return false;  
  275.                 }  
  276.             }  
  277.         }  
  278.     } else if(inlen == 0) {  
  279.         Destroy();  
  280.         return false;  
  281.     } else {  
  282.         // 連接已斷開或者錯誤(包括阻塞)  
  283.         if (hasError()) {  
  284.             Destroy();  
  285.             return false;  
  286.         }  
  287.     }  
  288.   
  289.     return true;  
  290. }  
  291.   
  292. bool CGameSocket::Flush(void)       //? 如果 OUTBUF > SENDBUF 則需要多次SEND()  
  293. {  
  294.     if (m_sockClient == INVALID_SOCKET) {  
  295.         return false;  
  296.     }  
  297.   
  298.     if(m_nOutbufLen <= 0) {  
  299.         return true;  
  300.     }  
  301.       
  302.     // 發送一段數據  
  303.     int outsize;  
  304.     outsize = send(m_sockClient, m_bufOutput, m_nOutbufLen, 0);  
  305.     if(outsize > 0) {  
  306.         // 刪除已發送的部分  
  307.         if(m_nOutbufLen - outsize > 0) {  
  308.             memcpy(m_bufOutput, m_bufOutput + outsize, m_nOutbufLen - outsize);  
  309.         }  
  310.   
  311.         m_nOutbufLen -= outsize;  
  312.   
  313.         if (m_nOutbufLen < 0) {  
  314.             return false;  
  315.         }  
  316.     } else {  
  317.         if (hasError()) {  
  318.             Destroy();  
  319.             return false;  
  320.         }  
  321.     }  
  322.   
  323.     return true;  
  324. }  
  325.   
  326. bool CGameSocket::Check(void)  
  327. {  
  328.     // 檢查狀態  
  329.     if (m_sockClient == INVALID_SOCKET) {  
  330.         return false;  
  331.     }  
  332.   
  333.     char buf[1];  
  334.     int ret = recv(m_sockClient, buf, 1, MSG_PEEK);  
  335.     if(ret == 0) {  
  336.         Destroy();  
  337.         return false;  
  338.     } else if(ret < 0) {  
  339.         if (hasError()) {  
  340.             Destroy();  
  341.             return false;  
  342.         } else {    // 阻塞  
  343.             return true;  
  344.         }  
  345.     } else {    // 有數據  
  346.         return true;  
  347.     }  
  348.       
  349.     return true;  
  350. }  
  351.   
  352. void CGameSocket::Destroy(void)  
  353. {  
  354.     // 關閉  
  355.     struct linger so_linger;  
  356.     so_linger.l_onoff = 1;  
  357.     so_linger.l_linger = 500;  
  358.     int ret = setsockopt(m_sockClient, SOL_SOCKET, SO_LINGER, (const char*)&so_linger, sizeof(so_linger));  
  359.   
  360.     closeSocket();  
  361.   
  362.     m_sockClient = INVALID_SOCKET;  
  363.     m_nInbufLen = 0;  
  364.     m_nInbufStart = 0;  
  365.     m_nOutbufLen = 0;  
  366.   
  367.     memset(m_bufOutput, 0, sizeof(m_bufOutput));  
  368.     memset(m_bufInput, 0, sizeof(m_bufInput));  
  369. }  

  1. // 發送消息  
  2. bSucSend = m_pSocket->SendMsg(buf, nLen);  
  3.   
  4. // 接收消息處理(放到遊戲主循環中,每幀處理)  
  5. if (!m_pSocket) {  
  6.         return;  
  7.     }  
  8.   
  9.     if (!m_pSocket->Check()) {  
  10.         m_pSocket = NULL;  
  11.         // 掉線了  
  12.         onConnectionAbort();  
  13.         return;  
  14.     }  
  15.   
  16.     // 發送數據(向服務器發送消息)  
  17.     m_pSocket->Flush();  
  18.   
  19.     // 接收數據(取得緩衝區中的所有消息,直到緩衝區爲空)  
  20.     while (true)  
  21.     {  
  22.         char buffer[_MAX_MSGSIZE] = { 0 };  
  23.         int nSize = sizeof(buffer);  
  24.         char* pbufMsg = buffer;  
  25.         if(m_pSocket == NULL)  
  26.         {  
  27.             break;  
  28.         }  
  29.         if (!m_pSocket->ReceiveMsg(pbufMsg, nSize)) {  
  30.             break;  
  31.         }  
  32.           
  33.         while (true)  
  34.         {  
  35.             MsgHead* pReceiveMsg = (MsgHead*)(pbufMsg);  
  36.             uint16  dwCurMsgSize = pReceiveMsg->usSize;  
  37. //          CCLOG("msgsize: %d", dwCurMsgSize);  
  38.   
  39.             if((int)dwCurMsgSize > nSize || dwCurMsgSize <= 0) {  // broken msg  
  40.                 break;  
  41.             }  
  42.   
  43.             CMessageSubject::instance().OnMessage((const char*)pReceiveMsg, pReceiveMsg->usSize);  
  44.   
  45.             pbufMsg += dwCurMsgSize;  
  46.             nSize   -= dwCurMsgSize;  
  47.             if(nSize <= 0) {  
  48.                 break;  
  49.             }  
  50.         }  
  51.     }  

        這樣的一個Socket封裝,適用於windows mac ios android等平臺, Socket處理是異步非阻塞的,所以可以放心的放到主線程處理消息, 最大支持64k的接收消息緩衝(一般一個消息不可能大於3k)。

        這裏展示這個,目的並不是說這個封裝有多麼優異,多麼高科技,多麼牛x。  恰恰是想表達它的簡單。  這個簡單的封裝完全可以勝任一個mmo客戶端的消息底層(注意是客戶端,服務器對消息底層的性能要求要遠遠大於客戶端),甚至是魔獸世界這類的大型mmo都可以用這麼一個小的封裝來做消息底層。

       對於遊戲客戶端消息底層的要求非常簡單,根本不需要boost::asio什麼的開源庫。

       1、非阻塞模型,這樣我才放心把消息處理放到主線程,多線程處理消息其實很浪費。不知道得多大型的mmo纔會用到。

       2、消息接收緩存處理,避免大消息被截掉。

       3、沒了,剩下的一些特殊處理應該是上層邏輯來考慮的。比如掉線重連等。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章