2  gen_server 行为

2 gen_server 行为

本节应与 gen_server(3) 手册页一起阅读,其中详细描述了所有接口函数和回调函数。

客户端-服务器模型的特点是中心服务器和任意数量的客户端。客户端-服务器模型用于资源管理操作,其中多个不同的客户端希望共享一个公共资源。服务器负责管理此资源。

IMAGE MISSING

图 2.1:   客户端-服务器模型

概述 中提供了使用纯 Erlang 编写的简单服务器的示例。可以使用 gen_server 重新实现该服务器,从而得到以下回调模块

-module(ch3).
-behaviour(gen_server).

-export([start_link/0]).
-export([alloc/0, free/1]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
    gen_server:start_link({local, ch3}, ch3, [], []).

alloc() ->
    gen_server:call(ch3, alloc).

free(Ch) ->
    gen_server:cast(ch3, {free, Ch}).

init(_Args) ->
    {ok, channels()}.

handle_call(alloc, _From, Chs) ->
    {Ch, Chs2} = alloc(Chs),
    {reply, Ch, Chs2}.

handle_cast({free, Ch}, Chs) ->
    Chs2 = free(Ch, Chs),
    {noreply, Chs2}.

代码将在下一节中解释。

在上一节的示例中,gen_server 通过调用 ch3:start_link() 启动

start_link() ->
    gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}

start_link 调用函数 gen_server:start_link/4。此函数会生成并链接到一个新进程,即 gen_server

  • 第一个参数,{local, ch3},指定了名称。gen_server 然后在本地注册为 ch3

    如果省略了名称,则 gen_server 不会注册。相反,必须使用其 pid。名称也可以指定为 {global, Name},在这种情况下,gen_server 使用 global:register_name/2 注册。

  • 第二个参数,ch3,是回调模块的名称,即包含回调函数的模块。

    接口函数 (start_linkallocfree) 然后位于与回调函数 (inithandle_callhandle_cast) 相同的模块中。这通常是良好的编程实践,将与一个进程对应的代码包含在一个模块中。

  • 第三个参数,[],是一个将按原样传递给回调函数 init 的项。在这里,init 不需要任何输入数据,并忽略该参数。

  • 第四个参数,[],是一个选项列表。有关可用选项,请参阅 gen_server(3) 手册页。

如果名称注册成功,则新 gen_server 进程将调用回调函数 ch3:init([])。预计 init 将返回 {ok, State},其中 Stategen_server 的内部状态。在这种情况下,状态是可用的通道。

init(_Args) ->
    {ok, channels()}.

gen_server:start_link 是同步的。它不会返回,直到 gen_server 初始化并准备接收请求为止。

如果 gen_server 是监控树的一部分,即由监控器启动,则必须使用 gen_server:start_link。还有另一个函数 gen_server:start 用于启动独立的 gen_server,即不是监控树一部分的 gen_server

同步请求 alloc() 使用 gen_server:call/2 实现

alloc() ->
    gen_server:call(ch3, alloc).

ch3gen_server 的名称,必须与用于启动它的名称一致。alloc 是实际的请求。

将请求转换为消息并发送到 gen_server。收到请求后,gen_server 将调用 handle_call(Request, From, State),预计它将返回一个元组 {reply,Reply,State1}Reply 是要发送回客户端的回复,而 State1gen_server 状态的新值。

handle_call(alloc, _From, Chs) ->
    {Ch, Chs2} = alloc(Chs),
    {reply, Ch, Chs2}.

在这种情况下,回复是分配的通道 Ch,新状态是剩余可用通道的集合 Chs2

因此,调用 ch3:alloc() 会返回分配的通道 Ch,然后 gen_server 将等待新的请求,现在具有更新的可用通道列表。

异步请求 free(Ch) 使用 gen_server:cast/2 实现

free(Ch) ->
    gen_server:cast(ch3, {free, Ch}).

ch3gen_server 的名称。{free, Ch} 是实际的请求。

将请求转换为消息并发送到 gen_servercast 以及 free 然后返回 ok

收到请求后,gen_server 将调用 handle_cast(Request, State),预计它将返回一个元组 {noreply,State1}State1gen_server 状态的新值。

handle_cast({free, Ch}, Chs) ->
    Chs2 = free(Ch, Chs),
    {noreply, Chs2}.

在这种情况下,新状态是更新后的可用通道列表 Chs2。现在,gen_server 已准备好接收新的请求。

如果 gen_server 是监控树的一部分,则不需要停止函数。gen_server 会被其监控器自动终止。这究竟是如何完成的,由在监控器中设置的 关闭策略 定义。

如果需要在终止之前进行清理,则关闭策略必须是超时值,并且必须将 gen_server 设置为在函数 init 中捕获退出信号。当被命令关闭时,gen_server 然后调用回调函数 terminate(shutdown, State)

init(Args) ->
    ...,
    process_flag(trap_exit, true),
    ...,
    {ok, State}.

...

terminate(shutdown, State) ->
    ..code for cleaning up here..
    ok.

如果 gen_server 不是监控树的一部分,则停止函数可能有用,例如

...
export([stop/0]).
...

stop() ->
    gen_server:cast(ch3, stop).
...

handle_cast(stop, State) ->
    {stop, normal, State};
handle_cast({free, Ch}, State) ->
    ....

...

terminate(normal, State) ->
    ok.

处理 stop 请求的回调函数返回一个元组 {stop,normal,State1},其中 normal 指定它是一个正常终止,而 State1gen_server 状态的新值。这将导致 gen_server 调用 terminate(normal, State1),然后它将正常终止。

如果 gen_server 要能够接收除请求以外的其他消息,则必须实现回调函数 handle_info(Info, State) 来处理它们。其他消息的示例包括退出消息(如果 gen_server 链接到其他进程(而不是监控器)并捕获退出信号)。

handle_info({'EXIT', Pid, Reason}, State) ->
    ..code to handle exits here..
    {noreply, State1}.

还必须实现 code_change 方法。

code_change(OldVsn, State, Extra) ->
    ..code to convert state (and more) during code change
    {ok, NewState}.