Archive for February 24th, 2008

Programming Erlang 第16章 OTP简介(完整)

Sunday, February 24th, 2008

OTP简介

译者: gashero

目录

OTP代表Open Telecom Platform。这个名字容易让人误解,但是其实OTP比你想象的更加通用。它是一个应用操作系统和一堆函数库,用以构建大型、容错和分布式的应用。它最初是在瑞典的爱立信开发的,并且在爱立信用于构建容错系统。(爱立信已经以EPL的形式放出了Erlang,EPL是从Mozilla许可协议继承来的)

OTP包含一些有用的工具,比如一个完整的WEB服务器,一个FTP服务器,一个CORBA ORB,等等,他们都是用Erlang编写的。OTP同时包含在电信技术中H248的技术实现标准、SNMP、和ASN.1到Erlang的交叉编译器。这里先不谈这些,你可以从后面的附录C了解更多。

如果你想使用OTP来编写你的系统,你会发现OTP行为(behavior)的概念对你很有用。一个行为包装了很多动作模式-可以理解为使用参数回调的应用程序框架。

OTP的强大之处在于容错、可量度的和代码热部署等等,他们都可以通过行为来提供。在其他部分,回调作者无需担心容错的问题,因为OTP已经内置了。在Java的思想中,你可以把行为看作是J2EE容器。

简单来说,行为解决了问题的非功能性部分,而回调函数解决功能性部分。对大多数应用来说,非功能性部分基本是相似的(例如代码热部署)。而功能性部分则各个应用不同。

在本章,我们来了解这些行为, gen_server 模块更详细一些。在我们讨论 gen_server 如何工作之前,我们首先启动一个简单的服务器(我们能想象的最简单的服务器),然后一步步的改变它,直到我们得到 gen_server 模块。通过这种方法,你可以深入的了解 gen_server 的工作原理,并且了解足够多的底层细节。

这里是本章的计划:

  1. 编写一个很小的CS程序
  2. 一步步扩展程序并添加功能
  3. 放入真实的代码

1   通向通用服务器之路

Note

这将是本书中最重要的章节,所以首先阅读一遍,然后再阅读一遍,直到一百遍,确保知道了每一个细节。

我们即将开始编写我们的服务器,分别叫做server1、server2、…,每个只是与前一个略有不同。目标是从程序中提取非功能性部分。最后一句可能对你来说有点晕,不过别担心。深呼吸……

1.1   例子1:基本服务器

如下是我们的第一个尝试。这是一个小服务器,我们可以参数化我们的回调模块:

-module(server1).
-export([start/2,rpc/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,Response} -> Response
    end.

loop(Name,Mod,State) ->
    receive
        {From,Request} ->
            {Response,State} = Mod:handle(Request,State),
            From ! {Name,Response},
            loop(Name,Mod,State)
    end.

这是一个服务器的代码精髓,我们来写server1的回调模块,如下是命名服务器回调:

-module(name_server).
-export([init/0,add/2,whereis/1,handle/2]).
-import(server1,[rpc/2]).

%% client routines
add(Name,Place) -> rpc(name_server,{add,Name,Place}).
whereis(Name) -> rpc(name_server,{whereis,Name}).

%% callback routines
init() -> dict:new().

handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.

这段代码实际完成了两项任务。他作为一个回调模块被服务器框架代码所调用,同时它包含可以被客户端调用的接口程序。OTP的惯例是把相同功能的代码放在一个模块中。

我们想让它工作,只需要:

1> server1:start(name_server,name_server).
true
2> name_server:add(joe,"at home").
ok
3> name_server:whereis(joe).
{ok,"at home"}

现在停止思考。回调没有并发,没有spawn,没有send,没有receive,没有register。他是纯的序列代码-这意味着什么呢?这意味着我们可以可以在并不理解底层并发模块的情况下泄CS模型。

这是一个简单服务器的通用模式。一旦你理解的简单的结构,就可以让你更快的理解了。

1.2   例子2:包含事务处理的服务器

这里的服务器在遇到客户端错误查询时会挂掉:

-module(server2).
-export([start/2,rpc/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,crash} -> exit(rpc);
        {Name,ok,Response} -> Response
    end.

loop(Name,Mod,OldState) ->
    receive
        {From,Request} ->
            try Mod:handle(Request,OldState) of
                {Response,NewState} ->
                    From ! {Name,ok,Response},
                    loop(Name,Mod,NewState)
            catch
                _:Why ->
                    log_the_error(Name,Request,Why),
                    %% 发送消息导致客户端挂掉
                    From ! {Name,crash},
                    %% 使用原始状态继续循环
                    loop(Name,Mod,OldState)
            end
    end.

log_the_error(Name,Request,Why) ->
    io:format("Server ~p request ~p ~n"
              "caused exception ~p~n",
              [Name,Request,Why]).

这个例子中使用了事务语义,在发生异常时,它使用原来的值继续循环。但是如果处理函数执行成功,则会以NewState值来继续下一次循环。

为什么要保留原来的值呢?当处理函数失败时,错误的消息来自于客户端的错误。而客户端无法处理,因为错误的请求已经让处理函数失败了。但是其他客户端希望服务器仍然可用。此外,服务器的状态在发生错误时不会改变。

注意回调模块与server1的非常像。通过改变服务器和保持回调模块不变,我们可以改变回调模块中的非功能行为。

Note

最后一个语句并不总是true。我们在从server1到server2时还是要修改一点代码,就是把 -import 语句从server1改为server2。除此之外,就没有改变了。

1.3   例子3:含有代码热交换的服务器

现在我们加上代码热交换:

-module(server3).
-export([start/2,rpc/2,swap_code/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

swap_code(Name,Mod) -> rpc(Name,{swap_code,Mod}).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,Response} -> Response
    end.

loop(Name,Mod,OldState) ->
    receive
        {From, {swap_code,NewCallBackMod}} ->
            From ! {Name,ack},
            loop(Name,NewCallBackMod,OldState);
        {From,Request} ->
            {Response,NewState} = Mod:handle(Request,OldState),
            From ! {Name,Response},
            loop(Name,Mod,NewState)
    end.

他如何工作?

我们首先交换代码消息给服务器,然后他会改变回调模块到消息中包含的新的模块。

我们可以通过启动server3来演示,然后动态交换毁掉模块。我们不能使用 name_server 作为回调模块,因为我们把服务器名硬编码到服务器中了。所以,我们需要复制一份,叫做 name_server1 ,我们还需要这么改变代码:

-module(name_server1).
-export([init/0,add/2,whereis/1,handle/2]).
-import(server3,[rpc/2]).

%%客户端程序
add(Name,Place) -> rpc(name_server,{add,Name,Place}).
whereis(Name) -> rpc(name_server,{whereis,Name}).

%%回调函数
init() -> dict:new().

handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.

首先我们以name_server1启动server3:

1> server3:start(name_server,name_server1).
true
2> name_server:add(joe,"at home").
ok
3> name_server:add(helen,"at work").
ok

现在假设我们想要找到名字服务器中的所有名字。而没有函数提供此功能,name_server1模块只提供了增加和查询名字的功能。

所以,我们以迅雷不及掩耳盗铃之势启动了编辑器并写了新的回调模块:

-module(new_name_server).
-export([init/0,add/2,all_names/0,delete/1,whereis/1,handle/2]).
-import(server3,[rpc/2]).

%%接口
all_names() -> rpc(name_server,allNames).
add(Name,Place) -> rpc(name_server,{add,Name,Place}).
delete(Name) -> rpc(name_server,{delete,Name}).
whereis(Name) -> rpc(name_server,{whereis,Name}).

%%回调函数
init() -> dict:new().

handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
handle(allNames,Dict) -> {dict:fetch_keys(Dict),Dict};
handle({delete,Name},Dict) -> {ok,dict:erase(Name,Dict)};
handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.

我们编译如上模块,并且叫服务器交换回调模块:

4> c(new_name_server).
{ok,new_name_server}
5> server3:swap_code(name_server,new_name_server).
ack

现在我们可以运行服务器上的函数了:

6> new_name_server:all_names().
[joe,helen]

这次我们修改模块就是热部署的-动态代码升级,在你眼前运行的,有如魔术一般。

现在停下再想想。我们做的最后两项任务看起来很简单,但是事实上很困难。包含“事务语义”的服务器非常难写,含有代码热部署的服务器也很困难。

这个技术非常强大。传统意义上,我们认为服务器作为程序包含状态,并且在我们发送消息时改变状态。服务器代码在第一次被调用时就固定了,如果我们想要改变服务器的代码就需要重启服务器。在我们给出的例子中,服务器的代码改变起来有如我们改变状态一样容易(在软件的不停机维护升级中,我经常使用该技术)。

1.4   例子4:事务和代码热交换

在上两个服务器中,代码热升级和事务语义是分开介绍的。这里我们把它们合并到一起:

-module(server4).
-export([start/2,rpc/2,swap_code/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

swap_code(Name,Mod) -> rpc(Name,{swap_code,Mod}).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,crash} -> exit(rpc);
        {Name,ok,Response} -> Response
    end.

loop(Name,Mod,OldState) ->
    receive
        {From,{swap_code,NewCallBackMod}} ->
            From ! {Name,ok,ack},
            loop(Name,NewCallBackMod,OldState);
        {From,Request} ->
            try Mod:handle(Request,OldState) of
                {Response,NewState} ->
                    From ! {Name,ok,Response},
                    loop(Name,Mod,NewState)
            catch
                _:Why ->
                    log_the_error(Name,Request,Why),
                    From ! {Name,crash},
                    loop(Name,Mod,OldState)
            end
    end.

log_the_error(Name,Request,Why) ->
    io:format("Server ~p request ~p~n"
              "caused exception ~p~n",
              [Name,Request,Why]).

这个服务器同时提供了代码热交换和事务语义,并且很整洁。

1.5   例子5:更有趣的功能

现在我们已经对代码热交换有主意了,我们还可以找些有趣的做。下面的服务器在你告知他作为特定类型的服务器之前什么都不做:

-module(server5).
-export([start/0,rpc/2]).

start() -> spawn(fun() -> wait() end).

wait() ->
    receive
        {become,F} -> F()
    end.

rpc(Pid,Q) ->
    Pid ! {self(),Q},
    receive
        {Pid,Reply} -> Reply
    end.

如果我们启动它然后发送 {become,F} 消息,它就会变成对F()求值的服务器,我们可以这样启动:

1> Pid=server5:start().
<0.57.0>

我们的服务器在等待消息时什么都不做。

现在我们定义服务器函数。也并不复杂,只要计算斐波拉契:

-module(my_fac_server).
-export([loop/0]).

loop() ->
    receive
        {From,{fac,N}} ->
            From ! {self(),fac(N)},
            loop();
        {become,Something} ->
            Something()
    end.

fac(0) -> 1;
fac(N) -> N*fac(N-1).

Note

PlantLab中的Erlang

几年前,在我PlanetLab做研究。我有权访问PlanetLab的网络,所以我在所有的服务器上都安装了空的Erlang服务器(大约450台)。我并不知道我要这些机器干什么用,所以后来我开始研究服务器结构。

当我可以让这一层很好的运行时,发送消息到一台空服务器,以便让他成为真正的服务器就很简单了。

常见的途径是启动WEB服务器,然后安装WEB服务器插件。我的方法是再后退一步,只是安装一个空服务器,然后在空服务器中安装WEB服务器。而当我们安装好WEB服务器以后,再告诉它应该变成什么。

只要确保他们编译好了,然后我们就可以告诉进程 <0.57.0> 变成一个斐波拉契服务器:

2> c(my_fac_server).
{ok,my_fac_server}
3> Pid ! {become,fun my_fac_server:loop/0}.
{become,#Fun<my_fac_server.loop.0>}

现在,我们的进程已经变成斐波拉契服务器了,我们可以这样调用:

4> server5:rpc(Pid,{fac,30}).
2652528598121910586363084800000000

我们的还会继续保持作为斐波拉契服务器的状态,直到我们发送消息 {become,Something} 来让他改变。

有如你所看到的,我们可以在一定范围内改变服务器的类型。这种技术很强大,使用该技术可以构建小巧而美观的服务器,却有很高的灵活性。在我们编写工业规模的,拥有数百名程序员的项目时,我们并不希望有些事情变得太过于动态。我们必须在通用和功能强大方面做出取舍,因为我们要的是产品。灵活多变的代码往往造成一堆难于调试的bug。如果我们已经在程序中做了多处动态修改,然后crash了,那会很难找到问题。

本节的服务器例子并不是非常正确。他们只是用来展示棘手问题的发展,他们实在太小了,而且还有些狡猾的错误。我不会立即告诉你们这些,但是在本章末尾,我会给出一些提示。

Erlang模块 gen_server 是构建久经考验服务器的优秀方案。

他在1998年就开始应用于工业级别的产品中了。一个产品中往往包含数百个服务器。这些服务器是使用正规的顺序化代码写成。所有的错误和非功能性行为都被放在了服务器中的通用部分了。

所以现在我们可以直接跳到 gen_server 的怀抱了。

2   gen_server 入门

这里是另一个极端。注意使用 gen_server 编写回调模块的三点计划:

  1. 决定回调函数名
  2. 编写接口函数
  3. 编写六个必需的回调函数

这很简单,无需思考,只要跟着感觉走就行了!

2.1   步骤1:决定回调函数名

我们来做一个简单的支付系统,我们把模块名叫做 my_bank ,这个系统已经在应用了,只是客户封闭了代码,如果他们再次发布源码你会感觉跟这个例子很像。

2.2   步骤2:编写接口函数

我们定义五个接口例程,都在模块 my_bank 中:

start() :开启银行

stop() :关闭银行

new_account(Who) :创建账户

deposit(Who,Amount) :存款

withdraw(Who,Amount) :取款

他们最终都是需要调用 gen_server 的,如:

start() -> gen_server:start_link({local,?MODULE},?MODULE,[],[]).
stop() -> gen_server:call(?MODULE,stop).

new_account(Who) -> gen_server:call(?MODULE,{new,Who}).
deposit(Who,Amount) -> gen_server:call(?MODULE,{add,Who,Amount}).
withdraw(Who,Amount) -> gen_server:call(?MODULE,{remove,Who,Amount}).

gen_server:start_link({local,Name},Mod,...) 开启一个本地服务器(通过参数global,可以构造一个可以被Erlang集群节点所访问的服务器)。宏 ?MODULE 会被解析为模块名 my_bank 。 Mod 是回调模块名。我们暂时会忽略其他传递给 gen_server:start 的参数。

gen_server:call(?MODULE,Term) 用于对服务器的远程调用。

2.3   步骤3:编写六个必需的回调函数

我们的回调模块导出了六个回调例程: init/1 、 handle_call/3 、 handle_cast/2 、 handle_info/2 、 terminate/2 、 code_change/3 。为了快速实现,我们使用模板来构造 gen_server 。如下是最简单的例子:

-module().
%% gen_server_mini_template

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

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

init([]) -> {ok,State}.

handle_call(_Request,_From,State) -> {reply,Reply,State}.
handle_cast(_Msg,State) -> {noreply,State}.
handle_info(_Info,State) -> {noreply,State}.
terminate(_Reason,_State) -> ok.
code_change(_OldVsn,State,Extra) -> {ok,State}.

模板包含了最简单的需要填入服务器的框架(skeleton)。关键字 -behaviour 用于告知编译器在我们没有定义适当的回调函数时给出错误信息。

Tip

如果你正在使用emacs,你可以把 gen_server 模板放到快捷键中。可以修改erlang-mode,然后 Erlang>Skeleton菜单提供创建 gen_server 的模块。如果你不使用emacs,也别担心,我在本章末尾提供了该模板。

我们从模板开始编辑修改。我们所需要做的只是让参数与接口相匹配。

最重要的部分是 handle_call/3 函数。我们需要写出匹配3个查询术语的接口程序。也就是我们需要填充省略号处的代码:

handle_call({new,Who},From,State) ->
    Reply = ...
    State1 = ...
    {reply,Reply,State1};
handle_call({add,Who,Amount},From,State) ->
    Reply = ...
    State1 = ...
    {reply,Reply,State1};
handle_call({remove,Who,Amount},From,State) ->
    Reply = ...
    State1 = ...
    {reply,Reply,State1};

Reply的值会被发送到客户端作为远程调用的返回值。

State是服务器状态的全局变量,需要被服务器中持续传递。在我们的银行模块中,状态不会改变;他只是一个ETS表索引,而且是个常量(尽管表格的内容会改变)。

当我们填写模板并且修改后,我们得到如下代码:

init([]) -> {ok,ets:new(?MODULE,[])}.

handle_call({new,Who},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
                [] -> ets:insert(Tab,{Who,0}),
                    {welcome,Who};
                [_] -> {Who,you_already_are_a_customer}
            end,
    {reply,Reply,Tab};
handle_call({add,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
                [] -> not_a_customer;
                [{Who,Balance}] ->
                    NewBalance=Balance+X,
                    ets:insert(Tab,{Who,NewBalance}),
                    {thanks,Who,your_balance_is,NewBalance}
            end,
    {reply,Reply,Tab};
handle_call({remove,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
                [] -> not_a_customer;
                [{Who,Balance}] when X =< Balance ->
                    NewBalance=Balance -X,
                    ets:insert(Tab,{Who,NewBalance}),
                    {thanks,Who,your_balance_is,NewBalance};
                [{Who,Balance}] ->
                    {sorry,Who,you_only_have,Balance,in_the_bank}
            end,
    {reply,Reply,Tab};
handle_call(stop,_From,Tab) ->
    {stop,normal,stopped,Tab}.

handle_cast(_Msg,State) -> {noreply,State}.
handle_info(_Info,State) -> {noreply,State}.
terminate(_Reason,_State) -> ok.
code_change(_OldVsn,State,Extra) -> {ok,State}.

我们可以通过调用 gen_server:start_link(Name,CallBackMod,StartArgs,Opts) 来启动服务器,然后第一个被调用的历程是回调模块的 Mod:init(StartArgs) ,而且必须返回 {ok,State} 。State的值会在以后作为 handle_call 的第三个参数一直传递。

注意我们如何停止服务器。 handle_call(Stop,From,Tab) 返回 {stop,Normal,stopped,Tab} 会停止服务器。第二个参数normal用作调用 my_bank:terminate/2 的第一个参数。第三个参数stopped会作为 my_bank:stop() 的返回值。

就这些了,让我们看看如何使用银行:

1> my_bank:start().
{ok,<0.33.0>}
2> my_bank:deposit("joe",10).
not_a_customer
3> my_bank:new_account("joe").
{welcome,"joe"}
4> my_bank:deposit("joe",10).
{thanks,"joe",your_balance_is,10}
5> my_bank:deposit("joe",30).
{thanks,"joe",your_balance_is,40}
6> my_bank:withdraw("joe",15)
{thanks,"joe",your_balance_is,25}
7> my_bank:withdraw("joe",45).
{sorry,"joe",you_only_have,25,in_the_bank}

3   gen_server 回调结构

现在我们已经有主意了,我们多了解一下 gen_server 的回调结构。

3.1   启动服务器时发生了什么?

通过 gen_server:start_link(Name,Mod,InitArgs,Opts) 启动时,他会创建一个叫做Name的通用服务器。回调模块是Mod。Opts控制通用服务器的行为,可以指定消息日志、调试函数等等。通用服务器会启动回调 Mod:init(InitArgs) 。

模板的初始化入口如下:

%% Function: init(Args) -> {ok,State}
%%                         {ok,State,Timeout}
%%                         ignore
%%                         {stop,Reason}
init([]) ->
    {ok,#state{}}.

对于正常操作,我们只需要返回 {ok,State} 。其他参数的含义可以参考 gen_server 的手册页。

如果返回了 {ok,State} ,我们就算是成功的启动了服务器,并初始化了状态State。

3.2   我们调用服务器时发生了什么?

想要调用服务器,客户端程序调用 gen_server:call(Name,Request) 。这最终会调用回调模块的 handle_call/3 。

handle_call/3 拥有如下入口模板:

%% Function: handle_call(Request,From,State) -> {reply,Reply,State}
%%                                              {reply,Reply,State,Timeout}
%%                                              {noreply,State}
%%                                              {noreply,State,Timeout}
%%                                              {stop,Reason,Reply,State}
%%                                              {stop,Reason,State}
handle_call(_Request,_From,State) ->
    Reply=ok,
    {reply,Reply,State}.

Request参数重新出现在 handle_call/3 的第一个参数中。From是请求客户端进程的PID,而State则是当前客户端的状态。

一般来说,我们返回 {reply,Reply,NewState} 。当这发生时,Reply会发送到客户端,而它是作为 gen_server:call 的返回值的。NewState是服务器的下一个状态。

另一个返回值 {noreply,…} 和 {stop,…} 用于罕见的特殊情况。没有返回值会让服务器继续工作,而客户端会继续等待服务器的响应,所以服务器需要委托其他进程来作出响应。调用stop会适当的结束服务器。

3.3   调用与投送(Cast)

我们已经看到了 gen_server:call 与 handle_call 的相互影响了。这是用于实现RPC(Remote Procedure Call)的。 gen_server:cast(Name,Name) 实现了投送(cast),就是没有返回值的调用(实际上是消息,但是一般叫做投送,以区别RPC)。

相应的回调例程是 handle_cast ,入口模板如下:

%% Function: handle_cast(Msg,State) -> {noreply,NewState}
%%                                     {noreply,NewState,Timeout}
%%                                     {stop,Reason,NewState}
handle_cast(_Msg,State) ->
    {noreply,NewState}.

处理函数通常返回 {noreply,NewState} ,就是改变服务器的状态。而 {stop,…} 则会停止服务器。

3.4   发到服务器的自发信息

回调函数 handle_info(Info,State) 用于处理发送到服务器的自发信息。那什么是自发信息呢?如果服务器连接到的一个进程突然退出了,那么他会立即收到不期望的 {“EXIT”,Pid,What} 消息。另外,系统中任何知道通用服务器PID的进程都可以给他发消息。在服务器有如Info的值时会发出死掉的消息(Any message like this ends up at the server as the value of Info)。

handle_info 的入口模板如下:

%% Function: handle_info(Info,State) -> {noreply,State}
%%                                      {noreply,State,Timeout}
%%                                      {stop,Reason,State}
handle_info(_Info,State) ->
    {noreply,State}.

返回值与 handle_cast 相同。

3.5   来吧,宝贝

服务器可能会因为多种原因而停止。其中一个原因是 handle_Something 例程可能会返回 {stop,Reason,NewState} ,或者服务器可能会挂了并抛出 {‘EXIT’,reason} 。在这些情况中,无论发生了什么, terminate(Reason,NewState) 会被调用。

如下是模板:

%% Function: terminate(Reason,State) -> void()
terminate(_Reason,State) ->
    ok.

这些代码不会返回新的状态,因为他们已经停止了。所以,我们应该如何处理状态呢?可以做很多事情,比如打扫战场。我们可以把它存储到磁盘,发送消息通知其他进程,或者抛弃其依赖的应用程序。如果你想要服务器在未来被重启,你还需要编写被 terminate/2 触发的“我会回来”函数。

3.6   代码改变

你可以在服务器运行时动态改变你的代码。这个回调函数包含处理释放子系统并提供软件更新的功能。

相关章节请参考OTP设计原则文档(http://www.erlang.org/doc/pdf/design_principles.pdf)。

%% Function: code_change(OldVsn,State,Extra) -> {ok,NewState}
code_change(_OldVsn,State,_Extra) -> {ok,State}.

4   代码与模板

这是通过emacs-mode生成的,通用服务器模板:

%%% -----------------------------------------------
%%% File: gen_server_template.full
%%% Author: ...
%%% Description: ...
%%% Created: ...
%%% -----------------------------------------------
-module().
-behaviour(gen_server).

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

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

-record(state,{}).

%%% Function start_link() -> {ok,Pid} | ignore | {error,Error}
%%% 启动服务器
start_link() ->
    gen_server:start_link({local,?SERVER},?MODULE,[],[]).

%%% Function init(Args) -> {ok,State} |
%%%                        {ok,State,Timeout} |
%%%                        ignore |
%%%                        {stop,Reason}
%%% 初始化服务器
init([]) ->
    {ok,#state{}}.

%%% Function handle_call(Request,From,State) -> {reply,Reply,State} |
%%%                                             {reply,Reply,State,Timeout} |
%%%                                             {noreply,State} |
%%%                                             {noreply,State,Timeout} |
%%%                                             {stop,Reason,Reply,State} |
%%%                                             {stop,Reason,State}
%%% 处理所有消息
handle_call(_Request,_From,State) ->
    Reply=ok,
    {reply,Reply,State}.

%%% Function handle_cast(Msg,State) -> {noreply,State} |
%%%                                    {noreply,State,Timeout} |
%%%                                    {stop,Reason,State}
%%% 处理所有投送消息
handle_cast(_Msg,State) ->
    {noreply,State}.

%%% Function handle_info(Info,State) -> {noreply,State} |
%%%                                     {noreply,State,Timeout} |
%%%                                     {stop,Reason,State}
%%% 处理所有调用或投送消息
handle_info(_Info,State) ->
    {noreply,State}.

%%% Function terminate(Reason,State) -> void()
%%% 在服务器停止时被调用,与init/1做的事情相反,释放资源。当它返回时,
%%% gen_server就会以Reason终止,返回值会被忽略
terminate(_Reason,_State) ->
    ok.

%%% Function code_change(OldVsn,State,Extra) -> {ok,NewState}
%%% 在代码改变时覆盖进程状态
code_change(_OldVsn,State,_Extra) ->
    {ok,State}.

%%% 内部函数

my_bank的代码:

-module(my_bank).
-behaviour(gen_server).
-export([start/0]).
%% gen_server回调
-export([init/1,handle_call/3,handle_cast/2,handle_info/2,
        terminate/2,code_change/3]).
-compile(export_all).

start() -> gen_server:start_link({local,?MODULE},?MODULE,[],[]).
stop() -> gen_server:call(?MODULE,stop).

new_account(Who) -> gen_server:call(?MODULE,{new,Who}).
deposit(Who,Amount) -> gen_server:call(?MODULE,{add,Who,Amount}).
withdraw(Who,Amount) -> gen_server:call(?MODULE,{remove,Who,Amount}).

init([]) -> {ok,ets:new(?MODULE,[])}.

handle_call({new,Who},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
            [] -> ets:insert(Tab,{Who,0}),
                    {welcome,Who};
            [_] -> {Who,you_already_are_a_customer}
          end,
        {reply,Reply,Tab};
handle_call({add,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
            [] -> not_a_customer;
            [{Who,Balance}] ->
                NewBalance=Balance+X,
                ets:insert(Tab,{Who,NewBalance}),
                {thanks,Who,your_balance_is,NewBalance}
          end,
        {reply,Reply,Tab};
handle_call({remove,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
            [] -> not_a_customer;
            [{Who,Balance}] when X =< Balance ->
                NewBalance=Balance-X,
                ets:insert(Tab,{Who,NewBalance}),
                {thanks,Who,you_only_have,Balance,in_the_bank}
          end,
        {reply,Reply,Tab};
handle_call(stop,_From,Tab) ->
    {stop,normal,stopped,Tab}.
handle_cast(_Msg,State) -> {noreply,State}.
handle_info(_Info,State) -> {noreply,State}.
terminate(_Reason,_State) -> ok.
code_change(_OldVsn,State,Extra) -> {ok,State}.

5   深度挖掘

gen_server 实际上很简单。不过我们并没有了解 gen_server 的所有接口,也没有必要知道所有参数的含义。当你明白基本原理之后,你只需要查阅 gen_server 的手册就够了。

在本章,我们只是讨论了使用 gen_server 的最简单情况,但是适用于大多数情况。复杂的应用经常让 gen_server 返回 noreply 而是委派真正的响应给另一个进程想要了解这些,请参与设计原则文档(http://www.erlang.org/doc/pdf/design_principles.pdf)。和模块 sysproc_lib 的手册页。