# gen_server 入门
以下三点是编写gen_server回调模块的简要步骤
(1) 确定回调模块名。
(2) 编写接口函数。
(3) 在回调模块里编写六个必需的回调函数。
gen_server:start_link({local, Name}, Mod, ...) 会启动一个本地服务器。如果第一个参数是原子global,它就会启动一个能被Erlang节点集群访问的全局服务器。 start_link的第二个参数是Mod,也就是回调模块名。宏**?MODULE**会展开成当前模块名。
**gen_server:call(?MODULE, Term)**被用来对服务器进行远程过程调用。
我们的回调模块必须导出六个回调方法: init/1、 handle_call/3、 handle_cast/2、handle_info/2、 terminate/2和code_change/3。
gen_server_template.mini
-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}.
2
3
4
5
6
7
8
9
10
11
12
13
14
调用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)被用作Mod:terminate/2的首个参数。第三个参数(stopped)会成为Mod:stop()的返回值。
# 1. 启动服务器
gen_server:start_link(Name, Mod, InitArgs, Opts)这个调用是所有事物的起点。它会创建一个名为Name的通用服务器,回调模块是Mod, Opts则控制通用服务器的行为。在这里可以指定消息记录、函数调试和其他行为。通用服务器通过调用**Mod:init(InitArgs)**启动。
init的模板项
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% Description: 初始化服务器
%%--------------------------------------------------------------------
init([]) ->
{ok, #state{}}
2
3
4
5
6
7
8
9
在通常的操作里,只会返回**{ok, State}。要了解其他参数的含义,请参考gen_server的手册页。
如果返回{ok, State},就说明我们成功启动了服务器,它的初始状态是State**。
# 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}
%% Description: 处理调用消息
%%----------------------------------------------------------------------
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
2
3
4
5
6
7
8
9
10
11
12
13
Request(gen_server:call/2的第二个参数)作为handle_call/3的第一个参数重新出现。From是发送请求的客户端进程的PID, State则是客户端的当前状态。我们通常会返回**{reply, Reply, NewState}。在这种情况下, Reply会返回客户端,成为gen_server:call**的返回值。 NewState则是服务器接下来的状态。
其他的返回值({noreply, ..}和{stop, ..})相对不太常用。 no reply会让服务器继续工作,但客户端会等待一个回复,所以服务器必须把回复的任务委派给其他进程。用适当的参数调用stop会停止服务器。
# 3.调用和播发
我们已经见过了gen_server:call和handle_call之间的交互,它的作用是实现远程过程调用。 **gen_server:cast(Name, Msg)**则实现了一个播发(cast),也就是没有返回值的调用(实际上就是一个消息,但习惯上称它为播发来与远程过程调用相区分)。
对应的回调方法是handle_cast,它的模板项如下:
%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, NewState} |
%% {noreply, NewState, Timeout} |
%% {stop, Reason, NewState}
%% Description: 处理播发消息
%%--------------------------------------------------------------------
handle_cast(_Msg, State) ->
{noreply, NewState}.
2
3
4
5
6
7
8
这个处理函数通常只返回**{noreply, NewState}或{stop, ...}**。前者改变服务器的状态,后者停止服务器
# 4.发给服务器的自发性消息
回调函数handle_info(Info, State)被用来处理发给服务器的自发性消息。自发性消息是一切未经显式调用gen_server:call或gen_server:cast而到达服务器的消息。举个例子,如果服务器连接到另一个进程并捕捉退出信号,就可能会突然收到一个预料之外的**{'EXIT', Pid,What}消息。除此之外,系统里任何知道通用服务器PID**的进程都可以向它发送消息。这样的消息在服务器里表现为info值。
handle_info的模板项如下:
%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non-call/cast messages 处理非调用/转播消息
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
2
3
4
5
6
7
8
它的返回值和handle_cast相同。
# 监控树
监控树是一种由进程组成的树形结构。树的上级进程(监控器)监视着下级进程(工作器),如果下级进程挂了就会重启它们。监控树有两种。
一对一监控树
在一对一监控里,如果某个工作器崩溃了,就会被监控器重启。
一对多监控树
在一对多监控里,如果任何一个工作器崩溃了,所有工作进程都会被终止(通过调用相应回调模块里的terminate/2函数)然后重启。
监控器是用OTP supervisor行为创建的。这个行为用一个回调模块作为参数,里面指定了监控策略以及如何启动监控树里的各个工作进程。监控树通过以下形式的函数指定:
init(...) ->
{ok, {RestartStrategy, MaxRestarts, Time},
[Worker1, Worker2, ...]}.
2
3
这里的RestartStrategy是原子one_for_one或one_for_all, MaxRestarts和Time则指定“重启频率”。如果一个监控器在Time秒内执行了超过MaxRestarts次重启,那么这个监控器就会终止所有工作进程然后退出。这是为了防止出现一种情形,即某个进程崩溃、被重启,然后又因为相同原因崩溃而形成的无限循环。Worker1和Worker2这些是描述如何启动各个工作进程的元组,稍后就会看到它们。
举例:
{ok, {{one_for_one, 3, 10},
[{tag1,
{area_server, start_link, []},
permanent,
10000,
worker,
[area_server]},
{tag2,
{prime_server, start_link, []},
permanent,
10000,
worker,
[prime_server]}
]}}.
2
3
4
5
6
7
8
9
10
11
12
13
14
这个数据结构定义了一种监控策略。
Worker的格式是下面这种元组:
{Tag, {Mod, Func, ArgList},
Restart,
Shutdown,
Type,
[Mod1]
}
2
3
4
5
6
这些参数的意义如下。
Tag
这是一个原子类型的标签,将来可以用它指代工作进程(如果有必要的话)。
{Mod, Func, ArgList}
它定义了监控器用于启动工作器的函数,将被用作apply(Mod, Fun, ArgList)的参数。
Restart = permanent | transient | temporary
permanent(永久)进程总是会被重启。 transient(过渡)进程只有在以非正常退出值终止时才会被重启。 temporary(临时)进程不会被重启。
Shutdown
这是关闭时间,也就是工作器终止过程允许耗费的最长时间。如果超过这个时间,工作进程就会被杀掉。(还有其他值可用,参见supervisor的手册页。)
Type = worker | supervisor
这是被监控进程的类型。可以用监控进程代替工作进程来构建一个由监控器组成的树。
[Mod1]
如果子进程是监控器或者gen_server行为的回调模块,就在这里指定回调模块名。(还有其他值可用,参见supervisor的手册页。)
这些参数看上去很吓人,但其实不是。在实践中,你可以剪切粘贴之前面积服务器代码里的值,然后插入你的模块名。这对大多数用途来说足够了。