Erlang OTP 新手教程

真・懒写于

译自 https://github.com/hcvst/erlang-otp-tutorial.

这是一篇写给 Erlang 新手的 Erlang/OTP 教程。若你已熟悉 Erlang 语法,知道如何编译模块,有探索 OTP 的好奇心,想在你自己的应用程序中发挥它的能力,那么本教程是为你量身定制的。我们以一个非 OTP 的服务器和监督器(supervisor)开始,看看它们有何不足,然后改进为正确的 OTP 应用程序。

本教程会涉及:

不过我们先来几发开场白。

熟悉 Erlang 语法,知道 Erlang 模块,怎样编译,了解一些诸如递归和单次赋值变量的函数式编程概念了吗?答案是肯定的?好极了,来学点 OTP 吧。你会发现起步并不难,但让我先简单说说我的 Erlang 体验吧。

我跟你还在同一起跑线呢,不同之处可能只在于,我已经开始阅读 Manning 的 Erlang & OTP in action 这本书(中译版《Erlang/OTP 并发编程实战》)。毫无夸张地说,这是一本我最爱的技术书籍之一,里面容易吸收的知识随处可见。可以的话买一本。

同等水平的人教同等水平的人,我觉得就是心理学课程所说的“脚手架”。写这篇教程,一是加固我对 OTP 的理解,二是当作你学 OTP 的助推器。一起学习,如果觉得哪不对请留言。

OTP

OTP 表示 Open Telecom Platform(开放通信平台),虽然叫这名,但它并不必一定要跟通信应用程序搭上关系。把 OTP 当作一个 Erlang 平台的框架,有了它可以写出健壮和高可用性的应用程序。

特别是,用这几个概念来理解 OTP 你可能会觉得更合理:

大致可以类比面向对象中的:

当然,这只是表面的相似而已。已有的知识对我们理解新概念可能会有所帮助,但苹果和桔子必须是要分清的啊。

非 OTP 的服务器

开场白叽歪完了,让在同一水平的我们来写一个非 OTP 的简易服务器吧。这是代码:

%%%---------------------------------------------------------------------------
%%% @doc 非 OTP 的建议服务器
%%% @author Hans Christian v. Stockhausen
%%% @end
%%%---------------------------------------------------------------------------

-module(no_otp).                     % 模块名(跟 erl 文件名一致,
                                     % 例如 no_otp.erl)

%% API
-export([                            % 需暴露出去的函数,亦即 API
  start/0,                           % - 启动新的服务器进程
  stop/0,                            % - 关停服务器
  say_hello/0,                       % - 打印 "Hello" 到标准输出
  get_count/0                        % - 回复轮询主体的调用次数
  ]).

%% 回调
-export([init/0]).                   % 暴露出来以便在 start/0 中创建
                                     % 单独列出来提示客户端不要使用

-define(SERVER, ?MODULE).            % SERVER 宏是 MODULE 的别名,
                                     % 会展开成 'no_otp'

-record(state, {count}).             % 服务器状态的记录。
                                     % 有点随心所欲,
                                     % 用来追踪主要轮询的频度 - 见 loop/1


%=============================================================================
% API - 客户端跟服务器交互的函数
%=============================================================================

start() ->                           % 创建调用 init/0 的新进程
  spawn(?MODULE, init, []).          % 返回新进程的 PID

stop() ->                            % 发送 stop 原子到进程
  ?SERVER ! stop,                    % 吩咐服务器关闭
  ok.                                % 并返回 ok 给调用者

say_hello() ->                       % 发送 say_hello 原子到进程
  ?SERVER ! say_hello,               % 以打印 "Hello" 到标准输出
  ok.

get_count() ->                       % 给调用者发送 Pid 和 get_count 原子
  ?SERVER ! {self(), get_count},     % 请求 count 的值
  receive                            % 等待匹配的响应并返回
    {count, Value} -> Value          % Value 给调用者
  end.


%=============================================================================
% 回调函数 - 客户端不要直接用
%=============================================================================

init() ->                            % 会被 start/0 触发
  register(?SERVER, self()),         % 在 no_otp 注册新进程 PID
  loop(#state{count=0}).             % 启动服务器的主要轮询

%=============================================================================
% 内部函数 - 注意,这些函数不暴露
%=============================================================================

loop(#state{count=Count}) ->         % 服务器的主要轮询
  receive Msg ->                     % API 函数发送消息时
    case Msg of                      % 检查里面包含的原子
      stop ->                        % 如果是 'stop' 原子
        exit(normal);                %   退出进程
      say_hello ->                   % 如果是 'say_hello' 原子
        io:format("Hello~n");        %   写 "Hello" 到标准输出
      {From, get_count} ->           % 如果 Msg 是元组 {Pid(), get_count}
        From ! {count, Count}        % 以标记元组 {count, Value} 的形式
    end                              %   回复当前的计数值
  end,
  loop(#state{count = Count + 1}).   % 以已更新的状态递归

试验

这是我的 shell 纪录。诚然,你想知道 get_count/0 意义何在。没有什么目的,只是想演示如何实现一个同步函数。

Eshell V9.2  (abort with ^G)
1> c(no_otp).
{ok,no_otp}
2> no_otp:start().
<0.70.0>
3> no_otp:get_count().
0
4> no_otp:say_hello().
Hello
ok
5> no_otp:get_count().
2
6>

对于这个语境下服务器这个术语,一开始我很困惑。这怎么就成了一个服务器啦?细思量,这跟分发此页面到你浏览器的 HTTP 服务器异曲同工。除非停掉否则会一直运行,它遵守某种协议(只不过是 Erlang 的内部信息协议而非 HTTP 协议),对输入回应消息或执行副作用的操作。这就是个服务器,没错。

我不打算讨论代码细节,注释已详述。我只想简单说说代码结构:

有何问题?

真没什么毛病。有了 Erlang 它就能一直跑一直跑一直跑。我们有足够的信心认为它是正确无误的实现,毕竟是个非常简单的服务器。同时,依托于 API,发送格式不对的数据,客户端也不会意外崩溃——哦,能打包票吗?

照前面的步骤启动服务器,然后在 shell 终端键入 no_otp ! crash.

服务器注册到 no_otp 下(参见init/0),因此我们可以随心所欲给它发送乱七八糟的消息,但代码里并没有匹配 crash 的从句。哦哟!真的 crash (崩溃) 了。

怎么办?我们可以加上一条匹配任何消息,不对消息做任何回应的从句就好了。然而,我们知道这跟处理异常时不区分类型一刀切的捕获一样是有害的,臭虫可能会遁地无形。

Erlang 信仰的哲学是“鼓励崩溃”。别嫌我啰嗦,这段话要过目几遍。让我给你——Erlang 程序员——划下重点:别防御,不要囊括所有状况,专注规范,专注代码需求并保持精炼紧凑。如果代码出错,虽会崩溃但可修复。因客户端代码出错的崩溃,随它崩,在保存良好的状态中重启,而不是不惜一切代价让它一直运行。不要为了解决客户端的烂摊子而去编写更复杂的代码,让客户端去修复,了解了么?

监督

好吧,随它崩,但崩溃后怎么样自动重启服务器呢?答案是编写另一个进程,用来监视服务器,一旦发现服务器挂了就重启。这个进程就叫做监督器,接下来是非 OTP 版本的代码。

非 OTP 监督器

开始前先看看以下带注解的 Erlang shell 纪录,了解下如何练习。函数 whereis/0 返回注册到某个名字上的进程的 Pid。可以看到 sup:start_server/0 已经启动了 no_otp 服务器,因为它显示了进程标识 <0.72.0>。自动重启看起来也跟预期的一样没有什么问题。

Eshell V9.2  (abort with ^G)
1> c(sup).                                   % 编译监督器模块
{ok,sup}
2> sup:start_server().                       % 启动监督器
<0.70.0>                                     % 得到进程标识 ..70..
3> whereis(no_otp).                          % 看看 `no_otp` 进程在哪
<0.72.0>                                     % 得到 ..72..
4> no_otp:say_hello().                       % 试试 API
Hello                                        % 没毛病
ok
5> no_otp:get_count().                       % 也没毛病
1
6> no_otp ! crash.                           % 是时候让它崩溃了
crash

=ERROR REPORT===
Error in process <0.40.0> with exit value:
  {{case_clause,crash},[{no_otp,loop,1}]}   % 如其所愿,发生 case_clause 错误

7> no_otp:get_count().                      % 但新进程已启动
0                                           % 证据在此
8> no_otp:stop().                           % 正常的方式停止
ok
9> whereis(no_otp).                         % 检查新的实例
undefined                                   % 然而并没有。正确!
10>

这是我们的监督器代码:

%%%----------------------------------------------------------------------------
%%% @doc 非 OTP 监督器
%%% @author Hans Christian v. Stockhausen
%%% @end
%%%----------------------------------------------------------------------------

-module(sup).

%% API
-export([start_server/0]).

%% Callbacks
-export([supervise/0]).

%%=============================================================================
%% API
%%=============================================================================

start_server() ->
  spawn(?MODULE, supervise, []).

%%=============================================================================
%% Callbacks
%%=============================================================================

supervise() ->
  process_flag(trap_exit, true),    % 链接进程挂了之后,
                                    % 不要崩溃,而是发一条消息
  Pid = no_otp:start(),             % 启动服务器
  link(Pid),                        % 并链接
  receive {'EXIT', Pid, Reason} ->  % 等待通知进程关闭的消息
    case Reason of                  %
      normal -> ok;                 % 如果理由是 'normal',无视
      _Other -> start_server()      % 否则重新来过
   end
  end.

在开场白提到,我也是个 Erlang 菜鸟。我对上面的监督器代码并没有多少信心,这正是问题所在。用 “Erlang 流” 来写应用程序,要拥抱“鼓励崩溃”的哲学,必须以服务器,监督器,监督器的监督器为单位来组织代码。大量结构性,重复的样板代码,给臭虫的引入提供了充足的机会。服务器和监督器层级的概念不难理解,但能不能也容易做对呢?

OTP 打救你

很幸运,有人已经解决了大难题。OTP 这个特制的框架,有助于组合服务器、监督器和其他组件,我们稍后会一探究竟。OTP 开发者提供了一个坚如磐石,久经沙场,谁都知道能用的框架,我们可以接入自己的,可能是脆弱的代码。

到用 OTP 行为重写服务器和监督器的时候了。我们先看看两个有所裨益的工具。

Rebar 和 Emacs

有了 OTP,不必重做监督层级的轮子,但任然需要手工输入一定量的代码,填充样板很快就会厌恶。而且,要养成用 Edoc 注释写文档的好习惯,还需更多的重复输入。因此,我们需要一个小帮手。

Rebar 是 Erlang 的构建工具。我只涉及必要部分。

运行以下命令,安装 rebar 并创建 "hello" 应用的脚手架。

mkdir hello
cd hello
wget https://raw.github.com/wiki/rebar/rebar/rebar
chmod u+x rebar
./rebar create-app appid=hello

新创建的 src 目录中生成以下文件:

缺少的是包含服务器的文件,让 rebar 来生成一个。

./rebar create template=simplesrv srvid=hello_server

不想用 rebar,你也可以手工创建这些文件,并放到 src 目录下。src 命名是个惯例,稍后我会解释。

Emacs 提供 Erlang 模式,真的能加速“编辑-编译-运行”交替的开发方式,还提供了模版。我常用 vi 来完成日常编辑,但总想试试 Emacs。Erlang 模式正是好机会,还没用 Emacs 的话我建议你也试试。

(译注:省略部分关于 Emacs 的例子,无关大体)

OTP 文件夹

使用以下命令

mkdir -p my_app/{doc,ebin,priv,include,src,test}

可以创建 OTP 文件夹结构,也可用 rebar 帮忙。只有 srcebin 是必须的。

OTP gen_server

如其名,gen_server 行为帮我们写通用服务器。这是用 OTP 行为重写的服务器。

%%%----------------------------------------------------------------------------
%%% @doc OTP gen_server 例子
%%% @author Hans Christian v. Stockhausen
%%% @end
%%%----------------------------------------------------------------------------

-module(hello_server).         % 这部分没什么新鲜的,
                               %  除了下一行,我们会告诉编译器
-behaviour(gen_server).        %  此模块实现 gen_server 行为。
                               %  编译器会警告我们,
-define(SERVER, ?MODULE).      %  如果不提供行为要求的回调函数。
                               %  它通过调用
-record(state, {count}).       %  gen_server:behaviour_info(callbacks)
                               %  能找到响应函数。试试看。
%%-----------------------------------------------------------------------------
%% API 暴露函数
%%-----------------------------------------------------------------------------

-export([                      % 跟之前定义 API 函数一样
  start_link/0,                % - 启动并链接进程,一步到位
  stop/0,                      % - 关停
  say_hello/0,                 % - 答应 "Hello" 到标准输出
  get_count/0]).               % - 返回计数状态

%% ---------------------------------------------------------------------------
%% gen_server 暴露函数
%% ---------------------------------------------------------------------------

-export([                      % 行为回调函数
  init/1,                      % - 初始化进程
  handle_call/3,               % - 处理同步调用(有回复)
  handle_cast/2,               % - 处理异步调用(无回复)
  handle_info/2,               % - 处理带外消息(以 ! 发送)
  terminate/2,                 % - 在关闭时调用
  code_change/3]).             % - 处理代码变更

%% ---------------------------------------------------------------------------
%% API 函数定义
%% ---------------------------------------------------------------------------

start_link() ->                % start_link 以一步原子操作创建和链接进程
    gen_server:start_link(     % 参数:
      {local, ?SERVER},        %  - 本地进程的注册名
      ?MODULE,                 %  - init/1 回调函数所在模块
      [],                      %  - init/1 所需参数
      []).                     %  - start_link 的其他选项

stop() ->                      % 注意我们不用 ! 了
    gen_server:cast(           %  用 cast 异步发送消息到注册的名字
      ?SERVER,                 %  这是异步的,
      stop).                   %  我们不想要回复

say_hello() ->                 % 跟 stop 差不多,
    gen_server:cast(           %  只不过是发送 say_hello 原子。
      ?SERVER,                 %  也不想要回应,
      say_hello).              %  只对副作用感兴趣。

get_count() ->                 % 而在此,我们希望得到回应,
    gen_server:call(           %  因此使用 call 同步触发服务器。
      ?SERVER,                 %  这个调用是阻塞的,直到得到回应。
      get_count).              %  gen_server:call/2 帮我们
                               %  隐藏 send/receive 逻辑。漂亮。
%% ---------------------------------------------------------------------------
%% gen_server 函数定义
%% ---------------------------------------------------------------------------

init([]) ->                    % 这是行为的回调函数。
    {ok, #state{count=0}}.     % init/1 在 gen_server:start_link/4 中调用,
                               % 并初始化状态

handle_call(get_count, _From, #state{count=Count}) ->
    {reply,
     Count,                    % 同步回复 Count
     #state{count=Count+1}     % 同时更新状态
    }.

handle_cast(stop, State) ->    % 这是第一个 handle_case 从句,处理 stop 原子。
    {stop,                     % 我们吩咐 gen_server 正常停止
     normal,                   % 并返回当前的 State,不要作任何改变
     State                     %
    };                         % 注意这有个分号

handle_cast(say_hello,
            State) ->          % 处理 say_hello 原子
    io:format("Hello~n"),      % 打印 "Hello"
    {noreply,                  % 这也是异步的,所以是 noreply
    #state{count=
      State#state.count+1}
    }.                         % 同时更新状态

handle_info(Info, State) ->    % handle_info 处理带外消息,
    error_logger:info_msg("~p~n", [Info]),
    {noreply, State}.          % 比如不是经由 cast 或 call 发出的消息。
                               % 在此我们只是简单记录下消息

terminate(_Reason, _State) ->  % 这是由 gen_server 容器在关闭时触发的。
    error_logger:info_msg("terminating~n"),
    ok.                        % 我们记录下来,并回复 ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.               % 升或降级的发布时更新内部状态

%% ------------------------------------------------------------------
%% 内部函数定义
%% ------------------------------------------------------------------

% 还没有呢。

进入应用程序目录(前面提到的 src 的上级)并运行 ./rebar compile,或者,你还没有用 rebar 的话,运行 erlc -o ebin scr/*.erl,就可以编译这个 OTP 服务器了。rebar 会自动创建 ebin 目录,用 erlc 请先自行创建该目录。

来测试下这个 OTP 服务器,这是输出:

$> erl -pa ebin                # 以 -pa (path add,加入路径) ebin 为参启动 erl
1> hello_server:start_link().  % 启动服务器
{ok,<0.34.0>}
2> hello_server:say_hello().   % 试试 say_hello/0 这个 API 函数
Hello
ok
3> hello_server:get_count().   % 观察服务器如何追踪状态
1
4> hello_server:get_count().
2
5> hello_server ! stop.        % 发送带外信息

=INFO REPORT===                % error_logger 的输出
stop
stop
6> hello_server:stop().        % 调用 stop/0 API 函数

=INFO REPORT===                % 这是 terminate 输出的信息
terminating
ok
7> whereis(hello_server).      % 进程已不存在
undefined
8>

从非 OTP 版本到 OTP 的 gen_server,予我们何益?

当然不止这些。如下记录所示,开启 SASL (System Application Support Libraries,系统程序支持库),然后强行崩溃,会收到非常有用的错误报告,这是非 OTP 版本所不具备的。

1> application:start(sasl).                    % 开启 SASL
ok
2>
=PROGRESS REPORT====                           % 为省版面我删了启动消息
          application: sasl                    % 只保留了最后一条
          started_at: nonode@nohost            % SASL 起来了。

2> hello_server:start_link().                  % 启动服务器
{ok,<0.44.0>}
3> whereis(hello_server).                      % 检查是否已注册
<0.44.0>
4> gen_server:cast(hello_server, crash).       % 来,崩了它

=INFO REPORT==== 17-Dec-2012::11:00:56 ===     % 不错,gen_server 调用了 terminate
terminating                                    % 因此可以清理,关闭 sockets 等操作

=ERROR REPORT==== 17-Dec-2012::11:00:56 ===    % 以下是 SASL 的报告
** Generic server hello_server terminating     % 只需用 OTP 就无偿获得
** Last message in was {'$gen_cast',crash}
** When Server state == {state,0}
** Reason for termination ==                   % 嗯嗯,这就是出错的地方
** {function_clause,[{hello_server,handle_cast,[crash,{state,0}]},
                     {gen_server,handle_msg,5},
                     {proc_lib,init_p_do_apply,3}]}

=CRASH REPORT==== 17-Dec-2012::11:00:56 ===    % 这里有更多环境信息
  crasher:
    initial call: hello_server:init/1
    pid: <0.44.0>
    registered_name: hello_server
    exception exit: {function_clause,
                        [{hello_server,handle_cast,[crash,{state,0}]},
                         {gen_server,handle_msg,5},
                         {proc_lib,init_p_do_apply,3}]}
      in function  gen_server:terminate/6
    ancestors: [<0.32.0>]
    messages: []
    links: [<0.32.0>]
    dictionary: []
    trap_exit: false
    status: running
    heap_size: 377
    stack_size: 24
    reductions: 142
  neighbours:
    neighbour: [{pid,<0.32.0>},
                  {registered_name,[]},
                  {initial_call,{erlang,apply,2}},
                  {current_function,{io,wait_io_mon_reply,2}},
                  {ancestors,[]},
                  {messages,[]},
                  {links,[<0.26.0>,<0.44.0>]},
                  {dictionary,[]},
                  {trap_exit,false},
                  {status,waiting},
                  {heap_size,2584},
                  {stack_size,30},
                  {reductions,6595}]
** exception exit: function_clause
     in function  hello_server:handle_cast/2
        called as hello_server:handle_cast(crash,{state,0})
     in call from gen_server:handle_msg/5
     in call from proc_lib:init_p_do_apply/3
5> whereis(hello_server).                      % 进程虽崩溃了,
undefined                                      % 至少以上报告给我们一个清晰的原因

OTP 监督器

这是 rebar 生成的监督器模版,并未改造。

-module(hello_sup).

-behaviour(supervisor).

%% API
-export([start_link/0]).

%% 监督器回调
-export([init/1]).

%% 子监督器的宏助手
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).

%% ===================================================================
%% API 函数
%% ===================================================================

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% ===================================================================
%% 监督器回调
%% ===================================================================

init([]) ->
    {ok, { {one_for_one, 5, 10}, []} }.

哇,这代码不长呢!哇,这到底是什么东东?待会儿我们就会细究然后把模版留的坑埋了,让它能配合 gen_server。尽管细节尚需捋顺,但大部分是我们已经掌握的了。

好吧,init/1 是神奇所在。对比必须手动监视和重启服务器的非 OTP 版本,这里是一个纯宣言式的,尽管配置监督器容器的元组形式很是神秘。

-define(CHILD(I, Type),...) 什么来头?这是一个宏,像之前的老熟脸 -define(SERVER, ?MODULE).,只不过这次是两个参数。在宏名前加上 ?,给 IType 赋值即可使用该宏。例如,?CHILD(hello_server, worker) 会在编译时展开为 {hello_server, {hello_server, start_link, []}, , permanent, 5000, worker, [hello_server]}。但这段代码里还没有用这个宏的地方,该怎么调教它呢?

看看 Emacs 怎样帮我们生成监督器模版。先忽视其它的,现在我们只对 init/1 函数感兴趣。来试试看,M-x tmm-menubar, E, S, 1E 表示 Erlang;S 表示骨架(Skeletons),1 选择生成监督器。这是结果:

init([]) ->

  SupFlags = #{strategy => one_for_one,
               intensity => 1,
               period => 5},

  AChild = #{id => 'AName',
             start => {'AModule', start_link, []},
             restart => permanent,
             shutdown => 5000,
             type => worker,
             modules => ['AModule']},

  {ok, {SupFlags, [AChild]}}.

(注:此处译者根据 Emacs 25.3 有所调整)

这应比 rebar 生成的好理解。当然,至少在结构上,最终应返回同一种配置元组。把元组里 ANameAModule 替换为上面的 hello_server 并展开所有变量,会得到以下结构,同时我加上一些注释以便理解:

{ok,                      % ok, 这些就是监督器的要求
  {
    {                     % 监督器全局选项
      one_for_one,        % - 使用 one-for-one 重启策略
      1000,               % - 允许各个子进程 1000 次重启
      3600                % - 在一个小时内
    },
    [                     % 需监督的子进程列表
      {                   % 在此只有一个
        hello_server,     % - 注册到 hello_server 名下
        {                 % - 以下是查找和启动子进程的代码
          hello_server,   %   * 模块叫 hello_server
          start_link,     %   * 需要调用的函数叫 start_link
          []              %   * 这是默认参数列表
        },
        permanent,        % - 子进程应持续运行,崩溃后重启
        2000,             % - 系统停止时,提供 2 秒时间让子进程进行清理,然后再杀掉
        worker            % - 另外,这个子进程是工作者,不是监督器
        [hello_server]    % - 这些是进程用到的模块
      }
    ]
  }
}.

你可以自己决定使用哪种格式,rebar 的紧凑标记,emacs 的啰嗦模版或是上面这个展开的片段。作为练习,请实现监督器并编译。在测试前解释一下上面的参数。

来试试 OTP 监督者。

1> hello_sup:start_link().                % 启动根监督器
{ok,<0.34.0>}
2> whereis(hello_server).                 % 确认子进程连带启动
<0.35.0>
3> hello_server:say_hello().              % 调用子进程 API
Hello
ok
4> hello_server:get_count().              % 检查是否跟以前一样有效
1
5> gen_server:cast(hello_server, crash).  % 崩掉工作者进程

=INFO REPORT====                          % 不错,terminate 还是被调用了
terminating

=ERROR REPORT==== 17-Dec-2012::13:55:51 ===
** Generic server hello_server terminating
** Last message in was {'$gen_cast',crash}
** When Server state == {state,2}
** Reason for termination ==             % 可以看到崩溃原因
** {function_clause,[{hello_server,handle_cast,[crash,{state,2}]},
                     {gen_server,handle_msg,5},
                     {proc_lib,init_p_do_apply,3}]}
ok
6> whereis(hello_server).                % 新的子进程已经启动
<0.40.0>
7> hello_server:get_count().             % 显然,状态是初始化过的
0
8> hello_server:stop().                  % 调用 stop 这个 API 函数

=INFO REPORT===
terminating                              % 看起来没毛病
ok
9> whereis(hello_server).                % 监督器已开启另一个服务器
<0.44.0>
10>

这虽按照设计规则运行,但可能有些结果并不是想要的。stop/0 原本要停止进程,但监督器马上启动了另一个新实例。我们可以把重启参数更换为 transient, 但会留下一个无任何子进程的监督器。我觉得这没问题,比模模糊糊的结果好。因此让我们转移注意力到 OTP 应用程序 上,学习怎么整体上启动和停止应用程序。

OTP 应用程序

前面已说过一个通过 application:start(sasl) 运行应用程序的例子:SASL。如何把根监督器和服务器打包到一起,用 SASL 的方式来运行呢?

gen_serversupervisor 的实现不同,OTP 应用程序的 application 需要两个文件:一个通常命名为 应用名称.app 的配置文件,和命名为 应用名称_app.erl 的 Erlang 源文件。在我们的例子中就是:

注意 hello.app 放在 ebin 目录中,显而易见,.app 文件告诉 Erlang 如何运行程序。通常不会分发源代码,只会保留编译好的 .beam 来发布程序,因此 ebin 才是 .app 归属。

作为 rebar 用户,你会发现多了一个前面提过的 src/hello.app.src 的文件,它是 ebin/hello.app 的模版,运行 ./rebar 编译后生成。

这是 rebar 生成的 hello_app.erl,实现了 OTP application 行为,不用我说太多了吧:

-module(hello_app).

-behaviour(application).

%% Application callbacks
-export([start/2, stop/1]).

%% ===================================================================
%% Application callbacks
%% ===================================================================

start(_StartType, _StartArgs) ->
    hello_sup:start_link().

stop(_State) ->
    ok.

这是告诉 Erlang 如何启动应用程序的配置文件:

{application,hello,
  [{description,[]},
   {vsn,"1"},
   {registered,[]},
   {applications,[kernel,stdlib]},
   {mod,{hello_app,[]}},
   {env,[]},
   {modules,[hello_app,hello_server,hello_sup]}]}.

只是一个元组而已。第一个元素是 application 原子,第二是应用程序名称,接下来是键-值元组列表。使用 rebar 的话别忘了最后那个句点。简单看看键值对:

进入 Erlang shell,启动应用程序。

1> application:start(hello).
ok
2> whereis(hello_sup).
<0.37.0>
3> whereis(hello_server).
<0.38.0>
4> hello_server:say_hello().
Hello
ok
5> application:stop(hello).

=INFO REPORT==== 17-Dec-2012::15:37:47 ===
    application: hello
    exited: stopped
    type: temporary
ok
6> whereis(hello_sup).
undefined
7> whereis(hello_server).
undefined

棒棒,只需名字就能启动和停止应用程序了。可好玩的事儿没完呢,还记得把非 OTP 服务器换成 gen_server 时,如何通过 SASL 自动得到更详尽的报告吗?下次试试先 appmon:strat()(译注:新版 Erlang 已用 observer:start() 取代)启动应用程序监控器,在运行 hello 点击 hello 按钮就会显示进程的层级。漂亮极了,你觉得呢?