最近使用erlang重构了游戏服务器的所有代码,之前看过C++/lua写的服务器引擎代码,引擎实现了玩家属性自动同步给前端和增量更新玩家数据到数据库的功能,这也是现在很多游戏服务器的优化方向,在引擎层面去解决数据同步和数据持久化,数据发生变化了业务层不需要关心怎么去同步给前端。由于游戏过程中玩家每个业务中玩家数据更改的量其实是很少的,增量的去持久化数据到数据库,可以大大减少数据库压力,基本可以做到实时回写数据(比如隔几s回写一次),就算服务器某个时候down了,对玩家的数据也不会造成很大影响。
本着程序员都要有偷懒的精神,所以我也希望在erlang这个架构下实现这样的功能。由于erlang没有类似lua有index, newindex这样的语义,在lua中数据改变时及时让引擎层知道,从而增量去记录数据的变动。一个方法就是自己重新封装所有修改数据的接口,但是这样效率会低很多。比如一次要修改record的某几个字段,就要循环遍历n次,每次修改一个字段,每次修改的时候会导致整个record浅拷贝一次。而且自己封装接口也显得非常不优雅。另一个方法就是改写erlang的vm代码,但是这样风险比较大,毕竟需要对vm很熟悉。怎么办??? 这时候parse_transform编译选项来搭救了。
编译erl文件,会先生成中间代码,然后在由中间代码进一步生成vm执行的opcode。erlang的parse_transform编译选项就是提供一个回调模块,让编程者在生成opcode之前对中间代码进行处理,以改变erlang的某些语义。
想了解erlang编译成vm最终执行的代码经过的步骤可参考:http://www.cnblogs.com/me-sa/p/know-a-little-erlang-opcode.html , 这里主要讲讲parse_transform这个编译选项,在github开源项目lager中就用到了。
在Emakefile的编译选项加上{parse_transform, test_transform}(test_transform可以是其他文件名),test_transform.erl需要export([parse_transform/2]). 在使用make编译erl文件生成abstract_code之后会调用test_transform:parse_transform/2对abstract_code进行处理,处理完返回新的abstract_code。在lager项目中就是通过在编译期间调用lager_transform:parse_transform/2对lager:debug/info/warn/error等函数进行展开实现代码的嵌入,因此在使用lager时,是找不到lager这个模块的,都是在编译期间展开。有兴趣的可以去阅读lager项目的代码。
我实现的方案就是在编译期间去记录数据的变更的,收集变更的数据,定时或者按照自己的方式把数据打包一次性发给前端,同时增量的去持久化数据。在写游戏逻辑的时候再也不需要关心数据同步和持久化了,框架层自动帮我做了,而且对性能几乎没什么损失。当然由于游戏数据是非常复杂的,很难做到所有数据的自动同步,而且由于某些业务逻辑的特殊性也不可能使用这种方式去同步数据。但是基本上单个数值这一类的属性就可以这样处理,已经可以省不少代码量了。具体最终怎么做到的就不细说了,这里只提供思路哈。
下面给个例子说明一下parse_transform编译选项的作用:
代码:
-module(test). -compile(export_all). main() -> A = 1, io:format("A: ~p~n", [A]).
-module(test_transform). -export([parse_transform/2]). parse_transform(AST, _Options) -> walk_ast([], AST). walk_ast(Acc, []) -> lists:reverse(Acc); walk_ast(Acc, [{function, Line, Name, Arity, Clauses}|T]) -> walk_ast([{function, Line, Name, Arity, walk_clauses([], Clauses)}|Acc], T); walk_ast(Acc, [H|L]) -> walk_ast([H|Acc], L). walk_clauses(Acc, []) -> lists:reverse(Acc); walk_clauses(Acc, [{clause, Line, Arguments, Guards, Body}|T]) -> walk_clauses([{clause, Line, Arguments, Guards, walk_body([], Body)}|Acc], T). walk_body(Acc, []) -> lists:reverse(Acc); walk_body(Acc, [H|T]) -> walk_body([transform_statement(H)|Acc], T). transform_statement({match, Line, {var, Line, 'A'}, {integer, Line, _V}}) -> {match, Line, {var, Line, 'A'}, {integer, Line, 2}}; transform_statement(Stmt) when is_tuple(Stmt) -> list_to_tuple(transform_statement(tuple_to_list(Stmt))); transform_statement(Stmt) when is_list(Stmt) -> [transform_statement(S) || S <- Stmt]; transform_statement(Stmt) -> Stmt.
test_transform的作用是把语句A = 1替换成A = 2.
测试结果:
([email protected])18> c(test).
{ok,test}
([email protected])19> test:main().
A: 1
ok
([email protected])20> c(test_transform).
{ok,test_transform}
([email protected])21> c(test, [{parse_transform,test_transform}]).
{ok,test}
([email protected])22> test:main().
A: 2
ok
我们看到A的值变成了2, 替换成功。
查看test.beam的abstract_code (一定要加上debug_info,才能查看abstract_code):
([email protected])25> c(test, [debug_info]).
{ok,test}
([email protected])26> {ok,{_,[{abstract_code,{_,AC}}]}} = beam_lib:chunks(filename:dirname(code:which(test)) ++ "/test.beam",[abstract_code]).
([email protected])27> AC.
[{attribute,1,file,{"test.erl",1}},
{attribute,1,module,test},
{attribute,2,compile,export_all},
{function,4,main,0,
[{clause,4,[],[],
[{match,5,{var,5,'A'},{integer,5,1}},
{call,6,
{remote,6,{atom,6,io},{atom,6,format}},
[{string,6,"A: ~p~n"},{cons,6,{var,6,'A'},{nil,6}}]}]}]},
{eof,7}]
([email protected])28>
由此可见erlang其实是很强大的,各种工具都齐全,只有想不到,没有做不到。。。。。