首先建立你自己的git仓库,然后执行git pull origin git@42.121.105.8:slg-server.git master
获取最新的代码。
如果slg-server
有改变或者bug修改,同样执行一次。
获取代码: git clone git@42.121.105.8:slg-server.git
获取依赖关系: make deps
生成协议代码: make g
编译: make
初始化数据库: make db
启动: make s
,默认用户名加密码为:root,密码空;如果不是,自行修改slg_server.erl
测试: 在新的终端执行make e
,执行: gt:start()
.
本项目是为小服设计的slg游戏服务器框架,主要包含以下四个组件:
(这四个组件也可以用于组合成分布式的单服).
每一个组件都对应于单独的项目,你可以从config/rebar.config
看出,下面会对每一个模块的使用方法进行详细描述。
slg_proto设计为处理游戏服务器中的tcp连接和协议打包解包,加密解密,玩家的基本进程运行在此模块.
在目录proto/下定义项目需要的包和协议,需要遵守以下规范:
api.txt
定义了服务器中所有的包类型。
_req,_ack,_ntf,_cah
结尾,如果违反make g
时会报错.proto.txt
定义了所有的包内容.
包内容的定义需要按找protocal.txt里的描述使用,有几点注意:
例子:登陆流程
玩家登陆时,客户端发送登陆_req,服务器验证成功后,先发送多个cache包,让客户端cache部分数据,之后发送登陆成功_ack。
error_code.txt
包含了游戏中可能出现的错误码,需要以下规范:
player:code_ack
回复。player:send
给客户端发送各种包。修改proto后执行make g && make
可生成可编译新的协议处理版本。
player.erl
模块有以下几个回调函数:
quit/2:
进程退出,可以视为玩家退出登陆的事件点。
cast/2
处理异步消息,gen_server:cast即可。
info/2
处理info,gen_server。
call/3
处理call,gen_server。
游戏中有大量的gd配置文件,gd采用ms-excel进行编辑后,服务器开发人员使用程序data/data.py脚本将其导出为csv文件,而slg_csv模块提供csv文件到ets表的直接映射。
slg_csv模块的使用大概如下:
slg_csv:root("data/")
指定了游戏中csv文件存放的路径.
slg_csv:add
加入一个新的映射项。
slg_csv:load()
执行csv配置文件加载.
以上代码的执行必须在application:start(slg_csv),
,在slg-server
的start函数中已经集成了。
在头文件include/gd_record.hrl
存放所有gd配置的映射数据结构.
在data/
路径下存放所有的csv文件。
本项目在slg_server:csv_config对slg-csv模块进行的初始化,你可以把它移动到任意一个你想它去的地方。
本模块要求csv文件中的第一行(列名)必须和其对应的gd配置文件的record一致,比如:
gd_record.hrl
: -record(gd_vip_exp, {level, exp}).
data/xx.csv
: INT_level;INT_exp
record里面不需要指定字段的类型,因为erlang是无类型的,但是csv文件里的第一行需要字典各列的类型,来帮助映射.
类型名与字段名通过下划线分割,比如:INT_level;INT_exp
在csv文件中不字段不区分大小写,现在支持以下类型:
slg_csv:add
接受3个参数来添加映射:(ets表设定,record指定,文件指定):
例如: slg_csv:add({gd_vip_exp, [public, duplicate_bag, {keypos,2}]},?csv_record(gd_vip_exp), ["vip_exp.csv"])
{gd_vip_exp, [public, duplicate_bag, {keypos,2}]}
:ets表名为:gd_vip_exp,属性:[public, duplicate_bag, {keypos,2}];也可以只是一个gd_vip_exp
。
?csv_record(gd_vip_exp)
其映射的record为gd_vip_exp。
["vip_exp.csv"]
对应的csv文件.
ets属性指定
默认ets属性为[public, set, named_table, {keypos, 2}]
, 你也可以自己制定类型属性,比如.
%% 指定`gd_vip_exp`为bag类型
add({gd_vip_exp, [bag]}, ?csv_record(gd_vip_exp), ["vip_exp.csv"]),
也就是每个ets表可以通过 atom | {atm, [attr|attr]}
指定。
多ets表指定
可以在add的第一个参数指定一个列表,已达到创建多个ets表的目的,但只有第一个ets表为csv文件的导入表,其余的用户自定义为辅助表或结果表.
add([gd_vip_exp, defined_but_not_use] , ?csv_record(gd_vip_exp), ["vip_exp.csv"]),
多csv文件
定义多csv文件将把多个csv文件导入一个相同的ets表:
add(gd_vip_exp, ?csv_record(gd_vip_exp), ["vip_exp.csv", "vip_exp2.csv"]),
注意重复的id会给出错误提示,你可以选择忽略。
inject函数
大部分时候csv表到ets是可以直接映射过去,但是不有的时候也需要手工处理一下再导入到ets,这时候可以使用inject函数.
inject函数是一个普通函数,它接受一个record为参数,返回另一个处理之后的record,只有slg_csv会把这个record导入到ets.
比如,你可以把你的inject函数定义到一个单独的erlang模块:
-module(csv_inject).
-export([compile_all]).
inject_vip_exp(Vip) ->
io:format("vip ~p~n", [Vip]).
然后通过以下参数指定:
add(gd_vip_exp, ?csv_record(gd_vip_exp), [ {fun csv_inject/inject_vip_exp/1, "vip_exp.csv"}]),
inject函数还可以定义一个参数,但是函数原型为:
inject_vip_exp(Vip, P) ->
io:format("vip ~p ~p~n", [Vip, P]),
Vip.
add(gd_vip_exp, ?csv_record(gd_vip_exp), [ {fun csv_inject/inject_vip_exp/2, "vip_exp.csv", 23}]),
可以slg_csv下代码:csv_inject2.erl ok.
程序使用ets表缓存玩家数据,查找数据时会先在ets表中进行,如果不存在则会在MySql中查询,同样,一些删除,更新,插入操作也在ets中直接执行,slg-model会实时的发送给异步持久化进程。
在slg_server.erl
的model_config函数对model层进行了初始化:
model:init_m(),
Dbc = #db_conf{username="root", password="", database="slg_server"},
model:add_m(users, record_info(fields, db_user), Dbc),
model:add_m(devices, record_info(fields, db_device), Dbc),
model:add_m(buildings, record_info(fields, db_building), Dbc),
model:gen_m(), %% 生成配置表
其中db_user
和db_device
都是在协议中定义的数据结构,而这个协议结构的字段和其类型必须和其对应的mysql表一一对应。
model:_add_m(表名,数据类型,连接配置).
数据操作函数集中在data.erl
模块,如下:
-export([lookup_s/2, lookup_a/2, lookup_i/2]).
-export([update_s/3, update_i/2, delete_i/3, delete_s/3, clear/1]).
-export([add_s/3, add_i/3, id/1]).
分别对应与增删查改。
约定:
_s: 为后缀的适合每个玩家只有一条的数据,比如玩家数据。 _a: 为后缀的适合每个玩家有多条的数据,比如建筑。
在player_account里有基本的例子。
data.erl
模块中有lookup_s_e, lookup_i_e, update_i_e, update_s_e
等四个函数,他们可以在模块被初始化之后使用,参看player_account:building_upl_req
本组件提供一些erlang编程或系统工具集合。
热更新进程,来源于michi-web,可以对线上运行的代码进行热替换,但应该遵守以下面规则:
默认情况下spt_reloader启动,当beam代码发生变化时将会自动热更新。
游戏中需要关注很多事件的发生,比如建筑升级事件,玩家打赢了一个boss事件,spt_notify提供事件的注册和发生接口,有以下3个api:
post事件的第2个参数将会被原样传递给注册的函数,使用例子如下:
Fun1 = fun(X) -> io:format("x1 ~p~n", [X]) end,
Fun2 = fun(X) -> io:format("x2 ~p~n", [X]) end,
spt_notify:sub(e1, Fun1),
spt_notify:sub(e1, Fun2),
spt_notify:post(e1, 23),
spt_notify:ubsub(e1, Fun1),
spt_notify:post(e1, 23),
虽然erlang的动态编程能力不强(也或者是我学的很浅),但是smerl这个模块用来做动态模块扩展是比较合适的,它来源于erlyweb项目,已经稳定了很多年。
以下情况适合使用smerl动态产生模块:
使用方法如下,来源于源码注释:
test_smerl() ->
M1 = spt_smerl:new(foo),
{ok, M2} = spt_smerl:add_func(M1, "bar() -> 1 + 1."),
spt_smerl:compile(M2),
foo:bar(), % returns 2``
spt_smerl:has_func(M2, bar, 0). % returns true
slg-proto提供了连接处理和协议打包,加密三个功能;
因为erlang使用了轻量级进程,所以连接处理代码比较简单,主要分为以下三个部分:
本项目使用tcp长连接,主要从hotwheels移植过来,hotwheels
是erlang中tcp连接处理的典范。
如果你使用过其他语言如C/C++,连接处理就不需要自己实现epoll或select之类的io多路复用,这些在erlang底层都帮你处理了,所以使用erlang编写这部分代码非常简单易懂。
有了传输层代码,下面介绍协议层,即传输数据包设计.
每一个数据包都由三部分组成: 包长度(2字节)+包类型(2字节)+包数据(剩余字节)
。
erlang语言本身在建立socket的时候可以指定参数:
inet:setopts(Socket, [{active, once}, {packet, 2}, binary]),
其中{packet, 2}
制定后,对于所有的gen_tcp:send操作,erlang将自动在send的内容之间加上2个字节的内容长度,也就是协议中包长度部分。
一个完整的包收好后,将通过包类型来决定如何处理,包类型的描述我定义在了proto/api.txt
文件中,每个包有以下5个属性需要配置:
数据包的内容我称其为payload,他们全部在proto/protocal.txt
中被定义,包内容定义比较复杂,以下是基础类型,你可以通过基础类型组合成自定义类型:
有了基本类型,你可以自定义一个用户类型:
pt_user=
name string
sex boolean
===
非常简单,你可以定义一个账号类型,它嵌套了用户类型:
pt_account=
user pt_user
money integer
===
很多情况下我们需要数组,你可以这样定义一个含有数组的类型:
pt_test=
ids array integer
users array pt_user
===
游戏中有很多错误码,什么金钱不足,等级不足之类的,建议在proto/error_code.txt
中列出你所有的错误码,大概如下:
10000-ok-成功
10001-inner_error-内部错误
10002-bad_param-参数错误
在api.txt中定义了code_ack作为统一的错误接口,90%的错误提示可以使用该包完成,在代码中你使用起来像这样:
conn:code_ack(timeout)
slg_model旨在提供这样一个模块:
游戏服务器中,用户登陆的时候会把玩家的数据从数据库总load到内存中,之后就在内存中对玩家数据进行操作,定时写回到数据库进行持久化(这里我选用了MySql数据,在游戏行业用的比mongodb稳定广泛),而这一套机制,可以作为服务框架的一部分存在,不需要框架的使用者编程,只需要他们按照相应的接口编写逻辑即可。
slg_model提供了以下三个重要的功能:
mnesia是erlang提供的分布式数据库,功能比较强大,但它的功能对于我设计游戏服务器并不是有用,暂时没有需要使用mnesia的理由,如果设计单服的游戏服务器,并且要防止单点故障,那么可能用mnesia比较合适。
再次说明本模块主要为小服的游戏服务器设计的。
国内有些页游开发团队使用进程字典存储玩家数据,即玩家登陆后将其数据加载到进程字典,然后就在进程字典中操作了,并且按时写回MySql,这样主要的好处是:进程字典操作比较快,纳秒级别;ets操作比较慢,微秒级别。
主要的坏处是:
ets也不是慢,微秒级别,对于小服,拖个几千人就ok了,完全够用,所以我采用ets,也推荐你采用。
进程字典只适合存储临时数据,那种丢了就丢了,或者session相关的,不需要造成你防御式编程的,其它场景慎用
。
按表组织玩家数据有以下好处:
灵活访问非在线玩家数据
当采用进程字典存储玩家数据时,任何对其它玩家数据的访问都需要通过进程通信获得数据,比如好友系统,只需要获取这个好友的名字和账号信息,这时需要建立起这个玩家的进程,然后走消息通信来获得数据,而使用ets表组织,你只需要拿user_id索引其基本信息表即可,非常优雅。
手机游戏的异步性
手机游戏对非在线玩家的数据访问非常频发,因为玩手机游戏的的时间是零碎的,需要灵活的操作非在线玩家数据。
在ge2里,数据大概是每10触发一次同步,但是在slg_server里采用了实时同步,这样的好处是玩家数据更稳定,更不容易丢失,坏处是MySql压力更大,但是slg_server面对的是小服,一个服务器人数是非常有限的(几W),同时在线人数(6K),负载压力本身不会很大,而且持久化过程是异步的,不影响玩家体验。
设计以下几个模块:
model_sql.erl:
提供SQL拼接功能,如果更复杂的SQL操作应丰富这个模块。
model_exec.erl:
执行SQL,使用erlang-mysql-driver实现。
model.erl:
提供model操作的统一接口
对每一张表都有一个单独的进程来进行会写,data_writer_sup.erl
和data_writer
。
slg_model中使用data_clear清除不活跃数据,使用时间超过6小时,并且当前没有人使用的数据被定义为不活跃数据,此部分数据将以玩家为单位执行清除。
每个表有单独的清除进程,代码集中在:data_clear_sup.erl
和data_clear.erl
ets
表不是数据库
,是对其基本操作加了读写锁的内存表,所以下面的代码会有很大问题:
R = read_ets(Id) # 1
if
R == ok ->
set_ets(Id, no) # 2
do_something # 3
no -> do_nothing
end
在#1
和#2
之间没有数据库的隔离性,所以同时会有多个进程可能进入代码#3
,引发逻辑错误,比如清除玩家数据就是这样一个场景。
data_clear
会选择不活跃且没人使用的玩家数据来清除,这其中会破坏ets表的结构,而如果同时又有用户登陆并对其进行操作,就会有逻辑错误。
data_guard
data_guard.erl
用来解决ets表的数据访问冲突,原理是模拟读写锁,当有玩家使用时获取读锁,而清除进程获取写锁。写锁必须要没有读数或写锁的情况下才能获取成功,读数必须要没有写锁的情况下获取成功。
见代码模块model_sql.erl
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。