http://blog.sina.com.cn/s/blog_7e0be1580101r09c.html
引言 “哦,怎么又是一门编程语言,够了,我不需要了!”
也许你看到本文题目首先想到的就是这个。我第一次看到Erlang语言时也是这么想的。
曾几何时,面向对象的程序设计语言(C++、JAVA、C#等)一直是我心目中无所不能的“大神”,但是在计算机多核化发展日新月异、“云计算”“云存储”火爆异常的今天,这些主流语言对并发和分布式程序的支持似乎不是那么得心应手。越来越多的锁、信号量、线程、同步、互斥之类的概念总是让程序员头痛不已。
为了适应并行计算和分布式应用的大趋势,Java5.0引入了concurrency库,C++有了标准化的OpenMP和MPI,微软发布了Parrallel FX和MPI.NET等一系列产品。然而,这些亡羊补牢的手段却不能弥补它们的“先天不足”——即这些语言在创作时就没有把并行化的问题放到优先的位置上去考虑。
也许是时候让天生面向并发的Erlang语言大显身手了。
Erlang最初应用于电信领域,但是现在也远不限于此。Facebook(也许中国人不大熟悉)用它重写了超大型聊天系统,Amazon用它开发了SimpleDB云存储系统,Yahoo!用它开发了Deliciou2.0,类似的成功项目比比皆是。
那么,Erlang语言到底是何方神圣呢?且听下文分解。
1 Erlang语言的由来
有人说Erlang语言是“因并发而生,因云计算而热”。与主流语言不同,它是一种“函数式语言(Functional programming,FP)”,使用的是面向并发编程(COP,Concurrency Oriented Programming)的方法。Erlang并不是一门新的语言,它已经有二十多年的历史了。只是由于近些年对高并发、分布部署、持续服务的需求日渐增加,以及多核CPU的全面普及,Erlang才又引起人们的关注。
Erlang得名于丹麦数学家及统计学家Agner Krarup Erlang,同时Erlang还可以表示Ericsson Language。1987年,Erlang语言创始人Joe Armstrong当年在爱立信做电话网络方面的开发,他使用Smalltalk,可惜那个时候Smalltalk太慢,不能满足电话网络的高性能要求。但Joe实在喜欢Smalltalk,于是定购了一台Tektronix Smalltak机器。但机器要两个月时间才到,Joe在等待中百无聊赖,就开始使用Prolog,结果等Tektronix到来的时候,他已经对Prolog更感兴趣,Joe当然不满足于精通Prolog,经过一段时间的试验,Joe给Prolog加上了并发处理和错误恢复,于是Erlang就诞生了。
2 Erlang语言的特性
Erlang语言非常有特色,并发、分布、容错贯穿于程序的始终。在它的语法里,相等其实不是相等,变量实际上不能改变,实在是非常古怪。但是为了不使本文过于冗长,这里不介绍它的语法,只
2.1 跨平台性
Erlang运行时环境是一个虚拟机,有点像Java虚拟机,这样的代码一经编译,同样可以随处运行。当然,如果你需要更高效的话,字节代码也可以编译成本地代码运行。
2.2并发性
现实世界是并发的。我们很多人可以共同协作完成一项工作。人与人之间不存在任何“Shared Memory(共享的记忆)”,通过传递消息(如语言、文字、手势、表情等)来进行交互。这是现实世界的存在方式,也是Erlang进程的工作方式。
Erlang的并发特性源自语言本身而非操作系统。它创建了一种新的做法,使进程可以非常快地创建和销毁。因此,Erlang程序可以由多达几百万个超轻量级的进程组成,这些进程可以运行于单处理器、多核处理器或者处理器网络上。Erlang进程之间高度隔离,没有共享任何数据,只能通过消息传递来进行交互,并且这种消息传递也是非常迅捷的。
Erlang程序的消息传递是通过进程的消息邮箱来实现的。每个进程都有一个用于接受别的进程发来的消息的邮箱。进程可以在任何时候检查自己的邮箱有没有新消息抵达,如果有,进程可以按照抵达的顺序依次读取。
消除了共享内存和进程之间的消息通信是Erlang最明显的优势之一,这一特点为进行并行与分布式应用的开发提供了非常好的基础。
2.3分布式
Erlang被设计用于运行在分布式环境下。一个Erlang虚拟机作为一个Erlang节点。一个分布式Erlang系统是由多个Erlang节点组成的网络。使用Erlang来编写分布式应用要简单的多,因为它的分布式机制是透明的:对于程序来说并不知道自己是在分布式运行。
2.4健壮性
Erlang可以说是一门为健壮而生动语言。它不像其它语言一样假设不会发生错误,而是假定错误随时可能发生,并随时准备修正错误。
在一个分布式系统中,如果一个进程出错,让这个进程自己修复错误有时并不可行,有时节点可能彻底崩溃(死机),因此将错误交给其他进程去处理是更恰当的选择。
在现实世界中,如果一个人停止工作,他就会告诉其他人“我头疼无法工作了”或者“我心脏病发作了”等。
Erlang错误侦测的工作机制也类似于此,通过进程间的相互“链接”来实现进程监控(这些进程可以运行于不同节点之上)机制,将错误进行分层处理,从而实现构造“容错系统”。Erlang引入了“速错(Fail fast)”的理念,在一个进程出错时不会试图自行处理,而是直接退出,并发出错误消息如“有人让我去除0”或者“有人让我从空列表中读取数据”等,让其他进程进行错误处理、故障恢复等工作。
2.5 软实时性
Erlang支持可编程的“软”实时系统,这种系统需要反应时间在毫秒级。而在这种系统中,长时间的垃圾收集(garbage collection)延迟是无法接受的,因此Erlang使用了递增式垃圾收集技术。
2.6热部署(Hot swap)
有些系统是不能够由于软件维护而停止运行的。Erlang允许程序代码在系统运行中被修改。旧第代码能被逐步淘汰而后被新代码替换。在此过渡期间,新旧代码是共存的。这也使得安装bug补丁、系统升级而不干扰系统运行成为了可能。
2.7 递增式代码装载
用户能够控制代码如何被装载的细节。在嵌入式系统中,所有代码通常是在启动时就被完全装载。而在开发系统中,代码是按需装载的,甚至在系统运行时被装载的。如果测试到了未覆盖的bug,那么只有具有bug的代码需要被替换。
2.8 外部接口
Erlang进程与外部世界之间的通讯机制与Erlang进程之间相同的消息传送机制相同。这种机制被用于和操作系统通讯、与其它语言编写的程序交互。如果出于高效率的需要,这种机制的一个特殊版本也允许例如C程序这样的代码直接链接到Erlang运行时系统中来。
3 Erlang语言的分布式编程
3.1 分布式系统的特性
George Coulouris认为:分布式系统是由位于网络中的一些计算机组成的系统,这些网络中的计算机之间只能通过传递消息来进行沟通和协调。Andrew S.Tanenbaum说:一个分布式系统是一些独立计算机的集合,但是对这个系统的用户来说,系统就像一台计算机一样。
尽管分布式系统还没有一个公认令人满意的定义,但是所有分布式系统都有以下两个特点:一、系统的各个节点(计算机)是相互独立的;二、各个节点之间只能通过消息传递来进行沟通和协调。
这两个特点很耳熟吧?没错,这也正是Erlang程序的特点!各进程相互独立,且只能通过消息传递来进行交互。
Erlang与分布式系统天性互相吻合,决定了使用Erlang开发分布式系统具有得天独厚的优势。Erlang天生面向并发,在Erlang开发的分布式系统增加和删除节点非常简便,具有良好的伸缩性。
3.2 Erlang语言分布式编程的两种基本模型
3.2.1 分布式Erlang
这种模型用于在一个紧密耦合的计算机集群上编写程序。这种模型几乎不需要有什么额外操作,一切就像在单节点编程时一样:我们可以在任何一个节点创建进程,所有的消息传递和错误处理原语都可以照搬。
分布式Erlang运行于一个可信任的环境中——因为任何节点都可以运行其他节点上的操作,这就需要一个高度信任的网络环境。
3.2.2 基于套接字的分布式应用
使用TCP/IP套接字,我们可以编写运行在非信任环境中的分布式应用程序。这种编程模型的功能比分布式Erlang要弱一些,但是却更加可靠。
3.3分布式编程实例:名字服务和远程过程调用
下面我们用Erlang语言来实现一个分布式应用实例:名字服务(Naming Service)。这里面将会用到远程过程调用(Remote Procedure Call,RPC),你会发现使用Erlang实现RPC简直是易如反掌。
名字服务就是一种“Key-Value”类型的服务。客户机向服务器发送一个名字(Key),服务器返回与这个名字(Key)相关联的值(Value),这样的服务就叫做名字服务。
3.3.1 程序源码
文件名kvs.erl:
-module(kvs). |
-export([start/0, store/2, lookup/1]). |
|
start() -> register(kvs, spawn(fun() -> loop() end)). |
|
store(Key, Value) -> rpc({store, Key, Value}). |
|
lookup(Key) -> rpc({lookup, Key}). |
|
rpc(Q) -> |
kvs !{self(), Q}, |
receive |
{kvs, Reply}-> |
Reply |
end. |
|
loop() -> |
receive |
{From, {store, Key, Value}} -> |
put(Key,{ok, Value}), |
From !{kvs, true}, |
loop(); |
{From, {lookup, Key}} -> |
From !{kvs, get(Key)}, |
loop() |
end. |
可以看到,使用Erlang编写的代码非常简洁。代码中使用了Erlang编程中常用的尾递归。
3.3.2 在同一个局域网内的不同机器上运行客户机和服务器
我所使用的系统是Microsoft Windows 7,所以下面的例子都是在Windows操作系统上完成的。应首先保证系统上安装有一个可运行的Erlang版本,我所安装的版本是5.8.1.1。
首先准备使用两个节点:第一个节点位于名为SERVER计算机上充当服务器的ss节点,另外一个节点是位于位于名为CLIENT的计算机上充当客户端的cc。
步骤1:在SERVER上启动一个名为ss的Erlang节点。kvs模块部署于ss节点上。
C:\ erl –sname ss –setcookie abc Eshell V5.8.1.1 (abort with ^G) (ss@SERVER)1> |
步骤2:在节点ss上启动服务。
(ss@SERVER)1>kvs:start(). true |
步骤3:在CLIENT上启动一个名为cc的Erlang节点,应保证与服务器节点使用相同的cookie。
C:\ erl –sname cc –setcookie abc Eshell V5.8.1.1 (abort with ^G) (cc@CLIENT)1> |
步骤4:现在我们就可以在客户端节点cc上进行远程过程调用了。首先调用服务器节点ss上kvs模块的store进程,存储一个名字 “wheather”,其关联值为“cold”。
(cc@CLIENT)1>rpc:call(ss@SERVER , kvs, store, [wheather, cold]). true (cc@CLIENT)2> |
注意:这里的rpc是Erlang标准库里的一个模块,与kvs模块中的rpc毫无关系。
现在就可以远程调用ss上kvs模块的lookup进程取得“wheather”的关联值。
(cc@CLIENT)2>rpc:call(ss@SERVER , kvs, lookup, [wheather]). {ok,cold} (cc@CLIENT)3> |
这样,我们就轻而易举地实现了一个简单名字服务。
4 Erlang语言的应用前景
4.1 Erlang至今没有广泛应用的原因
尽管Erlang已经有二十多高龄了,利用它也成功地实现了一批项目。但是,它迄今为止还是一门小众语言。连Joe Armstrong 也抱怨说,Erlang总是充当“救火队员”的角色,总是碰到用其它语言难以解决的难题时,人们才会想起Erlang。那么是什么阻碍了Erlang的广泛应用呢?我觉得主要有以下一些原因:
4.1.1 软件开发人员方面的原因。
传统程序员的思维方式还不能适应并行程序设计的理念;经过了OO训练的程序员也难以适应COP的模式。习惯了OO(面向对象)语言的程序员在刚接触到Erlang时一定会觉得很不适应。历史告诉我们,习惯的力量是强大的,不顺应大众习惯的东西,成长总是会很艰难。此外,Erlang是小众语言,使用它还存在着维护上的风险。
4.1.2 Erlang语法方面的原因
Erlang的语法的确有些“怪异”,学习困难。它实际上和我们主流的编程的方法、思路是不一样的。主流编程语言如C++、Java等是基于图灵机的命令式语言,而Erlang是基于λ-演算的函数式语言。
使用Erlang编写的代码非常简洁(几乎可以简洁到极致),代码量可以非常少。但是我个人认为它并不够优雅(Joe Armstrong一定不同意我的看法),难以读懂(在没有注释的情况下更是如此),并且也难以学习。
Erlang语言对二进制数据操作支持较好,但是在字符编码这方面的支持是比较薄弱的。当前版本的Erlang(v5.5.2)使用的是Latin-1(ISO-8859-1)编码的字符。使用Erlang操作多字节字符存在一定困难。这点对于使用中文的我们来说,非常不便。
4.1.3 开发工具方面的原因
Erlang至今仍没有一个优秀的集成开发环境(IDE),仍然停留在命令行阶段。尽管有些人(我觉得他们属于技术狂一类)认为IDE的功能太强大,会让程序员越来越懒,但是这的确造成Erlang的吸引力不足。使用Erlang进行桌面应用的界面设计也很麻烦。
4.2Erlang与其它面向并发的编程语言
除了Erlang以外,面向并发的编程语言还有很多。下面简单介绍我找到的几种:
q Scala语言
Scala语言近年来的流行度也在不断提升。它是一种JVM语言,运行于Java虚拟机之上。Scala语言是静态类型的,兼具函数式语言(FP)和面向对象编程(OO)的特点。它比Java有着更简洁的语法,但运行速度不逊于Java甚至超过Java。但是Scala进程与Erlang相比还是不够轻量级。
q Go语言
Go语言是谷歌2009发布的第二款开源编程语言。它专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。Go语言一诞生就备受青睐,由于在2009年市场份额增长最多而一举摘得了TIOBE公布的年度大奖。
q F#语言
F#语言是微软推出的一门函数式语言(FP),是一种运行在.Net CLR平台上的新语言。它是OCaml的一个分支,兼具了函数式和命令式面向对象语言的特点;同时它也是一种强类型的编程语言。作为微软唯一的FP语言,F#的特殊性引起了很多人的关注,而微软也致力于将F#打造成为.NET平台的顶峰语言,我们也期待着F#能够给函数式编程语言带来一次重大革命。
Scala、Go、F#、Erlang各有特点,很难分出高下。尽管不能断定Erlang是不是在并行领域是最优的语言,但至少目前来说我觉得Erlang是比较好的。Erlang凭借20多年的发展经验,在Erlang的层级上支持高并发、浓缩性和数据的持续性存储都有丰富的库有着相当多的成熟库(OTP)和开源软件,这些资产使得它有极高的实用价值。此外,Erlang还有非常活跃和成熟的社区为开发者解疑释惑,甚至在社区经常能看到。在熟悉了Erlang的思维方法和社区后,用Erlang开发将是一件非常轻松、非常高效的事情。如果你想在将来的构建多核的应用程序或者是健壮的分布式系统,你应该学习Erlang。
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
-s mnesia start相当于这样在erlang shell里启动Mnedia:
- 代码: 全选
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
OpenPoker在Mnesia表里保存配置信息,并且这些信息在Mnesia启动后立即自动被新的节点下载。零配置!
容错
通过添加廉价的Linux机器到我的服务器集群,OpenPoker让我随心所欲的变大。
将几架1U的服务器放在一起,这样你就可以轻易的处理500,000甚至1,000,000的在线玩家。这对MMORPG也是一样。
我让一些机器运行网关节点,另一些运行数据库master来写数据库事务到硬盘,让其他的机器运行游戏服务器。
我限制游戏服务器接受最多5000个并发的玩家,这样当游戏服务器崩溃时最多影响5000个玩家。
值得注意的是,当游戏服务器崩溃时没有任何信息丢失,因为所有的Mnesia数据库事务都是实时备份到其他运行Mnesia以及游戏服务器的节点上的。
为了预防出错,游戏客户端必须提供一些援助来平稳的重连接OpenPoker集群。
一旦客户端发现一个网络错误,它应该连接网关,接受一个新的游戏服务器地址,然后重新连接新的游戏服务器。
下面发生的事情需要一定技巧,因为不同类型的重连接场景需要不同的处理。
OpenPoker会处理如下几种重连接的场景:
1,游戏服务器崩溃
2,客户端崩溃或者由于网络原因超时
3,玩家在线并且在一个不同的连接上
4,玩家在线并且在一个不同的连接上并在一个游戏中
最常见的场景是一个客户端由于网络出错而重新连接。
比较少见但仍然可能的场景是客户端已经在一台机器上玩游戏,而此时从另一台机器上重连接。
每个发送给玩家的OpenPoker游戏缓冲包和每个重连接的客户端将首先接受所有的游戏包,因为游戏不是像通常那样正常启动然后接受包。
OpenPoker使用TCP连接,这样我不需要担心包的顺序——包会按正确的顺序到达。
每个客户端连接由两个OpenPoker进程来表现:socket进程和真正的玩家进程。
先使用一个功能受限的visitor进程,直到玩家登录。例如visitor不能参加游戏。
在客户端断开连接后,socket进程死掉,而玩家进程仍然活着。
当玩家进程尝试发送一个游戏包时可以通知一个死掉的socket,并让它自己进入auto-play模式或者挂起。
在重新连接时登录代码将检查死掉的socket和活着的玩家进程的结合。代码如下:
- 代码: 全选
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
]),
...
condition本身由如下代码决定:
- 代码: 全选
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}.
注意login方法的第一件事是修复死掉的进程id:
- 代码: 全选
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]).
Erlang里一个进程id包括正在运行的进程的节点的id。
is_pid(Pid)告诉我它的参数是否是一个进程id(pid),但是不能告诉我进程是活着还是死了。
Erlang自带的erlang:is_process_alive(Pid)告诉我一个本地进程(运行在同一节点上)是活着还是死了,但没有检查远程节点是或者还是死了的is_process_alive变种。
还好,我可以使用Erlang rpc工具和node(pid)来在远程节点上调用is_process_alive()。
事实上,这跟在本地节点上一样工作,这样上面的代码就可以作为全局分布式进程检查器。
剩下的唯一的事情是在不同的登录条件上活动。
最简单的情况是玩家离线,我期待一个玩家进程,连接玩家到socket并更新player record。
- 代码: 全选
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}};
注销玩家包括使用Object ID(只是一个数字)找到玩家进程id,停止玩家进程,然后在数据库更新玩家record:
- 代码: 全选
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.
这样我就可以完成多种重连接condition,例如从不同的机器重连接,我只需先注销再登录:
- 代码: 全选
login(Player, player_online, Args) ->
logout(Player#player.oid),
login(Player, player_offline, Args);
如果玩家空闲时客户端重连接,我所需要做的只是在玩家record里替换socket进程id然后告诉玩家进程新的socket:
- 代码: 全选
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;
总体来说,一个实时备份数据库,一个知道重新建立连接到不同的游戏服务器的客户端和一些有技巧的登录代码运行我提供一个高级容错系统并且对玩家透明。
负载均衡
我可以构建自己的OpenPoker集群,游戏服务器数量大小随心所欲。
我希望每台游戏服务器分配5000个玩家,然后在集群的活动游戏服务器间分散负载。
我可以在任何时间添加一个新的游戏服务器,并且它们将自动赋予自己接受新玩家的能力。
网关节点分散玩家负载到OpenPoker集群里活动的游戏服务器。
网关节点的工作是选择一个随机的游戏服务器,询问它所连接的玩家数量和它的地址、主机和端口号。
一旦网关找到一个游戏服务器并且连接的玩家数量少于最大值,它将返回该游戏服务器的地址到连接的客户端,然后关闭连接。
网关上绝对没有压力,网关的连接都非常短。你可以使用非常廉价的机器来做网关节点。
节点一般都成双成对出现,这样一个节点崩溃后还有另一个继续工作。你可能需要一个类似于Round-robin DNS的机制来保证不只一个单独的网关节点。
- 代码: 全选
网关怎么知晓游戏服务器?
OpenPoker使用Erlang Distirbuted Named Process Groups工具来为游戏服务器分组。
该组自动对所有的节点全局可见。
新的游戏服务器进入游戏服务器后,当一个游戏服务器节点崩溃时它被自动删除。
这是寻找容量最大为MaxPlayers的游戏服务器的代码:
- 代码: 全选
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.
pg2:get_closest_pid()返回一个随机的游戏服务器进程id,因为网关节点上不允许跑任何游戏服务器。
如果一个游戏服务器进程id返回,我询问游戏服务器的地址(host和port)和连接的玩家数量。
只要连接的玩家数量少于最大值,我返回游戏服务器地址给调用者,否则继续查找。
- 代码: 全选
多功能热插拔中间件
OpenPoker是一个开源软件,我最近也正在将其投向许多棋牌类运营商。所有商家都存在容错性和可伸缩性的问题,即使有些已经经过了长年的开发维护。有些已经重写了代码,而有些才刚刚起步。所有商家都在Java体系上大笔投入,所以他们不愿意换到Erlang也是可以理解的。
但是,对我来说这是一种商机。我越是深入研究,越发现Erlang更适合提供一个简单直接却又高效可靠的解决方案。我把这个解决方案看成一个多功能插座,就像你现在电源插头上连着的一样。
你的游戏服务器可以像简单的单一套接字服务器一样的写,只用一个数据库后台。实际上,可能比你现在的游戏服务器写得还要简单。你的游戏服务器就好比一个电源插头,多种电源插头接在我的插线板上,而玩家就从另一端流入。
你提供游戏服务,而我提供可伸缩性,负载平衡,还有容错性。我保持玩家连到插线板上并监视你的游戏服务器们,在需要的时候重启任何一个。我还可以在某个服务器当掉的情况下把玩家从一个服务器切换到另一个,而你可以随时插入新的服务器。
这么一个多功能插线板中间件就像一个黑匣子设置在玩家与服务器之间,而且你的游戏代码不需要做出任何修改。你可以享用这个方案带来的高伸缩性,负载平衡,可容错性等好处,与此同时节约投资并写仅仅修改一小部分体系结构。
你可以今天就开始写这个Erlang的中间件,在一个特别为TCP连接数做了优化的Linux机器上运行,把这台机器放到公众网上的同时保持你的游戏服务器群组在防火墙背后。就算你不打算用,我也建议你抽空看看Erlang考虑一下如何简化你的多人在线服务器架构。而且我随时愿意帮忙!
posted on 2016-03-15 14:16
思月行云 阅读(804)
评论(0) 编辑 收藏 引用 所属分类:
Erlang