diaox框架概述

前言

设计一款轻量级的游戏服务器框架diaox,需要考虑的点有:

  1. 网络IO性能
  2. 游戏逻辑处理的性能
  3. 访问数据库mysql, memcached的性能
  4. 如何保证游戏开发设计足够简洁
  5. 单进程下,支持的同时在线人数上限

1. 网络IO性能

linux内核已经支持REUSE_PORT socket选项,假定我们有N个CPU核,这样可以创建N个socket共同监听一个端口,创建N个线程分别对N个socket进行epoll_wait并处理,这是能想到的单进程高并发的IO方案,worker thread的主循环看起来是这样子的:

void _worker(void *p) {
	epoll_event events[1024];
	int efd, lfd, reuse=1, i;
	
	efd = epoll_create(1024);
	lfd = socket(...);
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&reuse, sizeof(int));
	epoll_ctl(efd, ADD, lfd);
	bind(lfd, addrinfo);
	
	while (1) {
		int n = epoll_wait(efd,  events, 1024);
		for (i=0;i<n;++i) {
			...
		}
	}
}

这里有个问题,recv收到的网络包如何传送给逻辑模块进行处理,逻辑模块使用一个线程还是多个线程,如果采用单线程,那在接收数据的时候,就转变成 多生产者(网络IO线程)-> 单消费者(逻辑线程)模型,这样的实现简单,但单线程的逻辑处理无法充分发挥多核的优势,请接着看下一节。

2. 游戏逻辑处理

如果需要游戏的逻辑处理充分发挥多核优势,那游戏的逻辑就必须分布在多线程里面执行,具体以什么单位来进行多线程的分布,根据不同的游戏,有不同的方式:

  1. 针对SLG,MOBA,卡牌类游戏
    这类游戏一般而言,都以一个房间、或者战场为单位把玩家聚集在一起,而且一个房间或战场的玩家数量也不多。那么,可以房间、战场为单位将游戏逻辑分布到不同的线程里面去执行。

  2. 大型的MMO游戏
    玩家必须在一个场景地图里面进行互动,PK,这样的游戏逻辑单位就是场景。那么,就以游戏具体场景地图为单位进行划分。

场景、房间的创建由主线程来进行维护,主线程使用一个基本的load balance策略,以免出现有些线程忙,有些线程又很闲的状况。

那么,如何实施呢,首先面临两种选择:

  1. 在网络IO线程里面,直接处理解包之后的游戏逻辑
  2. 重新创建N个线程,专门用于处理游戏逻辑,网络IO变成多生产者->多消费者模式

对于第1个case,如果把游戏逻辑直接放到线程的网络IO处理之后,那么,游戏逻辑的执行将会是按照客户端连接的socket来进行分布的。一旦client socke被哪个线程accept了,那么这个客户端的逻辑将永远在此线程中运行,想象一下,一个战场里面有10个玩家,这10个玩家的数据来自于10个不同的线程,处理起来是有多蛋疼。我们的预期设想是,这10个玩家的数据全部在一个线程里面,战场的逻辑也在当前的线程里面,这样根本就不用加锁,逻辑写起来更简单。

第二个case,网络IO和逻辑处理是完全分离的,针对多生产者->多消费者模式,如果直接使用一个全局的消息队列,N个生产者同时发送数据到一个队列,然后N个消费者从这个队列里面取数据,如图:

这样的并发处理,可以使用一个lock-free的队列来实现:细节可以参考codeproject-lockfree实现或者酷壳作者对lock-free的介绍 ,但是,使用这种方式,仍然解决不了“我们需要同一战场内的玩家在一个线程里面处理”这个问题。

为了解决这个问题,假定,我们在网络线程读取数据的时候,可以分析包头,而且可以通过socket的fd直接访问fd所对应的客户端对应哪个战场id,那么通过战场id可以找到threadid,通过threadid 找到对应线程的消息队列进行发送。
那么,对单个逻辑线程A来说,网络线程1,2,3,4均可以传送网络消息到它的接收队列中,这样,多个生产者对应一个消费者的队列,可以使用一个ring buffer来实现。

因为战场的管理是在主线程中进行的,所以主线程管理着client_fd<=>battle_id<=>thread_id之间的对应关系,而网络线程对这个数据结构进行只读的访问。

登录逻辑也直接放到主线程里面来执行,步骤如下:

  1. 网络线程收到登录消息,发到主线程消息队列里面(并通过event_fd唤醒主线程)
  2. 主线程收到消息进行处理
  3. 异步读缓存db
  4. 读缓存db返回,通过账号信息对登录进行验证,不合法则终止fd
  5. 验证合法,并通知登录成功,同时将读取db返回的一部分数据回传给客户端

账号快照信息,与玩家的战场、具体玩法相关数据,需分开存储到不同的mysql table中,考虑到账号信息变更不频繁,而具体玩法数据的变更非常频繁,因此分开存储是很有必要的。

考虑到有不少SLG,卡牌类游戏都有大厅类的玩法,大厅功能也在主线程实现,包括大厅的AD,聊天等等。
玩家登录成功,主线程即可创建一个player object与之对应,但具体玩法数据暂不加载,只有在大厅匹配成功,需要将玩家放入战场中时,才需向db请求具体玩法数据。同一个player object,在同一个时刻内,只允许一个战场线程和主线程同时读写访问,其它战场线程只可读,不可写。

玩家退出游戏时,这个player object由主线程负责删除。

3. 数据库的性能

3.1 cache

cache的存在是为了解决内存不足时引入的,例如虚拟内存使用的cache页面hit算法,在同时在线人数不多的情况下,所有数据可以全部放入内存中进行处理,完全不需要使用cache。
还有一种可能需要使用cache的地方,在玩家登陆游戏进行验证的时候,如果每次都读mysql查表,这样的性能也很低,这里倒可以使用cache,设预期的最大在线人数为mol,可以设定cache的大小为mol*10,cache只保存玩家账号,密码,和登陆token信息,以进行快速的认证,cache miss才需要读取mysql返回。

3.2 mysql

玩家的数据什么时候需要写库,嗯 这是个问题,最乐观的实现方式,当玩家上线时读库,下线时写库。读写库,均采用异步方式进行,完成后以回调的方式通知逻辑线程进行处理。

暂时没打算支持mysql的事务处理。

4. 让开发足够简单

先来看看游戏开发中,常有的需求:

  1. 各种活动系统
  2. 任务和成就系统
  3. 一些新的战场和玩法

这几种需求,对于游戏策划来说,可能会经常改动,在版本更新中也更频繁,可以使用脚本来完成,比如lua.
游戏的核心玩法仍使用c++实现,导出主要的接口供lua使用。
哪些是核心功能呢,如果是MMO游戏,其核心功能有:

  1. 角色移动
  2. 技能判定,伤害计算
  3. 移动和技能的网络同步,以及优化
  4. 视野管理
  5. 多人PK时的网络同步优化

如果是一款SLG游戏,其核心功能有:

  1. 英雄设定
  2. 技能判定,伤害计算
  3. 游戏回合逻辑

这些主要的功能,一旦写好了,以后改动的可能性很小,后续迭代维护时,更多的只是脚本层面的外围功能开发。

5. 同时在线人数

这里暂时不考虑分表分库的策略对海量的非活跃用户表进行优化,这里仅考虑,采用目前单进程-多线程的方案下,如何最大限度地承载更多的在线人数。

5.1 内存

采用动态的数据结构存储玩家数据,假定平均每个玩家身上的数据为321024bytes=32k,那么一个16G内存的服务器,可以承载的人数为 161024*1024/32 = 0.5M. 对目前市面上的任意一款游戏来说,同时在线达0.1M,数据就相当不错了。
所以,很明显,内存不是主要的瓶颈。

5.2 带宽

如果只是卡牌,SLG类游戏,在一个战场里面,玩家与玩家之间的广播量并没多大,但如果是MMO游戏,400人 pk 400人,我们可以大致估算一下,假定同屏玩家数量最多为200,则一个玩家pk释放技能,移动的消息需要同步给同屏内的所有玩家。

MMO大都采用状态同步,假设玩家一秒发送5条指令,包括有点击移动,选择某个人,释放技能,平均1秒内是可以完成的,那1秒钟5个包,每个包按64bytes计算,1秒钟,每个玩家的上行速度是645=320bytes/s。在800人的pk场景里面,服务端的下行网速是 320bytes/s800=256k/s。关键是服务端上行的带宽计算,也就是广播量所占用的带宽。

前面假定同屏玩家最多200,那在同屏内,一个玩家的技能释放或者移动,理想情况下,应该广播给同屏所有200个玩家,广播消息主要分以下几种:

  1. 玩家移动同步消息广播
  2. 单体攻击技能
  3. 群攻技能

移动消息同步,1秒3次,每次带上客户端待移动的路径点,服务端需要将玩家移动路径点转发,在200个同屏玩家内,服务器广播的带宽为 643200200 = 7.68Mbytes/s。一个战场内一共有800号玩家,所以7.684=30.720Mbytes/s,800人的战场,光移动同步量就这么大了。

移动同步如何优化呢,高热点的区域,服务器可以将客户端同步的路径点缓存起来,然后一起发送,缓存时间为500ms,这样客户端高频率的移动包,就变成1秒2次了。另外,广播时,那些距离更远的,再移动的时候频率可以更低一点,1秒1次同步就可以了。这样计算下来,同步某个玩家的移动频率平均下来是1.5次/s。这样子800人的战场,移动同步数据达15Mbytes/s。

单体攻击技能频率按1s1次计算下来同步带宽达10Mbytes/s。

群攻或者群疗的计算量比较大,这里主要是CPU负荷重,服务端计算好一次攻击计或治疗的结果后(可能伤害或者治疗就涉及到200人),直接把对同屏所有玩家的伤害一次性打包发给各客户端。假定群攻技能CD为10秒,每秒平均20人放群攻,一个群攻平均打到50人,那伤害包大小约为 5010 = 500bytes。所以群攻带来的带宽大概是 50020200 = 2Mbytes/s。一个战场800人,所以最后是24=8Mbytes/s。

把这三部分算下来,800人的战场,服务端上行带宽一共是15+10+8=23Mbytes/s。

如果单服要承载8000人呢,那就是23*8=184Mbytes/s,注意这个单位还是bytes,换算成bits,就是1.43Gbits/s。

对一般小公司来说,服务端这个上行带宽的压力是很大的。

如果是回合制MMO呢,首先回合制游戏来说,同屏玩家可以限制为100个,而且它对移动同步要求不高,优化一下,玩家移动的时候直接发送从一个屏幕内的路径点到服务端,走完屏幕大概需5秒,5秒同步一个64bytes的包就可以了,那么我们还是按照一个战场800人计算,估算一下:

  1. 移动同步带宽:64/55050*800/100 = 0.25Mbytes/
  2. 打斗时带宽(一个回合战场双方各5人,平均一秒攻击一次):6410800/10=51.2kbytes/s
    打斗所占用的带宽几乎可以忽略不计,按移动同步带宽来算就是0.25*8=2.0Mbits/s。

8000人同时在线,高峰期的带宽也只有20Mbits/s,这对小公司来说完全可以接受。如果能提供100Mbits/s的上行带宽,那单服可以承载32000人同时在线。

5.3 CPU

CPU这个最难估算了,按照配置4核3.2G,全部满负荷的情况下,分摊到32000人身上,每秒运算
4
3.2G/32k = 4*10M = 40M次。
每个玩家1秒内最多允许进行40M次的运算,方能承载32000人的同时在线,而且这种计算方式也很有争议,所以CPU的瓶颈关键得根据实际项目进行估算。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值