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

原文地址:http://www.cnblogs.com/lancidie/archive/2013/04/13/3019359.html#2659190

依照慣例,先上代碼:

#pragma once

#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、沒了,剩下的一些特殊處理應該是上層邏輯來考慮的。比如掉線重連等。



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