Programming Erlang 第16章 OTP简介(完整)
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 的工作原理,并且了解足够多的底层细节。
这里是本章的计划:
- 编写一个很小的CS程序
- 一步步扩展程序并添加功能
- 放入真实的代码
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 编写回调模块的三点计划:
- 决定回调函数名
- 编写接口函数
- 编写六个必需的回调函数
这很简单,无需思考,只要跟着感觉走就行了!
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 的手册页。