ACE开发游戏服务器一

 

《最冷清的课堂》这个帖子是我写作这个系列的源动力,希望它可以激励我完成我最初的计划。
我不想从很深的理论开始这样的一个话题,毕竟对初学者来说,一个可以运行的程序,比一个看上去很美的UML图更加的亲切。
这个系列是想介绍如何使用ACE这样一个工业级的framework构建一个堪用的游戏服务器程序。无论多么神秘,游戏服务器程序也是一个网络程序,最基本的开始是一个可以运行的网络服务器程序。首先,我们需要了解以下的几个事实:
l         绝大多数的游戏服务器是使用TCP协议,动作类游戏除外。
使用TCP协议是保证这样数据传输的可靠性,网络游戏在客户机和服务器之间同步着一个或若干个相当精确的状态机,可靠的数据传输是这一要求的保证。
l         所有的服务器程序需要设计自己的数据包格式。
使用TCP协议就会带来所谓“粘包”现象,因为当你的若干个数据被发送到网络后,会被其间的若干设备,分割,组合,再分割,再组合……,当你的对端在接收时没有找出原来的数据包分割位置。所以,你需要标识出数据包分割的信息。
l         绝大多数的游戏服务器追求效率。
尽量多的玩家在线,是网络游戏之所以带来生趣的重要卖点。那应对这一卖点的技术要求就是服务器程序能高效地处理客户端发来的数据。
 
基于第二事实,我们首先设计我了我们的数据包头部的定义
   16    16     16       32       16
+--------+--------+---------+----------------+--------+
 ‘TY’   长度   命令    ID         标志
第一部:是字符’TY’,用以标识我的交互协议簇。
第二部:是一个WORD,用来描述数据包后面的数据体的长度。
第三部:是一个WORD,用来描述数据包所包含的命令ID。
第四部:是一个32位的整型,用来描述数据包的发送者的ID
第五部:这个部分包含的内容比较多,通过位操作,可以解释也下面的信息:压缩方式、加密方式,数据编码方式,校验值等。
我设计交互协议的习惯是尽量让数据包可以自说明,虽然每个包会多出来几个字节,但这样的设计解耦了不同终端之间关于数据格式的语法约束。
包头定义出来了,我们再来看由第一、三两个事实所做的选择。ACE中,提供了两种IO模型,ACE_Reactor和ACE_Proactor。前者是同步方式,后者是异步方式。我们重点注意Win32平台上的ACE_Proactor实现,因为,它采用了大家熟知的完成端口实现。

 

 
上面这张图我给出了服务器框架的静态类图,因为整个游戏服务器其实就是一个接收、处理、返回消息的一个总线式的结构,所以,游戏服务器的主体是一个ACE_Task,ACE_Task是ACE里面非常常用的一个类,它里面主要包含了两个部件:
l         一个消息队列ACE_MessageBlock_Queue
l         一个线程池ACE_ThreadManager
为什么要在ACE_Task与CProactorServer之间加入CNetServer这一个接口层次,因为,在不同的平台上,可能要替换服务器的IO模型,比如在Linux平台上,我可能使用select方式。其实CProactorServer层次仅仅实现了数据包的接收入发送,而对数据处理是完全交给它的父类的消息总线中进行处理的。所以,在这里加入一个CNetServer层次,可以无须考虑网络IO模型,但关注于游戏逻辑的实现,吼吼,模式模式!!!
Code:
  1. void CProactorNetHandler::handle_read_stream(const ACE_Asynch_Read_Stream::Result &result)   
  2. {   
  3.     m_nReadCount--;    
  4.   
  5.     if (!result.success() || result.bytes_transferred()==0)   
  6.         //读出错或连接断开   
  7.     {   
  8.         disconnect();   
  9.         if (test_delete_self())   
  10.             return;   
  11.         return;   
  12.     }   
  13.   
  14.     else if (result.bytes_transferred() < result.bytes_to_read())   
  15.         //碎片包,接收余下部分   
  16.     {   
  17.         read_packet_body(result.bytes_to_read()-result.bytes_transferred());   
  18.     }   
  19.     else if (m_pReaderCache->length() == PACKET_HEADER_SIZE)   
  20.         //接收完成的数据包头   
  21.     {   
  22.         LPTPACKET_HEADER pHeader = (LPTPACKET_HEADER)(m_pReaderCache->base());   
  23.         if ((pHeader->m_wPacketHead != 0x5342)||(pHeader->m_wSize > BUFFERSIZE))   
  24.         {   
  25.             ACE_DEBUG((MY_LOG_ERROR "CProactorNetHandler:receive unknown format packet,kick socket %d",handle()));   
  26.             disconnect();   
  27.             return;   
  28.         }   
  29.   
  30.         if (pHeader->m_wSize > 0)   
  31.         {   
  32.             read_packet_body(pHeader->m_wSize);   
  33.             return;   
  34.         }   
  35.   
  36.         m_pServer->putq(m_pReaderCache);   
  37.   
  38.         read_packet_head();   
  39.     }   
  40.     else if (result.bytes_to_read() == result.bytes_to_read())   
  41.     {   
  42.         m_pServer->putq(m_pReaderCache);   
  43.   
  44.         read_packet_head();   
  45.     }   
  46.     return;   
  47. }   
  48.   
在解释上面的代码之前,我想先多说两句完成端口异步读写的问题。
l         所谓异步读操作,就是当一个远端的socket连接时,向系统微进程投递一个读操作,然后立即返回。这样当这个socket上有数据时,系统微进程会将结果插入到事件队列里面,你的程序轮循时,就会处理到这个返回的数据,当然ACE中连轮循的工作都不需要你做了,它会自动分离异步的事件,你只需要在handle_read_stream中处理各种情况了。
l         所谓异步写操作,就是当你有数据要发送时,将你的数据投递给系统微进程,由它去发送,它会将发送的结果也放到事件队列中。
不过,貌似很完美的方式,却有着种种的陷阱,最常见的有如下的两种:
l         你需要关闭这个socket时,肯定还有一个异步读取事件投递在微进程是没有收回,所以,你需要调用CanelIO来结束一个异步的操作,系统接收到这样的请求后,会回调一个数据量为0的异步读回调;但当对端主动关闭连接时,你同样也会收到一个数据量为0的异步读回调,所以此时你需要区分这样的操作,以在适合的时候清除这个事件处理器。这样的问题同样存在于异步写操作。所以,最佳实践是在发起读和写操作时,都进行计数;在读或写操作回调时清除计数。
l         在多线程中,如果发起了超过了1个异步写操作,那么在就有可能在不同一的事件处理线程中来处理这两个异步写操作的结果。最糟糕的情况是它回调的顺序和你投递时的顺序截然不同,那么对端收到的数据将是被打乱的。针对这种情况,要保证同一时间只能投递一个写操作。那么在多线程的情形下,需要在事件句柄里建立一个发送消息队列,所有的外界的写操作实践上都只是将消息入队。然后由写操作的回调来读取消息队列的头部来做真正的发送工作。
上面的这段代码是CProactorNetHandler的接收数据回调事件。代码不复杂,先接收头,再接收数据体。请注意m_nReadCount这个变量,这就是清除读操作的计数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值