查看源代码 gen_server 行为

建议同时阅读 STDLIB 中的 gen_server 部分。

客户端-服务器原则

客户端-服务器模型以一个中央服务器和任意数量的客户端为特征。客户端-服务器模型用于资源管理操作,其中多个不同的客户端想要共享一个公共资源。服务器负责管理此资源。

---
title: Client Server Model
---

flowchart LR
    client1((Client))
    client2((Client))
    client3((Client))
    server((Server))

    client1 --> server
    server -.-> client1

    client2 --> server
    server -.-> client2

    client3 --> server
    server -.-> client3

    subgraph Legend
        direction LR

        start1[ ] -->|Query| stop1[ ]
        style start1 height:0px;
        style stop1 height:0px;

        start2[ ] -.->|Reply| stop2[ ]
        style start2 height:0px;
        style stop2 height:0px;
    end

示例

概述中提供了一个用纯 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() 来启动 gen_server

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

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

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

    如果省略名称,则不会注册 gen_server。而是必须使用它的进程 ID。名称也可以指定为 {global, Name},在这种情况下,将使用 global:register_name/2 注册 gen_server

  • 第二个参数 ch3 是回调模块的名称,回调函数位于该模块中。

    接口函数(start_link/0alloc/0free/1)与回调函数(init/1handle_call/3handle_cast/2)位于同一模块中。通常,良好的编程习惯是将对应于一个进程的代码包含在单个模块中。

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

  • 第四个参数 [] 是一个选项列表。有关可用选项,请参阅 gen_server

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

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

gen_server:start_link/4 是同步的。它在 gen_server 初始化并准备好接收请求之前不会返回。

如果 gen_server 是监督树的一部分,这意味着它是由监督者启动的,则必须使用 gen_server:start_link/4。还有另一个函数 gen_server:start/4,用于启动不属于监督树的独立 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}.

在这种情况下,新状态是更新后的可用通道列表 Chs2gen_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

如果 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/3

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