`
liangjianss
  • 浏览: 51332 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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

阅读更多

OTP简介
译者: gashero


目录

? 1   通向通用服务器之路
? 1.1   例子1:基本服务器
? 1.2   例子2:包含事务处理的服务器
? 1.3   例子3:含有代码热交换的服务器
? 1.4   例子4:事务和代码热交换
? 1.5   例子5:更有趣的功能
? 2   gen_server 入门
? 2.1   步骤1:决定回调函数名
? 2.2   步骤2:编写接口函数
? 2.3   步骤3:编写六个必需的回调函数
? 3   gen_server 回调结构
? 3.1   启动服务器时发生了什么?
? 3.2   我们调用服务器时发生了什么?
? 3.3   调用与投送(Cast)
? 3.4   发到服务器的自发信息
? 3.5   来吧,宝贝
? 3.6   代码改变
? 4   代码与模板
? 5   深度挖掘
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)。和模块 sys 、 proc_lib 的手册页。
分享到:
评论

相关推荐

    Erlang/OTP Application完整例子

    Erlang/OTP Application完整例子,含代码和二进制,对于学习Erlang/OTP Application很有帮助。 配套文章http://blog.csdn.net/mycwq/article/details/12610677

    erlang otp25 win安装包

    erlang otp25 win安装包

    programming erlang src code

    programming erlang 源码

    《Programming Erlang》

    Programming Erlang

    erlang_otp_win64_25.0

    erlang_otp_win64_25.0

    erlang programming

    Programming+Erlang.pdf+ 面对软件错误构建可靠的分布式系统.pdf+ Concurrent Programming in ERLANG

    erlang绿色解压版 otp_win64 21.0.1.zip

    erlang绿色解压版 otp_win64 21.0.1 OTP 20.0 下载,OTP (Open Telecom Platform) 是一个开源的 Erlang 分发和一个用 Erlang 编写的应用服务器,由爱立信开发。Erlang / OTP 21.0.1 是一个新的主要版本,新增了一些新...

    Erlang-otp_win64_23.0.zip

    rabbitmq安装前的环境安装,Erlang23.0 Windows64安装包,官网下载了几个小时

    Erlang / OTP 21.0 版本下载

    官网下载实在是困难,我把当前Erlang / OTP 21.0 版本提交到这里提供给大家和自己下载 原先积分0的现在都这么高了,为了方便大家,放出云盘地址:https://pan.百度.com/s/1hb8vPiMslXxNuJC8PvnKgg提取码wmx8

    Programming Erlang(Pragmatic,2ed,2013)

    In this second edition of the bestselling Programming Erlang, you'll learn how to write parallel programs that scale effortlessly on multicore systems. Using Erlang, you'll be surprised at how easy ...

    erlang_otp_20.3_man开发手册

    erlang_otp_20.3_man开发手册,erlang_otp_20.3_man开发手册,erlang_otp_20.3_man开发手册

    Erlang / OTP并发编程实战

    二十多年来,在传统电信领域高并发、高可靠、高容错的严酷环境下,Erlang语言和OTP平台被锻炼得坚如磐石,浓郁的函数式特质更是恰到好处地弥补了传统命令式语言在并发编程上的固有缺陷,大大降低了构筑并发、容错、...

    erlang_otp_win64_22.1.zip

    erlang 22.1 win64 安装包 otp官网下载资源,适配rabbitmq 3.8.2等版本,可以放心下载使用

    Erlang and OTP in Action

    Erlang and OTP in Action

    Erlang最新版本 otp-win64-25.0.3

    Erlang最新版本 25.0.3 windows 安装文件 解压直接安装 5个币-----------------------------------------------------------------------------------------------------------------------------------------------...

    Programming Erlang

    Programming Erlang 官方文档

    分布式应用Erlang:Erlang_OTP_19_win64

    Erlang是一种通用的面向并发的编程语言,它有瑞典电信设备制造商爱立信所辖的CS-Lab开发, 目的是创造一种可以应对大规模并发活动的编程语言和运行环境。

    Erlang/OTP 26.2.1

    Erlang/OTP 26.2.1,Erlang,OTP,26.2.1

Global site tag (gtag.js) - Google Analytics