今天找到了贵论坛,发现坛主的很多想法和本人不谋而合,本人近1年主要精力都致力于开发一个大型多人在线游戏的基本架构和相关的技术模组。而我欣喜的发现我与坛主的研究方向正好相反:我是先从服务器端开始研究入手的,目前服务器端告一段落,正准备开始客户端的研发,在寻找客户端引擎的时候碰巧找到了这里。
我看到坛主的这个板块,了解到Orz正需要一些服务器方面的资料,在此我先奉上个人的服务器端的一些成果,希望能有所帮助。
(一)自己开发的一个基于boost::asio的网络引擎
首先这个网络引擎是基于boost::asio的一个封装,网络部分功能非常底层,API只有基本的listen、connect、send、kick等(均为异步,目前只实现了TCP协议),而其他方面提供的是基于mysql的db接口和log接口,还有一个自己开发的对象池,用于使用FreeList的概念来事先分配内存,降低运行时期内存的分配时间;
另外就是开发了一个多线程下的数据结构,一个线程安全的map,这个map可以让无限个线程同时读和写(包括添加元素、删除元素、修改元素)而无需任何因为互斥锁定带来的线程等待等开销。即是说1000个线程和1个线程操作这个map的效率是相同的。
发布形式:win32(64位未测试,但是开发考虑了相关的定制,例如指针和long在64位下从4字节提高到8字节,引擎底层做了数据类型的typedef)下 dll+lib+include;linux(Redhat、CentOS5.x,gcc3.4以上,需要安装boost1.37和mysql5.0)so+include;source code,yes,of course!
网络部分的基本结构是这样的:
#1 io部分设计。一个线程池负责处理io,这个实际上就是一组boost::asio::io_service::run,每个boost::asio::io_service下有一组私有线程,负责处理异步io事件,这里,boost::asio::io_service得数量和其下私有线程的数量是可以根据配置文件自由设置的,如果你了解boost::asio,那么一定知道它推荐一个cpu对应一个boost::asio::io_service对象(或者一个boost::asio::io_service,但是每个boost::asio::io_service下的私有线程对应每个cpu),这样在多处理器(或者多核处理器)下效率可以达到最高;
#2 complete handler设计。另一个线程池负责处理封装好的complete handler,即对应io事件的用户定义的逻辑处理,例如io recv事件,对应一个用户实现邦定的(使用boost::bind和boost::function)handler来处理当接受到socket消息的时候调用对应的handler(函数、仿函数对象、成员函数等)。#1和#2中的线程池之间使用一组线程安全的队列来传递消息(传递使用直接的值拷贝,不使用动态内存,因为动态内存的申请和释放太消耗时间,即便使用内存池也一样。1k以下的值拷贝的时间损耗都远远小于对应的动态内存申请的时间;另外使用值拷贝也有线程安全的考虑);
#3 封包的设计。head+body,head中有固定4字节的body长度信息(int32)和4字节的封包类型信息(int32),如果愿意,可以修改代码进行扩展(packet部分是独立于引擎的模块,也是一组dll,lib,include或者so,include),接受和发送由于是tcp,所以按照head中的body长度来控制一个封包的完整性。
#4 多线程模型。boss-worker模型,主要用于广播消息、查找、和db、log的实现上;生产者、消费者模型,主要用于#1和#2 的两个线程池之间的事件传递(io线程池产生completehandler,用户的线程池负责处理、消费)
#5 session的设计。一个session就是一个成功连接进来的客户端socket代理,出于线程安全和效率的考虑,session的存储容器不使用任何stl和boost的容器,而是使用——C的数组(当然不是静态数组,而是:new char[n]这样的),来实现。而且是二维数组,这样配合对象池(指与先分配好一组“空”的session),我们无需任何互斥变量就能够毫无阻碍的访问和修改数组中的每个元素(session),并发性能达到最大。至于二维数组的设计,第二维的值对应io线程池和用户线程池中的线程数量,即一个线程唯一邦定一组session,这是为了分配session id时候效率和安全的考虑。(例如id 0~9对应一组session,10~19是第二组,而每组id和session都唯一绑定一个线程)
#6 线程之间数据传递的设计。线程安全的queue,使用了boost::thread::locks中的mutex、shared_mutex、condition_variable_any等概念,当queue空闲时,使用条件变量等待特定的事件(例如新的元素push进来,或者程序退出等);
#7 异步下session的唯一性。由于整体是异步的,所以不可避免的会出现当一个session的某个处理还未结束的时候,这个session已经消失了,甚至换了一个新的session(指id的分配),那么这个时候如果没有任何防范处理,之前的那个未完成的处理很可能会作用到这个新的session上,就不可避免的出现一些错误和未知情况,我们如何防范呢?使用valid_code,设计一个64位的无符号整型变量,给每个session按照事先给定的session总量(这个引擎使用预先分配内存方法,所以开始前必须手动指定session的最大数量——通过配置文件),分配一个唯一的数据段(例如1~10000000,10000001~2000000等),每次这个session发现有新连接进来的socket使用了自己,则将自己的valid_code +1,当加到最大值的时候(例如刚才举例的10000000等),自动变为最小值(例如刚才的1等)。每个session的valid_code在引擎的初始化阶段是随机生成的(在事先指定好的数据段中随机)。再加上每个session的数据段时唯一的,所以不会产生重复的valid_code,这样鉴别某个时间段内唯一的session就成了可能;
#8 agent基类的设计。这个其实就是封装了boost::thread类,即“带私有线程的类”,主要用于作为一些相对独立的工作,例如log记录、db访问处理、定期ping某个ip等,引擎中的logger和db接口都是继承自它;
#9 db接口设计。一个数据库对应一个database对象,每个database对象拥有一个自己私有的线程池,其中线程的数量可以根据配置文件自由设定(例如和mysql的连接数量等,处理query的线程数量等)
#10 一些零碎。BIG_ENDIAN问题,封包内部自动进行了处理,无须用户单独设置;跨平台以及不同编译器的预编译设置,以及不同cpu的针对处理(x86,powerpc等)
目前的不足之处:
#1 并未开发完全。udp没有实现封装,当然boost::asio完全支持。logger目前只支持printf功能,对于写file和传递到专门的log服务器方面只留下了接口;封包加密以及安全方面是一个空白,目前的版本并未添加。
#2 面向底层,并不提供高层功能,所以很多开发都需要自己进行;(对,这并不是一个“网游引擎”)
#3 性能方面并未经过大量测试,由于本人工作较忙,这些都是业余时间搞得,所以只是初步测了一下连接并发部分:使用某个不知道配置的笔记本测得3秒并发连接1500。再多的并发连接并没有尝试过。
PS 由于本人工作较忙,故只能提供源代码(只提供windows版,便于查看,linux可以直接使用源代码编译,gcc3.4以上boost1.37即可),文档方面一直没有时间整理,这篇文章都是中午抽空写的(零零散散修改到现在),所以暂时就写这么多把。
PS2 提供的源代码是vs2005sln,只包含source code、配置和工程文件。
PS3 我看到贵论坛在研究RakNet,据我的一个前同事说,他认为RakNet并不好,网络底层用的是select,而且不是异步,代码质量不高,建议我不要使用它的网络层。我感觉RakNet的一些高层功能还是可以参考的,例如安全加密、大厅分流等,至于网络底层,我建议还是用boost::asio,跨平台、性能和扩展性都很优秀,而且被C++标准委员会所支持,很被看好。
作者:Nouness