cocos2dx上的通用socket通信(二)

前言

额,我发现我竟然1年多没上CSDN写东西了,而且关于cocos2d-x上得通用socket通信的文章竟然是阅读量最大的。确实没想到,而且当时那篇文章只是记录了一个想法。那时候对网络编程不是很熟悉,并且对UI的多线程处理也没什么经验,所以没有具体的实现,看到评论里面大家都求代码,应该这方面需求量还是挺大的。而且我当时记录文章的时候,还在用cocos2d-x 2.1.3,里面比较成熟的应该是CCHttpClient吧,用的时curl库(其实curl库中是能得到socket对象的)实现了HTTP请求的发送和接收,BSD的socket实现还没有例子。然后当时记得是用了一个ODSocket的简单实现,郁闷的是当时搜索相关的文章讲在cocos2d-x下实现socket通信的还真不多,当时还觉得好怪异,为啥没这方面的文章,现在回过头来想想,实在是这个问题提得不是很合理,以为BSD的socket其实在Win32、iOS、Android的NDK上都有标准支持,一般写过对应客户端的网络通信的都会写,所以,我把它归类为大家不屑于写这方面的代码。

文章比较多的还是提供思路上的的引导,而非代码上的,所以要代码的同学可能是要失望了,不过如果能看完这篇文章,自己应该能大致有个概念,因为写代码还是需要查阅不少资料的呀,比如stl库的实用,讲开去实在不是我能力范围,憋了快两年才来写这个文章续篇的人的水平也就这样了。

还有就是,写教程的难度太大,好的教程,作者还是要花费很多的精力来做到语言和表意上的准确,还有好多的校对工作,最重要的还不能误人子弟,压力太大。丑话说在前头,我这个不是教程,仅仅是自己总结的一些经验,只能保证说,我不是有意来“误人子弟”的,大家借鉴借鉴即可,别认真到直接拿去商用啥的,责任太大,扛不起啊。不过我还是想帮助对这方面知识有困扰的朋友,毕竟想想当年遇到这些问题时各种搜索无门的郁闷也是醉了。

阅读要求

对此文章阅读的读者,应该具备一定的C++程序编写能力,大概了解socket是个什么东西,并且有写过一点相关的代码,能使用stl库等基础类库的使用,最好能对多线程编程有一定了解,否则看不懂我也无法帮你了,这个不是扫盲类的文章,还是需要有一定的编程基础,所以看不懂就去google吧。
对于cocos2d-x的了解倒要求不高,有个大致了解即可。

运行环境

引擎版本:cocos2d-x 2.1.3以上(之前的我没接触过,不敢保证一定可行)
操作系统:Windows7以上(XP应该也没问题)、iOS 4.3以上、Android 2.2以上
开发工具:VS2008以上(我用的是2010)、XCode 4以上(我这里从这个版本开始接触)、NDK r8e以上(我开始接触的版本)
开发语言:C++(C++99,C++11这里会涉及到创建多线程的代码上的不同)

特别说明

这里特别说明下,因为是基于cocos2d-x的通用socket通信,所以一切都是针对客户端的代码处理,而不讨论任何服务端的socket代码编写。平心而论,客户端的通信处理代码复杂度要小于服务器端。甚至,客户端使用单线程也能完成大部分的工作。
在Windows、iOS、Android的SDK中,都支持BSD的socket实现,所以,这才算是最原生的通信实现吧。
最好的老师其实不是别人的文章,而是源代码本身,不要畏惧源代码。特别是我写的东西,最多只是带着进门的师兄罢了(我一般不带源代码,额,因为我比较懒惰,写文章都快把我折腾死了,所以我不是好老师),修行得看你自己的悟性了。

如何和服务器完成基本的交互

使用socket和服务器交互,我们需要首先创建socket对象(其实是个系统提供的int值),然后connect服务器,成功后可以send数据,然后receive返回的数据。这就是一个基本的客户端和服务端的socket交互流程了(顺带提一下,服务器的流程就略微复杂一点,它要先创建socket对象,然后bind绑定socket服务的端口,再开始listen监听外部的socket链接请求,如果有链接过来了,还需要accept接收并创建一个真正交互用的socket对象,之后再是根据业务去send或者recv对应的数据。步骤多了也会复杂许多,这里就不讨论了,有兴趣的可以自己去搜索下相关的文章)。应该网络编程的课上都有这个流程吧。
既然要写socket的例子,那肯定要用到那些socket的函数,直接用未免暴力了点,那我就来做点小封装吧(直接使用ODSocket也可以,我这里就把ODSocket做个小罗列)。

GameSocket解析

头文件引用

#ifdef WIN32
	#include <winsock2.h>
	typedef int				socklen_t;
#else
	#include <sys/socket.h>
	#include <netinet/in.h>
	#include <netdb.h>
	#include <fcntl.h>
	#include <unistd.h>
	#include <sys/stat.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	#include <errno.h>
	typedef int				SOCKET;

	//#pragma region define win32 const variable in linux
	#define INVALID_SOCKET	-1
	#define SOCKET_ERROR	-1
	//#pragma endregion
#endif

头文件主要是为了跨平台,其实三个平台的话就Windows特殊点(Android和iOS说穿了是一条大血脉上的),需要加载winsock2.h的头文件。Windows下需要定义socklen_t类型(主要是socket对应API函数返回值)。

GameSocket类

class GameSocket {

public:
	GameSocket(SOCKET sock = INVALID_SOCKET);
	~GameSocket();

	// Create socket object for snd/recv data
	bool Create(int af, int type, int protocol = 0);

	// Connect socket
	bool Connect(const char* ip, unsigned short port);
	//#region server
	// Bind socket
	bool Bind(unsigned short port);

	// Listen socket
	bool Listen(int backlog = 5); 

	// Accept socket
	bool Accept(GameSocket& s, char* fromip = NULL);
	//#endregion
	
	// Send socket
	int Send(const char* buf, int len, int flags = 0);

	// Recv socket
	int Recv(char* buf, int len, int flags = 0);
	
	// Close socket
	int Close();

	// Get errno
	int GetError();
	
	//#pragma region just for win32
	// Init winsock DLL 
	static int Init();	
	// Clean winsock DLL
	static int Clean();
	//#pragma endregion

	// Domain parse
	static bool DnsParse(const char* domain, char* ip);
    
   <span style="white-space:pre">	</span>int  SetNonblock();

	GameSocket& operator = (SOCKET s);
<span style="white-space:pre">	</span><pre name="code" class="cpp"><span style="white-space:pre">	</span>void SetConnected(bool bConned);
// Statusbool IsInited(); bool IsConnected();bool IsCreated(); public:// AttributesSOCKET m_sock;protected:// Attributesstatic bool m_bInited;bool m_bCreated; bool m_bConnected;};
 
 

看下头文件,大致能了解这个做了啥,基本的就是把socket对应的API分平台的封装了一系列接口。

m_bInited这个是静态变量,其实只是针对Win32平台设置的,其余两个平台都是直接赋值为true,而Win32平台因为需要初始化socket环境。只要初始化成功后就不需要再初始化了。

注意,Connect函数的第一个参数需要ip地址,域名是不行的,但是有提供DnsParse静态函数,把域名转换为ip地址,这就搞定了。还有个设置socket为非阻塞模式的成员函数SetNonblock()。(啊,这个我要坦白,此模式不甚清楚,可能是设置当前套接字在发送数据时,直接返回啥的)

源文件实现

#include "GameSocket.h"

#ifdef WIN32
#pragma comment(lib, "Ws2_32")
#endif

// 初始化标志 WIN32 只需要初始化一次
bool GameSocket::m_bInited = false;

GameSocket::GameSocket(SOCKET sock)
{
	m_sock = sock;

	// 记录初始化标志
	m_bInited = Init() == 0 ? true : false;

	m_bCreated = false;

	m_bConnected = false;
}

GameSocket::~GameSocket()
{
	Close();
	Clean();
}

int GameSocket::Init()
{       
	if (!m_bInited)
	{
#ifdef WIN32
		/*
		http://msdn.microsoft.com/zh-cn/vstudio/ms741563(en-us,VS.85).aspx

		typedef struct WSAData { 
		WORD wVersion;		//winsock version
		WORD wHighVersion;	//The highest version of the Windows Sockets specification that the Ws2_32.dll can support
		char szDescription[WSADESCRIPTION_LEN+1]; 
		char szSystemStatus[WSASYSSTATUS_LEN+1]; 
		unsigned short iMaxSockets; 
		unsigned short iMaxUdpDg; 
		char FAR * lpVendorInfo; 
		}WSADATA, *LPWSADATA; 
		*/
		WSADATA wsaData;
		//#define MAKEWORD(a,b) ((WORD) (((BYTE) (a)) | ((WORD) ((BYTE) (b))) << 8)) 
		WORD version = MAKEWORD(2, 0);
		int ret = WSAStartup(version, &wsaData);//win sock start up
		if ( ret )
		{
			//		cerr << "Initilize winsock error !" << endl;
			return -1;
		}
#endif
		m_bInited = true;
	}
	return 0;
}
//this is just for windows
int GameSocket::Clean()
{
	m_bInited = false;
#ifdef WIN32
	return (WSACleanup());
#endif
	return 0;
}

GameSocket& GameSocket::operator = (SOCKET s)
{
	m_sock = s;
	return (*this);
}

//create a socket object win/lin is the same
// af:
bool GameSocket::Create(int af, int type, int protocol)
{
	m_sock = socket(af, type, protocol);
	if ( m_sock == INVALID_SOCKET ) 
	{
		return false;
	}
	m_bCreated = true;
	return true;
}

bool GameSocket::Connect(const char* ip, unsigned short port)
{
	struct sockaddr_in svraddr;
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = inet_addr(ip);
	svraddr.sin_port = htons(port);

	int ret = connect(m_sock, (struct sockaddr*)&svraddr, sizeof(svraddr));
	if ( ret == SOCKET_ERROR )
	{
		m_bConnected = false;
		return false;
	}

	m_bConnected = true;
	return true;
}

bool GameSocket::Bind(unsigned short port)
{
	struct sockaddr_in svraddr;
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = INADDR_ANY;
	svraddr.sin_port = htons(port);

	int opt =  1;
	if ( setsockopt(m_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)) < 0 ) 
		return false;

	int ret = bind(m_sock, (struct sockaddr*)&svraddr, sizeof(svraddr));
	if ( ret == SOCKET_ERROR ) {
		return false;
	}
	return true;
}
//for server
bool GameSocket::Listen(int backlog)
{
	int ret = listen(m_sock, backlog);
	if ( ret == SOCKET_ERROR ) {
		return false;
	}
	return true;
}

bool GameSocket::Accept(GameSocket& s, char* fromip)
{
	struct sockaddr_in cliaddr;
	socklen_t addrlen = sizeof(cliaddr);
	SOCKET sock = accept(m_sock, (struct sockaddr*)&cliaddr, &addrlen);
	if ( sock == SOCKET_ERROR ) {
		return false;
	}

	s = sock;
	if ( fromip != NULL )
		sprintf(fromip, "%s", inet_ntoa(cliaddr.sin_addr));

	return true;
}

int GameSocket::Send(const char* buf, int len, int flags)
{
	int bytes;
	int count = 0;
    
	while ( count < len )
	{

		bytes = send(m_sock, buf + count, len - count, flags);
		if ( bytes == -1 || bytes == 0 )
			return -1;
		count += bytes;
	} 

	return count;
}

int GameSocket::Recv(char* buf, int len, int flags)
{
    <span style="white-space:pre">	</span>if (m_sock ==INVALID_SOCKET) {
        <span style="white-space:pre">	</span>return 0;
    <span style="white-space:pre">	</span>}
    
<span style="white-space:pre">	</span>return (recv(m_sock, buf, len, flags));
}

int GameSocket::Close()
{
	m_bCreated = false;
	m_bConnected = false;
#ifdef WIN32
	return (closesocket(m_sock));
#else
	{
		int nRet = 0;
		if (m_sock!=INVALID_SOCKET)
		{
            
			nRet = shutdown(m_sock, SHUT_RDWR);  
			m_sock = INVALID_SOCKET;
		}
		return nRet;
	}
#endif
}

int GameSocket::GetError()
{
#ifdef WIN32
	return (WSAGetLastError());
#else
	return (errno);
#endif
}

bool GameSocket::DnsParse(const char* domain, char* ip)
{
	struct hostent* p;
	if ( (p = gethostbyname(domain)) == NULL )
		return false;

	sprintf(ip, 
		"%u.%u.%u.%u",
		(unsigned char)p->h_addr_list[0][0], 
		(unsigned char)p->h_addr_list[0][1], 
		(unsigned char)p->h_addr_list[0][2], 
		(unsigned char)p->h_addr_list[0][3]);

	return true;
}

//将一个socket设置成非阻塞模式
//不论什么平台编写网络程序,都应该使用NONBLOCK socket的方式。这样可以保证你的程序至少不会在recv/send/accept/connect这些操作上发生block从而将整个网络服务都停下来
int GameSocket::SetNonblock()
{
#ifdef WIN32
	// setsockopt(m_sock, SO_BROADCAST)
#else
	int flags;
	// fcntl()用来操作文件描述符的一些特性
	if ((flags = fcntl(m_sock, F_GETFL)) == -1)
	{
		return -1;
	}

	if (fcntl(m_sock, F_SETFL, flags | O_NONBLOCK) == -1) {
		return -1;
	}

#endif // WIN32
	return 0;
}


void GameSocket::SetConnected(bool bConned)
{
	m_bConnected = bConned;
}

bool GameSocket::IsConnected()
{
	return m_bConnected;
}

bool GameSocket::IsInited()
{
	return m_bInited;
}

bool GameSocket::IsCreated()
{
	return m_bCreated;
}

基本通信

通过上面的类,我们已经可以简单走通socket的数据了。大致要这么写:(我搞下伪代码,你们自己感受感受)
#define BREAK_IF(cond) if (cond) break

/* 初始化socket对象 */
GameSocket sock;

do
{
	/* 针对WIN32 */
	bool bRet = GameSocket::IsInited();
	BREAK_IF(!bRet);

	/* 创建socket对象,表示使用TCP/IP协议,传递的是流数据 */
	bRet = sock.Create(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	BREAK_IF(!bRet);

	/* 连接到服务器,假设IP地址是192.1681.1.100,监听端口是9001 */
	bRet = sock.Connect("192.168.1.100", 9001);
	BREAK_IF(!bRet);
	
	/* 发送数据到服务器 */
	const char* pszData = "Yo! Server!";
	int nLen = sock.Send(pszData, strlen(pszData), 0);
	BREAK_IF(nLen < 0);

	/* 接收等待接收服务器的数据 */
	char szRecvBuff[1024] = {0};
	nLen = sock.Recv(szRecvBuff, sizeof(szRecvBuff));
	BREAK_IF(nLen <= 0);

	/* 清理工作,在sock对象析构的时候会执行 */

} while(0);
这个明显只是为了使用而使用,如果类似这个代码都运行不过的话,就有三种可能,1. 我伪代码有问题,2. 你代码写错了,3.拿伪代码去编译了。对于最后一种我无力吐槽,前两种的话,您再推敲推敲。
看到这里,会socket客户端代码编写的人估计已经快受不了了。是的,我要很负责任地说,这个代码即使对了也没用的。那就看看下面的章节吧,如何写一个客户端基本的交互模型。

客户端基本的交互模型

什么是客户端基本的交互模型呢?额,其实只要是一个能满足数据发送,数据接收,并且不影响UI响应的模型罢了。实现的方法肯定不只一种,不过最容易让人联想到的就是引入多线程。本来对于游戏客户端来说,至少是多线程的,比如,UI线程和声音媒体播放线程,两个其实是独立时间轴的(本来操作的就是两个硬件设备,一个显卡和屏幕,一个是声卡和喇叭)。单线程处理网络数据收发,是有很大瓶颈的。

单线程的瓶颈

其实很难想象,一个单线程的程序(假设游戏没有声音吧,就一个UI主线程那里跑),在做网络数据发送时是一个什么狗血的样子,socket的send、recv,甚至connect都是阻塞的,当某一个函数未返回时,界面就卡主了,多狗血。比如connect卡个30秒,send卡个5秒,recv卡个50秒,还让不让人愉快地玩耍了?!况且,还有数据发送前的业务处理和数据收到后的业务处理,不过相对而言,这些业务处理的时间铁定不会比网络卡顿要过分,但好歹也是耗时啊。
哦,好累,我发现让我去写这种说教性的东西好累,尼玛,反正单线程不行!

引入多线程处理

来吧,搞个多线程吧,我还是写说下思路。
首先,发送数据一般是客户端的主动行为,比如,当用户输入一段文字,想要在聊天中展示给别人时。而接受数据一般是被动的,或者说,是异步的(呃,同步异步这个东西其实也是困扰了我很久,因为很容易误解它描述的范围)。而上面提到的阻塞是指,当connect函数调用时,直到服务器被连接上,或者没连上超时返回了,这段时间调用的县城会一直等这个函数的返回,如同你定义一个函数,内部仅仅是做了10万次循环浪费个CPU,直到循环全部结束,函数才会返回,这个函数之后的代码才会得到执行一样。阻塞其实是我们编码一直在用的默认模式。而非阻塞指的是函数会马上返回,不会等任务被处理完之后再执行后面的代码,这个其实是需要在设计程序架构上要做的工作,相当于是增加一个任务管理调度的模块,使得在添加任务时不必受到任务是否执行完成的困扰。非阻塞模式和异步模式常常结合在一起,因为非阻塞模式往往需要异步回调的通知。而阻塞和同步模式能独立解决的问题比较单一。
总之,同步异步是指业务上的流程组合形式,而阻塞非阻塞指的是函数完成某项操作时的表现——是否立即返回(立即返回并不是说已经马上处理完成了,可能会后面某个时刻再处理,好吧,我觉得我没有解释好 = =!)。
函数让我们先来了解下同步和异步的模式。

请求应答的同步模式

首先我们要确定这个同步和异步的适用范围,这里单指请求和应答的模式。而这个模式是由业务产生的。
比如,约定好,如果客户端连上服务器后,服务器先等待,客户端先发送一个问题,比如请求服务器当前的时间,然后服务器接收到这个请求后,根据请求分类判断是取当前服务器时间的请求,然后把当前的服务器时间发送给客户端,客户端收到这个请求后隔1秒再次发送请求时间的请求到服务器,服务器再次同样处理。那这个一问一答的模式,就是同步模式。
相当于,你明确知道下一步你要作什么,请求和应答的顺序是被规定好的,整个流程的开始和结束斗有着固定的顺序。同步模式被用来处理一些固定的业务流程。比如登陆的时候校验用户名和密码,校验通过后获取相关个人信息。

请求应答的异步模式

所谓异步,就是不需要遵循一问一答的顺序,甚至可以是问问答答的模式,甚至问1问2答2答1或问1问2答1答2也可以。明显,这种模式会比较适合并接近真实世界的处理模式,A先向B发一个邮件问晚上是否有空,然后再向C发送一个电子邮件询问第二天是否去健身,无论是C先回复还是B先回复都不应该造成A的困扰。(除非当晚A和B吃饭会引起C的不满导致第二天健身活动取消,哈哈哈,开个玩笑)并不会发生一定要A等B回复了才可以发邮件问C的情况。

数据接收

很不幸,如果是单线程处理网络连接,你相对使用同步阻塞的模式来实现你的程序会比较简单。坑爹的是你开始使用recv(int socket, void *buff, size_t len, int flags)函数接收服务器的数据时,你并不知道服务器是否会发送给你数据,并且也不知道啥时候会发,所以你只能死等,可能到了超时时间都没返回都说不定。更有甚者,客户端需要等待服务器不定时的推送数据进行处理。那这个数据接收真变成了烫手山芋,不知道放哪里好了。
最简单的处理方式就是多线程了,直接开个子线程专门处理数据接收,这样就不会阻塞其他操作了。

数据操作的限制

操作UI的函数必须是主线程调用,否则会引起数据混乱,程序崩溃。
为了处理数据粘包的问题,数组最好做个简单的封包处理,比如,定义一个结构体,包含两个数据,前四个字节表示整个数据包的大小,后续是真实数据。在接收数据的时候,可以先判断当前一个业务包是否收全,如果没有收全数据则继续接收,如果收全了就解包,然后主线程处理业务包,该干啥干啥。
还需要注意一些整形变量

简单模型

了解了以上的一些知识点后,可以用伪代码写一个大致的流程如下:

一个可用的交互模型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值