查看源代码 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/0
、alloc/0
和free/1
)与回调函数(init/1
、handle_call/3
和handle_cast/2
)位于同一模块中。通常,良好的编程习惯是将对应于一个进程的代码包含在单个模块中。第三个参数
[]
是一个原样传递给回调函数init
的项。在这里,init
不需要任何输入数据,并且忽略该参数。第四个参数
[]
是一个选项列表。有关可用选项,请参阅gen_server
。
如果名称注册成功,则新的 gen_server
进程调用回调函数 ch3:init([])
。init
预期返回 {ok, State}
,其中 State
是 gen_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).
ch3
是 gen_server
的名称,并且必须与用于启动它的名称一致。alloc
是实际的请求。
该请求被制成消息并发送到 gen_server
。收到请求时,gen_server
调用 handle_call(Request, From, State)
,该函数预期返回一个元组 {reply,Reply,State1}
。Reply
是要发送回客户端的回复,而 State1
是 gen_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}).
ch3
是 gen_server
的名称。{free, Ch}
是实际的请求。
该请求被制成消息并发送到 gen_server
。cast
,因此 free
,然后返回 ok
。
收到请求时,gen_server
调用 handle_cast(Request, State)
,该函数预期返回一个元组 {noreply,State1}
。State1
是 gen_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
如果 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
指定这是一个正常终止,而 State1
是 gen_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}.