业务逻辑服务器里主要包括以下四个模块
- 计时服务器
- 资源服务器
- 其他逻辑服务
- 对外的WCF接口模块/Socket接口模块
1. 计时服务器
计时服务器的作用是给需要长耗时的功能提供一个延时管理模块,比较典型的应用如“种菜”的计时,武将升级的计时,科技升级的计时,建筑升级的计时等。计时服务器主要由四个元素组成:
- 用于保存计时队列的数据表
- 添加计时的函数接口
- 删除计时的函数接口
- 用于加速的函数接口
- 定时机制
- 可以动态扩展的计时过期处理程序
用于保存计时队列的数据表
先来说一下存储结构,计时的存储大体上有两种方案:1.基于内存;2.基于数据库。首先要说明的,从经验角度出发,本人更青睐于第二个方案。下面对这两个存储方案进行评比:
基于内存的优点:
操作速度快
基于内存的缺点:
如果服务器停电,所有用户的队列数据将全部消失!(客服的灾难)
基于数据库的优点:
不怕服务器掉电,只要服务程序启动,就可以处理过期的队列
基于数据库的缺点:
读取数据的速度较慢,
基于上面的比较,以及我的基本原则--“不让客服的电话被打爆!”,得到的结论就是:使用数据库来存储计时队列。计时队列的表结构如下图所示:
我们可以看到,计时队列的存储,是由两张表来实现的,一张主表(PrimaryQueue),一张辅表(SecondaryQueue)。
每一次添加一个新计时队列的时候,我们要先在PrimaryQueue中添加一条记录,在该记录里要包括,这个队列什么时候开始,多长时间结束,以及非常重要的队列类型名。读者会对两件事儿产生疑问:1.为什么记录的是时长,而不是结束时间。答案是:为了方便计时的加速操作,为了方便界面上的倒计时显示。2.队列类型名(QueueTypeName)字段是干什么的。答案是:每一个队列都要有一个标记,该标记通过字符串形式告诉应用程序,当该队列过期后,应该找哪个函数,或者哪个类来处理相关数据。
仅仅在PrimaryQueue中添加一条记录是不够的,因为我们还不知道当这个队列过期后,有哪些要处理的数据是相关的,为此我们还要在Secondary表中添加对应的记录,PrimaryQueueID是外部键,当然在SecondaryQueue中最重要的是ForeignID字段,该字段存储的是相关逻辑表中,和本队列有关的那条记录的ID,根据此ForeignID,我们可以在对应的计时过期处理程序中对相关记录做处理(这儿说的很别扭,当然也不太好表达,请读者注意理解!)。
呵呵,看到这里,很多读者又会产生一个疑问,为什么要两张表,上面说的那点儿事儿,一个表不就搞定了吗?答案是:一个表是搞不定的,因为在很多情况下,在一个队列里,可能处理的是多个元素的计时,这个时候可能会出现的情况是,在PrimaryQueue中有一个记录,表示是一条队列,并且这个队列中的数据要作的事儿都一样,开始时间也一样,过期时间也一样,在SecondaryQueue里对应的是本次队列的多个计时元素。举例说明,在用户的一次操作中要为该基地建造5只船,因为每只船都有自己独立的状态,所以每只船的数据都存储在基地表和船只列表的关系表中(每只船一条记录),此时,我们的队列表中的数据是:有一条记录在PrimaryQueue中,记录着这些操作的开始时间以及时长,另外,对应主表中这个新的队列,在SecondaryQueue表中会存在5条子记录,每个记录的ForeignID字段的值就是每一只船的ID(基地表和船只列表的关系表的ID)。现在应该明白使用两个表的原因了吧!
添加计时的函数接口以及删除计时函数接口
这两个功能相对来讲是比较简单的,无非就是将数据添加到数据表中,以及从数据表中删除。
用于加速的函数接口
加速的函数接口,主要实现两点功能:1.直接将加速的时间从TimeLengthSeconds字段上减下去;2.计费作用,因为一般的游戏中,加速是要收费的,这时候,在加速之前就要检测用户费用是否充足,在加速完毕后,将相应的费用扣除到。
定时机制
我们如何才能知道哪些数据已经过期了呢?这时我们需要利用Thread编写一个Timer,这个Timer每间隔一定时长就Sleep一次(比如60秒),每一次Timer启动时,就去找出已经过期的队列(now-StartTime>TimeLength),并将其中的记录按QueueTypeName字段的值进行分类(即:功能相同的计时放到一组),然后将每一类记录的集合都提交到ThreadPool中进行异步处理,基本代码如下所示:
var
queueTable
=
Monitor.GetExpiredQueuesTable();
var
groups =
queueTable.AsEnumerable().GroupBy(row
=> row["QueueTypeName"]);
foreach
(var
item
in
groups)
{
ThreadPool.QueueUserWorkItem(obj
=>
{
IGrouping<object,
DataRow>
items = (IGrouping<object,
DataRow>)obj;
foreach
(var
row
in
items)
{
//deal with the row
}
},item);
}
可以动态扩展的计时过期处理程序
前面我们已经将过期的队列取出来,并对其进行了分类,那么我们如何对每一类队列中的记录进行处理呢?另外,随着应用程序规模的不断扩大,处理程序的数量也不会断增加,那么,我们又应当如何保证处理程序的可扩展性呢?
其实,我们可以很容易发现,不同的处理程序中的函数的名称和参数列表应该是一样的,只不过是该函数所处的文件或者类不同,并且逻辑不同而已。大家会问题,为什么不同的处理程序的函数名和参数列表也要相同呢?其实想法也很简单,因为我们需要通过一个统一的调用接口实现函数调用过程。
为了达到可扩展并且方便修改的的目,我的处理程序可以利用IronPython脚本来实现,使用脚本语言来实现一些逻辑是游戏编程中常常用到的(当然用C#写成DLL,并动态加载也可以)。我们要做的是,在应用程序所在的目录中添加一个处理程序的脚本目录,在这个脚本目录中添加若干个IronPython脚本,每个脚本的文件名应该对应着每一类队列的分类名(QueueTypeName),在这些脚本中都提供一个名称相同,而逻辑不同的函数,在队列服务程序加载的时候,可以建立一个Dictionary,应用程序可以去扫描脚本目录,并将脚本名以及脚本中的函数以键值对的形式添加到Dictionary中。发现已过期的队列后,就可以根据队列所处的分类名,在Dictionary中查找相应的函数并调用即可。
使用脚本的好处:语法简单;可以直接修改脚本文件并保存,不用重新编译;处理程序经常会出现Bug,并且处理程序的需求会经常发生改变,写成脚本方便修改;当添加了新的处理程序时,也不用重新编译服务器代码,只要重新Load一次脚本就可以实现计时服务的可扩展性了。
使用脚本语言的速度如何?速度肯定会慢一点点,但是你绝对感觉不出来!
总结,根据上面的描述,我们可以得到一个流程图,如下所示:
|