基于skynet引擎的回合制游戏搭建
这里我们介绍的是一种单服的游戏服务器结构,即我们在一台物理机上只运行一个服务器进程,所有的游戏内容都运行在此进程中
影响服务器承载玩家上限的点主要有这么几点
1.机器性能:主要在机器的主频,内存,cpu缓存上
2.网络问题:带宽,IO读写
3.游戏代码结构设计:单进程单线程的设计,所有的事务都需要等待上一条事务执行完毕才能开始处理
4.游戏逻辑代码实现:游戏逻辑代码实现中需要注意编程语言的性能问题,以及一些玩法开发的耗时注意事项
基于skynet的开发,我们将我们的游戏结构设计成一个单进程多线程的结构,对于比较耗时的服务,如场景,战斗,
广播等一些功能我们可以另多开几条线程<lua虚拟机>去专门做这些处理,分散线程的压力。在此份代码中,可以看
到在service下回出现war/scene/broadcast 等目录;这就是针对上述说表的具体实现
game_dev -
| - shell/ --存放常用的shell指令,如启动、关闭服务器等
| - log/ --游戏日志输出
| - service/ --游戏逻辑玩法服务
| - config/ --游戏服务器基本配置信息
| - skynet/ --skynet引擎源码以及执行文件
a.检出一份skynet源代码,地址:https://github.com/cloudwu/skynet.git
b.注意:skynet版本我只检出至c91efa513435e71f24fd869e15ef409e0caf6c86,往后有大修改,部分代码无法支持
c.编译skynet代码,期间会遇到一些库缺失的问题,安装完后编译即可
编译完成后,在skynet下我们会看到一份skynet/skynet的执行文件,skynet的启动流程可以通过
追踪skynet-src/skynet_main.c进行了解,在这里不在做具体的阐述。游戏的启动脚本我把他放
在了shell/gs_run.sh脚本下,通过执行该脚本我们可以将游戏服务器运行起来,当然你得先配置
好config/gs_config.lua文件,通过配置我们可以知道最终的启动工具是通过snlua bootstrap启
动bootstrap,然后通过bootstrap启动gs_launcher,gs_launcher.lua去启动游戏逻辑中需要的
各项服务内容
游戏后台在这套框架中称之为dictator; dictator主要实现的是后台指令,区别游戏中的gm
指令,dictator主要用来实现在线更新,是否开放玩家登陆,关闭存盘等相关指令;每个服务的启动
都会向该服务注册一份各服务的地址信息,dictator做为一个指令的发起方,将指令根据地址传送
到各个服务上;根据这个机制可以实现各个服务代码的在线更新,启动这个服务后,我们会监听
本机器上的dictator_port端口(在config/gs_config.lua中配置),通过nc localhost dictator_port
可以直连上后台,就可以输入相关的指令进行相关操作,如在线更新指令
update_code service/world/dictatorobj
与此同时,我们还启动了debug_console后台,用于监控各个服务的状态,相应代码位于 skynet/service/debug_console.lua下;
参照云风博客:https://blog.codingnow.com/2008/03/hot_update.html
在这个系统中,载入一个文件我们使用了两种方式,一个是系统自带的require,另外一个是
自己实现的import;代码位于lualib/base/reload.lua下,这种划分相当于把文件按照能否在线更
新作为了标准;使用require的,我们默认认为此文件不能进行在线更新,使用import则是可以使用
lualib/base/reload.lua 下的reload文件进行在线更新
具体原理待阐述
在原生lua环境下,我们引用一个文件通常使用require,使用require后我们会把内容放置到package.loaded
下,如果我们要更新这个文件,首先我们会把package.loaded置控,然后重新require一次,但是这
种写法会使得一些地方无法更新到 如:local mod = require "mod"; 就算你重新require了一次,
这里的引用关系保持的还是旧的,除非我们把所有引用关系都遍历更新一次。而这个在线更新机制
在不解除旧有引用关系的前提下对内部数据进行了替换,详细实现参照代码:
lualib/base/reload.lua
在skynet中有一个叫做sharedatad的全局服务, 代码路径:skynet/lualib/sharedata.lua
支持new|query|update 三种方法;
sharedata.new(name, v, ...) 通过此方法可以创建一个新的sharedata数据块,v可以是字符串,table
sharedata.query(name) 通过此方法可以获得已经创建的sharedata数据块,但是我们发现实际美一次
query都会去创建一次新的table,实际上我们可以在上层的使用方法上去规避他
sharedata.update(name, v) 可以将名字为name的sharedata使用v进行一次更新
同时,在我的设想中,这个框架对于sharedata的数据应用应该只停留在读取游戏中的配置数据,一般来 讲只能是导表数据,停留在只读层面上, 我们不会去也不应该去改写sharedata上的数据,有数据需要 需要变动的时候,我们通过在线调用update的方式去更新这些数据
由上述分析可以知道,我们要实现这写功能,需要有一个专门的服务去启动sharedatad和在线更新 sharedata 同时还需要有个文件去做数据加载的功能,需要对query做一层封装,规避重复创建table问题, 把sharedata设置为只读
在这套框架中,我们的数据库采用mongodb;没有特别的原因,只是刚好我在使用这个数据库而已,
想要使用mysql也是可以的。在使用mongodb的时候要注意规避内存OOM的问题,因为mongodb引擎采用的
是内存换效率的策略,如果不加已限制,则会导致机器的内存被完全耗尽。
skyent 封装了一套mongodb的lua接口,位于skyent/lualib/mongo.lua;这个框架的所有存储实现都基于 这个模块去做的实现。
根据skyent的单进程多线程模型,我们对整个游戏启动了一个专门处理数据存储的服务,叫做gamedb 在gamedb中,我们实现了一套适用于游戏存储的一套方法,位于:service/gamedb/gamedb.lua下 其他服务如果有存储的需求,都会通过actor模型,将需要存储的数据发送到gamedb服务下,去做存储 有效的去分担主线程的压力
在这套回合制游戏框架中,我们的存盘策略采用的是相对来说比较简单的策略,目前的想法只涉及到 [定时存盘]:对于容错率相对来说比较高的数据块,我们采用打脏标记,5分钟一次的存盘策略 [及时存盘]:对于容错率低的数据块,则我们采用的是即时存盘,比如玩家的ID分配等一些相关内容 [关联存盘]:对于数据关联性比较大的多个块,我们采用关联存盘,就算回档也会回到同一时间段, 比如玩家之间的交易行为
在[5]中我们提到了要对共享数据进行在线更新,而共享数据也是单独的一个服务,我们怎么做到
在dictator后台对sharedatad服务的数据进行更新?在[6]中我们也提到了gamedb用于存储逻辑服务产生
的存盘数据,但是我们怎么将存盘块数据发送到gamedb中进行存储,需要的使用我们又怎么从gamedb中
获取到相关数据。这就是我们需要做跨服务数据交互通讯的理由。
在skynet中,我们每启动一个服务都会有个专门的标识,在log/gs.log中我们可以看到一串的十六进制 字符,这个其实就是每个服务的一个标识符,也可以说是每个服务的地址,我们通过这个地址在整个进 程中找到这个服务,与此同时我们在每启动一个服务的时候也会向skynet注册一个服务的别名。如在启 动gamedb的时候,我们有skynet.register(".gamedb"),只要注册了这个别名,我们也可以通过该别名 去访问相应的服务线程
在interactive中我们定义了另外通信协议PTYPE_LOGIC,专门用于游戏逻辑服务间的通讯,在每个服务 启动的时候我们需要调用interactive.dispatch_logic进行一次初始化
具体的代码实现在lualib/base/interactive.lua下。
CS之间的通讯我们引入protobuf,协议的解析我们采用云风写的pbc
首先我们需要在机器上安装一个protobuf,我采用的是直接检出git安装的,git地址:
git clone https://github.com/google/protobuf.git
在运行./autogen.sh 之前,我们还需要安装一些必要的内容,有unzip,autoconf,automake,libtool
1.执行 ./autogen.sh
2.执行 ./configure 执行过程中可能会遇到一些问题,通常是库缺失,自行安装即可
3.make
4.make check
5.make install
编译pbc的时候采用了一种比较取巧的方式,直接将pbc:https://github.com/cloudwu/pbc.git
下中我们所需要的文件放入到skynet下的pbc文件夹下,有makefile,src,pbc.h,将pbc/binding下
的pbc-lua53放到pbc下,然后通过改写skynet/Makefile文件直接进行编译导入,最终,我们的使用方
式和pbc/binding下介绍的一致,只是我们将protobuff.lua文件放到了game_dev/lualib/base下,这样
我们就可以直接require "base.protobuf" 使用它