http://blog.csdn.net/slmeng2002/article/details/5532771
最近关注erlang游戏服务器开发 erlang大牛写的游戏服务器值得参考
介绍
本文以我的OpenPoker项目为例子,讲述了一个构建超强伸缩性的在线多游戏玩家系统。OpenPoker是一个超强多玩家纸牌服务器,具有容错、负载均衡和无限伸缩性等特性。源代码位于我的个人站点上,大概10,000行代码,其中1/3是测试代码。在OpenPoker最终版本敲定之前我做了大量调研,我尝试了Delphi、Python、C#、C/C++和Scheme。我还用Common Lisp写了纸牌引擎。虽然我花费了9个月的时间研究原型,但是最终重写时只花了6个星期的时间。我认为我所节约的大部分时间都得益于选择Erlang作为平台。相比之下,旧版本的OpenPoker花费了一个4~5人的团队9个月时间。Erlang是什么东东?我建议你在继续阅读本文之前浏览下Erlang FAQ,这里我给你一个简单的总结...Erlang是一个函数式动态类型编程语言并自带并发支持。它是由Ericsson特别为控制开关、转换协议等电信应用设计的。Erlang十分适合构建分布式、软实时的并发系统。由Erlang所写的程序通常由成百上千的轻量级进程组成,这些进程通过消息传递来通讯。Erlang进程间的上下文切换通常比C程序线程的上下文切换要廉价一到两个数量级。使用Erlang写分布式程序很简单,因为它的分布式机制是透明的:程序不需要了解它们是否分布。Erlang运行时环境是一个虚拟机,类似于Java虚拟机。这意味着在一个价格上编译的代码可以在任何地方运行。运行时系统也允许在一个运行着的系统上不间断的更新代码。如果你需要额外的性能提升,字节码也可以编译成本地代码。请移步Erlang site,参考Getting started、Documentation和Exampes章节等资源。为何选择Erlang?构建在Erlang骨子里的并发模型特别适合写在线多玩家服务器。一个超强伸缩性的多玩家Erlang后端构建为拥有不同“节点”的“集群”,不同节点做不同的任务。一个Erlang节点是一个Erlang VM实例,你可以在你的桌面、笔记本电脑或服务器上上运行多个Erlang节点/VM。推荐一个CPU一个节点。Erlang节点会追踪所有其他和它相连的节点。向集群里添加一个新节点所需要的只是将该新节点指向一个已有的节点。一旦这两个节点建立连接,集群里所有其他的节点都会知晓这个新节点。Erlang进程使用一个进程id来相互发消息,进程id包含了节点在哪里运行的信息。进程不需要知道其他进程在哪里就可以通讯。连接在一起的Erlang节点集可以看作一个网格或者超级计算设备。超多玩家游戏里玩家、NPC和其他实体最好建模为并行运行的进程,但是并行很难搞是众所皆知的。Erlang让并行变得简单。Erlang的位语法∞让它在处理结构封装/拆解的能力上比Perl和Python都要强大。这让Erlang特别适合处理二进制网络协议。OpenPoker架构OpenPoker里的任何东西都是进程。玩家、机器人、游戏等等多是进程。对于每个连接到OpenPoker的客户端都有一个玩家“代理”来处理网络消息。根据玩家是否登录来决定部分消息忽略,而另一部分消息则发送给处理纸牌游戏逻辑的进程。纸牌游戏进程是一个状态机,包含了游戏每一阶段的状态。这可以让我们将纸牌游戏逻辑当作堆积木,只需将状态机构建块放在一起就可以添加新的纸牌游戏。如果你想了解更多的话可以看看cardgame.erl的start方法。纸牌游戏状态机根据游戏状态来决定不同的消息是否通过。同时也使用一个单独的游戏进程来处理所有游戏共有的一些东西,如跟踪玩家、pot和限制等等。当在我的笔记本电脑上模拟27,000个纸牌游戏时我发现我拥有大约136,000个玩家以及总共接近800,000个进程。下面我将以OpenPoker为例子,专注于讲述怎样基于Erlang让实现伸缩性、容错和负载均衡变简单。我的方式不是特别针对纸牌游戏。同样的方式可以用在其他地方。伸缩性我通过多层架构来实现伸缩性和负载均衡。第一层是网关节点。游戏服务器节点组成第二层。Mnesia“master”节点可以认为是第三层。Mnesia是Erlang实时分布式数据库。Mnesia FAQ有一个很详细的解释。Mnesia基本上是一个快速的、可备份的、位于内存中的数据库。Erlang里没有对象,但是Mnesia可以认为是面向对象的,因为它可以存储任何Erlang数据。有两种类型的Mnesia节点:写到硬盘的节点和不写到硬盘的节点。除了这些节点,所有其他的Mnesia节点将数据保存在内存中。在OpenPoker里Mnesia master节点会将数据写入硬盘。网关和游戏服务器从Mnesia master节点获得数据库并启动,它们只是内存节点。当启动Mnesia时,你可以给Erlang VM和解释器一些命令行参数来告诉Mnesia master数据库在哪里。当一个新的本地Mnesia节点与master Mnesia节点建立连接之后,新节点变成master节点集群的一部分。假设master节点位于apple和orange节点上,添加一个新的网关、游戏服务器等等。OpenPoker集群简单的如下所示:- 代码:
-
erl -mnesia extra_db_nodes /['db@apple','db@orange'/] -s mnesia start
- 代码:
-
erl -mnesia extra_db_nodes /['db@apple','db@orange'/]Erlang (BEAM) emulator version 5.4.8 [source] [hipe] [threads:0]Eshell V5.4.8 (abort with ^G)1> mnesia:start().ok
- 代码:
-
login({atomic, [Player]}, [_Nick, Pass|_] = Args) when is_record(Player, player) -> Player1 = Player#player { socket = fix_pid(Player#player.socket), pid = fix_pid(Player#player.pid) }, Condition = check_player(Player1, [Pass], [ fun is_account_disabled/2, fun is_bad_password/2, fun is_player_busy/2, fun is_player_online/2, fun is_client_down/2, fun is_offline/2 ]), ...
- 代码:
-
is_player_busy(Player, _) -> {Online, _} = is_player_online(Player, []), Playing = Player#player.game /= none, {Online and Playing, player_busy}.is_player_online(Player, _) -> SocketAlive = Player#player.socket /= none, PlayerAlive = Player#player.pid /= none, {SocketAlive and PlayerAlive, player_online}.is_client_down(Player, _) -> SocketDown = Player#player.socket == none, PlayerAlive = Player#player.pid /= none, {SocketDown and PlayerAlive, client_down}.is_offline(Player, _) -> SocketDown = Player#player.socket == none, PlayerDown = Player#player.pid == none, {SocketDown and PlayerDown, player_offline}.
- 代码:
-
fix_pid(Pid) when is_pid(Pid) -> case util:is_process_alive(Pid) of true -> Pid; _-> none end;fix_pid(Pid) -> Pid.
- 代码:
-
-module(util).-export([is_process_alive/1]).is_process_alive(Pid) when is_pid(Pid) -> rpc:call(node(Pid), erlang, is_process_alive, [Pid]).
- 代码:
-
login(Player, player_offline, [Nick, _, Socket]) -> {ok, Pid} = player:start(Nick), OID = gen_server:call(Pid, 'ID'), gen_server:cast(Pid, {'SOCKET', Socket}), Player1 = Player#player { oid = OID, pid = Pid, socket = Socket }, {Player1, {ok, Pid}}.
- 代码:
-
login(Player, bad_password, _) -> N = Player#player.login_errors + 1, {atomic, MaxLoginErrors} = db:get(cluster_config, 0, max_login_errors), if N > MaxLoginErrors -> Player1 = Player#player { disabled = true }, {Player1, {error, ?ERR_ACCOUNT_DISABLED}}; true -> Player1 = Player#player { login_errors =N }, {Player1, {error, ?ERR_BAD_LOGIN}} end;login(Player, account_disabled, _) -> {Player, {error, ?ERR_ACCOUNT_DISABLED}};
- 代码:
-
logout(OID) -> case db:find(player, OID) of {atomic, [Player]} -> player:stop(Player#player.pid), {atomic, ok} = db:set(player, OID, [{pid, none}, {socket, none}]; _-> oops end.
- 代码:
-
login(Player, player_online, Args) -> logout(Player#player.oid), login(Player, player_offline, Args);
- 代码:
-
login(Player, client_down, [_, _, SOcket]) -> gen_server:cast(Player#player.pid, {'SOCKET', Socket}), Player1 = Player#player { socket = Socket }, {Player1, {ok, Player#player.pid}};
- 代码:
-
login(Player, player_busy, Args) -> Temp = login(Player, client_down, Args), cardgame:cast(Player#player.game, {'RESEND UPDATES', Player#player.pid}), Temp;
- 代码:
-
网关怎么知晓游戏服务器?
- 代码:
-
find_server(MaxPlayers) -> case pg2:get_closest_pid(?GAME_SERVER) of Pid when is_pid(Pid) -> {Time, {Host, Port}} = timer:tc(gen_server, call, [Pid, 'WHERE']), Coutn = gen_server:call(Pid, 'USER COUNT'), if Count < MaxPlayers -> io:format("~s:~w ~w players~n", [Host, Port, Count]), {Host, Port}; true -> io:format("~s:~w is full...~n", [Host, Port]), find_server(MaxPlayers) end; Any -> Any end.
- 代码:
-
多功能热插拔中间件