建立一个OTP应用

概述

Erlang是一门干净简洁的语言,也容易学习。这只是Erlang,我们现在要讨论OTP。当进入到OTP之后,学习曲线就一下子上去了。各种各样的问题就来了,比如:我怎么启动一个应用,监督员(supervisor)的功能是什么,还有我要怎样使用gen_server?这还只是开始,后面还有更让人迷惑的……“.app文件是啥,怎么发布,还有,有人知道.script和.boot文件是干嘛的吗?”

本教程将带你一起创建一个叫做location_server的OTP应用。这个应用可以储存客户端的位置,允许别人从连接的节点查询这些位置,同时还允许客户端订阅位置变更时间所发出的通知。在整个创建location_server的过程中,我将解答以上那些关于OTP应用的问题,以及一些别的东西。

第一部分:建立服务器

首先要下载本教程的引用构建系统,可以在www.erlware.org上的downloads otp_base-<vsn>中找到。你在自己本地的Unix/Linux机器上下好这个代码之后,将构建系统解压出来。

我们首先要做的是对这个构建系统进行初始构建,只要在otp目录下输入make即可。注意:如果你的Erlang没有安装/usr/local/lib/erlang下的话,你可以创建一个指向你所安装的目录的符号链接,或者也可以将环境变量ERL_RUN_TOP设置为安装目录(确保使用的是绝对路径)。

然后第二件要做的事情是搭好应用程序的骨架。完成之后我就会剖析这个骨架并解释它的各个部分的含义。使用appgen工具来创建location_server的应用程序骨架。

我们已经在lib/location_server中创建好了一个应用、并在release/location_server_rel中创建好了一个发布。现在转入lib/location_server目录看看都有些什么。

其中include目录包含了一个叫作location_service.hrl的文件。我们会把所有共享的宏(macro)和记录定义放在这里。src目录包含了所有的Erlang文件(.erl)。vsn.mk文件包含了make变量LOCATION_SERVER_VSN=1.0,表示我们的应用的初始版本(可以随意更改)。这个版本在处理发布的时候会起到作用,我们会在后面讲到。

src目录包含了一个叫做location_server.erl的文件。注意这个Erlang源文件和我们的应用的名字一样。根据约定,这个文件将包含我们的应用的API。这个文件也包含了-behaviour(application)指令。这表示这个文件可以用作该OTP应用结构的入口点。

start/2函数是一个回调函数,OTP应用系统将用它启动由OTP发布系统所指定的应用。这个函数要求两个参数(你可以阅读关于监督机制的文章来了解更多关于该参数的信息)同时它会返回{ok, Pid}或者{ok, Pid, State},前者更为常见,代表应用成功启动。注意start/2函数调用了ls_sup:start_link/1函数。ls_sup.erl包含了我们的顶层监督员。注意名称ls_sup中的前缀“ls”其实是我们的应用程序名称“location_server”的两个单词的缩写。这种前缀也是一种约定,它用于实现Erlang中的包结构来避免一个发布中的名称冲突。你也可以随意指定一个前缀,这种应用程序名称缩写的方式仅仅是我的建议。

顶层监督员将启动应用程序所必须的所有的工人(worker)以及底层的监督员。它指定了一种策略,详细描述了当进程崩溃的时候如何被重启。这在Erlang中是一个关键的概念,我建议你阅读一下www.erlang.org上的关于监督机制的设计原理。

这个文件导出了两个函数start_link/1init/1。这是遵照监督员行为的两个函数——注意出现在文件顶部的-behaviour(supervisor)指令。指定一个函数start_link是在OTP中的一种约定,表示该函数会产生(spawn)一个进程并且这个函数的调用者,也就是父进程,会被联接到(参见link/1)到子进程。这种情况下OTP应用结构会被联接到监督员进程上。

init/1函数则是真正的监督员核心所在。记住不要将任何程序逻辑放到监督员中,它必须尽可能简单并且不会出错,你应用程序的命运就掌握在这里。让我们将它进行分解:

RestartStrategy = one_for_one表示对于每个挂掉的进程只需要重启该进程而不影响其他的进程。还有其他两种重启策略,one_for_llrest_for_all,你可以在监督员文档中了解到它们的信息。MaxRestarts表示在MaxTimeBetweenRestarts秒内随多允许的重启次数。

这个监督员只有一个ChildSpec表示它仅会启动一个进程。按顺序看看数据结构,ls_server是要被启动的进程的名字,{ls_server, start_link, []}是启动这个子进程要调用的模块函数和参数,permanent表示这个进程除了重启决不允许挂掉(还有其他的选项,参考erlang.org上关于监督机制的文档),1000是在野蛮杀死进程之前等待进程结束的时间,worker表示该子进程并非一个监督员,[ls_server]是该进程所依赖的模块的列表。

监督员回调模块函数init/1返回所有这些信息,同时监督员行为会负责按指定顺序启动所有子进程。

现在进入ls_server.erl文件。这个文件展示了gen_server行为。这表示它将遵循这种行为的规定导出以下回调函数:

另外我们还要创建一下外部函数:

我们开始吧。为所有的外部函数编写文档非常重要。OTP基本构建系统已经通过“make docs”来支持“edoc”,这个指令可以自动遍历并编译应用的所有文档并存入应用目录中的doc目录中(location_server/doc)。回到我们的函数:

这个函数启动服务器,并在本地注册为?SERVER(在源代码里找找-define(SERVER,?MODULE).看)并且指示gen_server行为当前模块导出了必要的回调函数。

stop很简单,不过注意gen_server:cast/2的使用。这个函数基本和使用“!”操作符类似。他用于给一个通用服务器发送一个异步消息。这里我们给标识为?SERVER的本地注册进程发送原子stop

store_location/3是一个非常简单的函数,但是也有不少地方需要引起我们注意。注意我们使用了守护表达式(guard expression),is_atom/1以及is_list/1。要确保传递进来的变量的类型是正确的。这是个不信任边界外事物的例子:在这里检查过之后就不用再检查了。你有没有注意到,这个函数的名字和我们发送的消息中的“标签”是一样的?没有么?我来解释一下:

上面这行中的store_location就是我们的“标签”,它可以将这个消息和其他消息区分开来。{Id, Location}是我们所发送的在标签store_location之下的有效载荷。在gen_server中所发送的消息应该遵循“{标签, 值}”的约定。其实在上面这行还有一个更重要的东西需要注意。注意这个函数的第一个参数是{?SERVER, Node}而不仅仅是我们在前面一个cast中看到的?SERVER。出现这个情况是因为我们想要能够与在网络中任何节点上进程联系而不仅仅是我们自己的进程。要给远程注册的消息发送消息,就可以使用这种语法{注册名,节点}其中节点可以是在网络上的任何节点包括本地节点。

这里要注意的东西是我们没有再使用守护条件——我们不关心——这个函数不会改变服务器的状态,如果它挂了对我们也没什么影响。最重要需要注意的地方是gen_server:call/2的使用。这个函数是向一个gen_server发起一个同步(阻塞的)调用。

现在我们完成了所有的外部程序,让我们充实服务器吧。对于新接触Erlang的人来说,有一个事情很容易混淆,就是:这些外部函数是在调用进程的进程空间中被调用的,虽然他们生成的消息是是被同一个模块中的语句接收的,但是这些语句却是存在于一个完全不同的进程中的。有可能的话,要使用模块来封装服务器协议,这点很重要。

在进程能被外界访问前,由init函数设置进程状态。这里状态储存于一个字典dict中。这个字典可以用于储存形如{Id, {Location, ListOfSubscribers}}这样的键值对。init函数还记录了ls_server进程启动的信息。日志文件的位置和管理将在本教程的第二部分(讲述如何为应用创建一个发布)进行阐述。

这样服务器和location_server应用的所有功能就完成了,但是我们还有最后一步要做。显然,直接让应用程序的客户直接调用ls_server:store_location这种非常笨拙和丑陋。我们所需要的就是针对客户良好定义的外部API。放这些API的地方就是和我们的应用程序名称一样的那样模块——我们的应用行为模块,location_server.erl

毫无疑问必须得导出这些函数。

现在我们完成了location_server的编码,剩下要做的就是到应用程序的目录中,输入“make”和“make docs”,并等待代码和文档编译成功。

注意应用程序根目录下出现了两个新目录“doc”和“ebin”。location_server/doc包含了前面写的代码中的文档。所有的外部函数都会出现在这些文档中。让我们稍微深入研究一下ebin目录。

ebin包含了每个.erl源文件对应的编译好的.beam字节码文件。ebin目录也有一个.app文件和一个.appup文件。.appup文件用于处理升级发布,这超出了本教程的范围。.app文件对于创建和安装OTP发布是必不可少的,所以我们再更深入地研究一下它。

以上代码中的注释已经基本完全解释清楚了。这个.app文件是动态创建由make进程从location_server/src/location_server.app.src文件中自动创建的。这表示你应该基本无须手动更改这个文件。

恭喜你已经创建了首个漂亮的OTP应用。现在让我们进入本教程的第二部分并创建一个发布,一边能运行和部署我们的OTP应用。

第二部分:打造一个发布

在otp目录中:

该目录包含了一个vsn.mk文件,用于储存本发布的版本字符串。还有一个yaws.conf.src文件用于生成yaws.conf文件,如果你选择在发布中包含yaws服务器。目前我们将注意力集中在location_server_rel.config.srclocation_server.rel.src这两个文件上。.config.src文件由make翻译为.config文件。这个文件包含了发布中的必须的各种不同应用程序(诸如截断翻转日志)的所有配置。.rel.src文件则会被make转成.rel文件。.rel文件又会用于创建.script.boot文件,这些文件将告诉Erlang系统当启动的时候要载入哪些代码。下面是我们的.rel.src文件。

这个文件指定了我们的发布的名字,并且跟了一个版本串,当进行make的时候会将vsn.mk文件中的版本串填入这个地方。接下来指定了我们的发布中要包含erts,以及kernel, stdlib, sasl, fslib, gas最后我们自己的应用location_server。其中fslib和gas这两个应用为我们的发布提供了配置和日志翻转截断,关于他们的详细信息可以到erlware.org找到完整的文档。另外你还可以进入otp/lib/fslib或者otp/lib/gas目录中并运行“make docs”就可以在本地生成所有的文档了。我们运行了make之后,这个.rel.src文件就会被转换成实际的.rel文件,它包含了.rel.src文件中出现的所有应用名字结合了对应的版本号。关于.rel.src结构的更多信息,可以看一下fslib文件fs_boot_smithe的文档。

是时候该编译并运行我们的发布了。

生成了不少文件还有两个目录。我们看到了一个.rel文件,一个.script文件和一个.boot文件。每个文件的内容都看看,.boot文件就不用看了因为它只是.script文件的二进制版本。注意刚刚创建的两个目录:locallocation_server_rellocation_server_rel是一个用于安装和应用程序生产发布的一个场景区域,更多信息请参考erlware.org上的OTP Base中的文档。接下来将焦点放在这里的local目录。这个目录让我们可以交互式地运行我们的发布。如果我们要将其作为一个守护进程运行,只需要简单地调用local/location_server_rel.sh,并加上-detached标志就行了。现在转到local目录中并直接运行我们的应用程序。

err_log文件包含了所有通过saslerror_loggerinfo_msgerror_msg打印的日志。其他日志,如在sasl_log中你可以看到所有的sasl错误报告。如果发布的程序崩溃了,首先要查看sasl_log。默认情况下构建系统已经加入了对fs_elwrap_h(通过G.A.S应用,已作为这个发布的一部分)的应用以便截断和翻转日志文件。这可以防止日志文件无限制地增长。如果要配置或者移出这个功能,可以配置“local”目录下的location_server.config文件。

mod_specs告诉GAS应用对于某个发布,要启动哪些服务以及如何启动。后面的实际的配置元组,elwrap将根据它来工作。首先是err_log的位置,后面跟着wrap specs,指定了sasl日志和错误日志的单个文件进行翻转之前最大可以达到多少,以及指定的日志记录器保存多少个文件。配置的最后一位指定错误记录器是否要打印到屏幕。

第三部分:测试我们所构建的东西

若要测试我们的项目,首先要启动三个单独的Erlang命名节点。打开三个终端窗口并启动三个节点,其中第一个是位置服务器location server,由local/location_server.sh启动,第二个由erl -name a -pz <location_server/ebin的路径>启动,第三个erl -name b -pz <location_server/ebin的路径>。其中-pz参数告诉解释起将指定的路径添加到代码加载器的搜索路径中。这表示我们可以在节点a和b上使用location_server的接口了。(这种代码加载没有文件。)这样我们启动了三个节点a@machinename.comb@machinename.com以及<username>_location_server@machinename.com。要连接节点,从其他两个节点上执行nat_adm:ping('a@machinename.com')。完了后,在任意一个节点上运行nodes(),应该得到在我们的云中的其他节点的列表。在节点上运行以下命令。确保以下例子中的节点正确,因为这些命令不都是运行于同一个节点上的。

上面所发生的内容是:我们通过location_server_rel.sh启动脚本将位置服务器作为节点’martinjlogan_location_server_rel@core.martinjlogan.com’启动了。然后我们启动了两个节点a@core.martinjlogan.com和b@core.martinjlogan.com。然后执行了以下步骤:

  1. 在“a”上的第1个提示,我们存储了键“martin”和值“at work”
  2. 在“b”上的第1个提示,我们发出了要更改键“martin”的警告
  3. 在“b”上的第2个提示,我们等待接受信息并打印
  4. 在“a”上的第2个提示,我们又存储了个位置,将“martin”的位置改成了“at home”并让“b”退出了接受语句并打印了该消息:Msg: {location_changed, {martin, "at home"}}.订阅消息也成功了!
  5. 最后在“a”上的第3个地址,我们验证了fetch_location正常工作。

你可以从这里下载本教程的全部代码,祝你用Erlang愉快编程。

发表评论

电子邮件地址不会被公开。 必填项已用*标注