# 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/1handle_call/3handle_cast/2handle_info/2terminate/2code_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}.
1
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的通用服务器,回调模块是ModOpts则控制通用服务器的行为。在这里可以指定消息记录、函数调试和其他行为。通用服务器通过调用**Mod:init(InitArgs)**启动。

init的模板项

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% Description: 初始化服务器
%%--------------------------------------------------------------------
init([]) ->
{ok, #state{}}
1
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}.
1
2
3
4
5
6
7
8
9
10
11
12
13

Request(gen_server:call/2的第二个参数)作为handle_call/3的第一个参数重新出现。From是发送请求的客户端进程的PIDState则是客户端的当前状态。我们通常会返回**{reply, Reply, NewState}。在这种情况下, Reply会返回客户端,成为gen_server:call**的返回值。 NewState则是服务器接下来的状态。
    其他的返回值({noreply, ..}和{stop, ..})相对不太常用。 no reply会让服务器继续工作,但客户端会等待一个回复,所以服务器必须把回复的任务委派给其他进程。用适当的参数调用stop会停止服务器。

# 3.调用和播发

我们已经见过了gen_server:callhandle_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}.
1
2
3
4
5
6
7
8

这个处理函数通常只返回**{noreply, NewState}{stop, ...}**。前者改变服务器的状态,后者停止服务器

# 4.发给服务器的自发性消息

回调函数handle_info(Info, State)被用来处理发给服务器的自发性消息。自发性消息是一切未经显式调用gen_server:callgen_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}.
1
2
3
4
5
6
7
8

它的返回值和handle_cast相同。

# 监控树

监控树是一种由进程组成的树形结构。树的上级进程(监控器)监视着下级进程(工作器),如果下级进程挂了就会重启它们。监控树有两种。

一对一监控树
在一对一监控里,如果某个工作器崩溃了,就会被监控器重启。
一对多监控树
在一对多监控里,如果任何一个工作器崩溃了,所有工作进程都会被终止(通过调用相应回调模块里的terminate/2函数)然后重启。

监控器是用OTP supervisor行为创建的。这个行为用一个回调模块作为参数,里面指定了监控策略以及如何启动监控树里的各个工作进程。监控树通过以下形式的函数指定:

init(...) ->
{ok, {RestartStrategy, MaxRestarts, Time},
[Worker1, Worker2, ...]}.
1
2
3

这里的RestartStrategy是原子one_for_oneone_for_allMaxRestartsTime则指定“重启频率”。如果一个监控器在Time秒内执行了超过MaxRestarts次重启,那么这个监控器就会终止所有工作进程然后退出。这是为了防止出现一种情形,即某个进程崩溃、被重启,然后又因为相同原因崩溃而形成的无限循环。Worker1Worker2这些是描述如何启动各个工作进程的元组,稍后就会看到它们。

举例:

{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]}
	]}}.
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这个数据结构定义了一种监控策略。
Worker的格式是下面这种元组:

{Tag, {Mod, Func, ArgList},
    Restart,
    Shutdown,
    Type,
   [Mod1]
}
1
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的手册页。)
这些参数看上去很吓人,但其实不是。在实践中,你可以剪切粘贴之前面积服务器代码里的值,然后插入你的模块名。这对大多数用途来说足够了。

Last Updated: 4/23/2021, 12:47:12 PM