使用Erlang的OTP框架创建应用

时间:2013-3-9     作者:smarteng     分类: 编程


概述

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机器上下好这个代码之后,将构建系统解压出来。

> tar -xzfv otp_base-R1.tgz
> cd otp
> ls
build  lib  licence.txt  Makefile  README  release tools

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

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

> cd tools/utilities
> ./appgen location_server ls
> cd -

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

> cd lib/location_server
> ls
include  Makefile  src  vsn.mk

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

> cd src
> ls
location_server.app.src    location_server.erl  ls_sup.erl
location_server.appup.src  ls_server.erl        Makefile

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

%%%-------------------------------------------------------------------
%%% Author  : martinjlogan
%%% @doc The entry point into our application.
%%% @end
%%%-------------------------------------------------------------------
-module(location_server).

-behaviour(application).
%%--------------------------------------------------------------------
%% Include files
%%--------------------------------------------------------------------
-include("location_server.hrl").

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
     start/2,
     shutdown/0,
     stop/1
     ]).

%%====================================================================
%% External functions
%%====================================================================
%%--------------------------------------------------------------------
%% @doc The starting point for an erlang application.
%% @spec start(Type, StartArgs) -> {ok, Pid} | {ok, Pid, State} | {error, Reason}
%% @end
%%--------------------------------------------------------------------
start(Type, StartArgs) ->
    case ls_sup:start_link(StartArgs) of
    {ok, Pid} ->
        {ok, Pid};
    Error ->
        Error
    end.

%%--------------------------------------------------------------------
%% @doc Called to shudown the auction_server application.
%% @spec shutdown() -> ok
%% @end
%%--------------------------------------------------------------------
shutdown() ->
    application:stop(location_server).

%%====================================================================
%% Internal functions
%%====================================================================

%%--------------------------------------------------------------------
%% Called upon the termination of an application.
%%--------------------------------------------------------------------
stop(State) ->
    ok.

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上的关于监督机制的设计原理。

%%%-------------------------------------------------------------------
%%% Author  : martinjlogan
%%% @doc The top level supervisor for our application.
%%% @end
%%%-------------------------------------------------------------------
-module(ls_sup).

-behaviour(supervisor).

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([start_link/1]).

%%--------------------------------------------------------------------
%% Internal exports
%%--------------------------------------------------------------------
-export([init/1]).

%%--------------------------------------------------------------------
%% Macros
%%--------------------------------------------------------------------
-define(SERVER, ?MODULE).

%%====================================================================
%% External functions
%%====================================================================
%%--------------------------------------------------------------------
%% @doc Starts the supervisor.
%% @spec start_link(StartArgs) -> {ok, pid()} | Error
%% @end
%%--------------------------------------------------------------------
start_link(StartArgs) ->
    supervisor:start_link({local, ?SERVER}, ?MODULE, []).

%%====================================================================
%% Server functions
%%====================================================================
%%--------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok,  {SupFlags,  [ChildSpec]}} |
%%          ignore                          |
%%          {error, Reason}
%%--------------------------------------------------------------------
init([]) ->
    RestartStrategy    = one_for_one,
    MaxRestarts        = 1000,
    MaxTimeBetRestarts = 3600,

    SupFlags = {RestartStrategy, MaxRestarts, MaxTimeBetRestarts},

    ChildSpecs =
    [
     {ls_server,
      {ls_server, start_link, []},
      permanent,
      1000,
      worker,
      [ls_server]}
     ],
    {ok,{SupFlags, ChildSpecs}}.

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

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

RestartStrategy    = one_for_one,
MaxRestarts        = 1000,
MaxTimeBetRestarts = 3600,

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

{ls_server,
  {ls_server, start_link, []},
   permanent,
   1000,
   worker,
   [ls_server]}

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

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

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

%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

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

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
     start_link/0,
     stop/0,
     store_location/3,
     fetch_location/2,
     alert_to_change/2
     ]).

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

%%--------------------------------------------------------------------
%% @doc Starts the server.
%% @spec start_link() -> {ok, pid()} | {error, Reason}
%% @end
%%--------------------------------------------------------------------
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

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

%%--------------------------------------------------------------------
%% @doc Stops the server.
%% @spec stop() -> ok
%% @end
%%--------------------------------------------------------------------
stop() ->
    gen_server:cast(?SERVER, stop).

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

%%--------------------------------------------------------------------
%% @doc Stores the location of a client.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%%  Location = string()
%% </pre>
%% @spec store_location(Node, Id, Location) -> ok
%% @end
%%--------------------------------------------------------------------
store_location(Node, Id, Location) when is_atom(Id), is_list(Location) ->
    gen_server:cast({?SERVER, Node}, {store_location, {Id, Location}}).

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

gen_server:cast({?SERVER, Node}, {store_location, {Id, Location}}).

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

%%--------------------------------------------------------------------
%% @doc Fetches the location of a client by its Id.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec fetch_location(Node, Id) -> {ok, Location} | {error, Reason} | EXIT
%% @end
%%--------------------------------------------------------------------
fetch_location(Node, Id) ->
    gen_server:call({?SERVER, Node}, {fetch_location, Id}).

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

%%--------------------------------------------------------------------
%% @doc Subscribes the caller to notifications of location change for
%% the given Id. When a location changes the caller of this function
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec alert_to_change(Node, Id) -> bool() | EXIT
%% @end
%%--------------------------------------------------------------------
alert_to_change(Node, Id) ->
    gen_server:call({?SERVER, Node}, {alert_to_change, {Id, self()}}).

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

%%====================================================================
%% Server functions
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init/1
%% Description: Initiates the server
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
%%--------------------------------------------------------------------
init([]) ->
    error_logger:info_msg("ls_server:init/1 starting~n", []),
    {ok, dict:new()}.

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

%%--------------------------------------------------------------------
%% Function: handle_call/3
%% Description: Handling call messages
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_call({fetch_location, Id}, From, State) ->
    case dict:find(Id, State) of
        {ok, {Location, ListOfSubscribers}} -> {reply, {ok, Location}, State};
        error                               -> {reply, {error, no_such_id}, State}
    end;
handle_call({alert_to_change, {Id, Subscriber}}, From, State) ->
    case dict:find(Id, State) of
        {ok, {Location, ListOfSubscribers}} ->
            NewListOfSubscribers = [Subscriber|lists:delete(Subscriber, ListOfSubscribers)],
            NewState             = dict:store(Id, {Location, NewListOfSubscribers}, State),
            {reply, true, NewState};
        error ->
            {reply, false, State}
    end.
%%--------------------------------------------------------------------
%% Function: handle_cast/2
%% Description: Handling cast messages
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_cast(stop, State) ->
    {stop, normal, State};
handle_cast({store_location, {Id, Location}}, State) -> % Remember that State is a dict()
    NewState =
        case dict:find(Id, State) of
            {ok, {OldLocation, ListOfSubscribers}} ->
                lists:foreach(fun(Subscriber) -> Subscriber ! {location_changed, {Id, Location}} end, ListOfSubscribers),
                dict:store(Id, {Location, ListOfSubscribers}, State);
            error ->
                dict:store(Id, {Location, []}, State)
        end,
    {noreply, NewState}.

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

%%--------------------------------------------------------------------
%% @doc Stores the location of a client.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%%  Location = string()
%% </pre>
%% @spec store_location(Node, Id, Location) -> ok
%% @end
%%--------------------------------------------------------------------
store_location(Node, Id, Location) ->
    ls_server:store_location(Node, Id, Location).

%%--------------------------------------------------------------------
%% @doc Fetches the location of a client by its Id.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec fetch_location(Node, Id) -> {ok, Location} | {error, Reason} | EXIT
%% @end
%%--------------------------------------------------------------------
fetch_location(Node, Id) ->
    ls_server:fetch_location(Node, Id).

%%--------------------------------------------------------------------
%% @doc Subscribes the caller to notifications of location change for
%% the given Id. When a location changes the caller of this function
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec alert_to_change(Node, Id) -> bool() | EXIT
%% @end
%%--------------------------------------------------------------------
alert_to_change(Node, Id) ->
    ls_server:alert_to_change(Node, Id).

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

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
      start/2,
      shutdown/0,
      store_location/3,
      fetch_location/2,
      alert_to_change/2,
      stop/1
     ]).

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

> cd ..
> make
> make docs
> ls
doc  ebin  include  Makefile  src  vsn.mk

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

> cd ebin
> ls
location_server.app    location_server.beam  ls_sup.beam
location_server.appup  ls_server.beam

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

{application, location_server,
 [
  % 本应用的简介
  {description, "An Erlang Application."},

  % 本应用的版本
  {vsn, "1.0"},

  % 本应用用到的所有模块
  {modules,
   [
    location_server, ls_server, ls_sup
   ]},

  % 本应用到要注册的所有名称。可以忽略。
  {registered, []},

  % 在本应用启动之前要启动的应用。可以忽略。
  % 除非你理解该内容否则不要动它,留给发布中的.rel来处理。
  {applications,
   [
    kernel,
    stdlib
   ]},

  % OTP应用加载器会载入——但不会启动——所包含的应用。这也可以被忽略。
  % 要载入但不启动某个应用,在.rel文件中包含它的名字并跟上原子'none'。
  {included_applications, []},

  % 配置参数,类似于在命令行中指定的配置文件中的参数。
  % 可以通过gas:get_env获取。
  {env, []},

  % 用于启动本应用的模块和参数。
  {mod, {location_server, []}}
 ]
}.

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

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

第二部分:打造一个发布

在otp目录中:

> cd release/location_server_rel
> ls
location_server_rel.config.src  Makefile  yaws.conf.src
location_server_rel.rel.src     vsn.mk

该目录包含了一个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文件。

{release,
 {"location_server_rel", "%REL_VSN%"},
 erts,
 [
  kernel,
  stdlib,
  sasl,
  fslib,
  gas,
  location_server
 ]
}.

这个文件指定了我们的发布的名字,并且跟了一个版本串,当进行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的文档。

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

> make
> ls
local                           location_server_rel.rel          Makefile
location_server_rel             location_server_rel.rel.src      vsn.mk
location_server_rel.boot        location_server_rel.rel.src.tmp  yaws.conf.src
location_server_rel.config.src  location_server_rel.script

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

> cd local
> ./location_server_rel.sh
Erlang (BEAM) emulator version 5.4.12 [source] [hipe]

INFO REPORT 14-Oct-2006::00:18:33
gas_sup:init

INFO REPORT 14-Oct-2006::00:18:33
ls_server:init/1 starting

Eshell V5.4.12  (abort with ^G)
(martinjlogan_location_server_rel@core.martinjlogan.com)1>
We have successfully started the location_server release. Can you see where the log message we placed in ls_server to indicate its having reached the init/1 function has shown up on the console?  That is not the only place our message shows up:

> ls log/1.0/
err_log  sasl_log

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

{gas,
  [
   % Tell GAS to start elwrap.
   {mod_specs, [{elwrap, {fs_elwrap_h, start_link}}]},

   % elwrap config.
   {err_log, "/home/martinjlogan/work/otp/release/location_server_rel/local/log/1.0/err_log"},
   {err_log_wrap_info, {{err,5000000,10},{sasl,5000000,10}}},
   {err_log_tty, true} % Log to the screen
   ]},

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(),应该得到在我们的云中的其他节点的列表。在节点上运行以下命令。确保以下例子中的节点正确,因为这些命令不都是运行于同一个节点上的。

(a@core.martinjlogan.com)1> location_server:store_location('martinjlogan_location_server_rel@core.martinjlogan.com', martin, "at work").
ok

(b@core.martinjlogan.com)1> location_server:alert_to_change('martinjlogan_location_server_rel@core.martinjlogan.com', martin).
true
(b@core.martinjlogan.com)2> receive Msg -> io:format("Msg: ~p~n", [Msg]) end.

(a@core.martinjlogan.com)2> location_server:store_location('martinjlogan_location_server_rel@core.martinjlogan.com', martin, "at home").
ok

(b@core.martinjlogan.com)2>
Msg: {location_changed,{martin,"at home"}}
ok

(a@core.martinjlogan.com)3> location_server:fetch_location('martinjlogan_location_server_rel@core.martinjlogan.com', martin).
{ok,"at home"}

上面所发生的内容是:我们通过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愉快编程。