2 gen_server 行为
本节应与 gen_server(3) 手册页一起阅读,其中详细描述了所有接口函数和回调函数。
2.1 客户端-服务器原则
客户端-服务器模型的特点是中心服务器和任意数量的客户端。客户端-服务器模型用于资源管理操作,其中多个不同的客户端希望共享一个公共资源。服务器负责管理此资源。
2.2 示例
在 概述 中提供了使用纯 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}.
代码将在下一节中解释。
2.3 启动 Gen_Server
在上一节的示例中,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_link、alloc 和 free) 然后位于与回调函数 (init、handle_call 和 handle_cast) 相同的模块中。这通常是良好的编程实践,将与一个进程对应的代码包含在一个模块中。
-
第三个参数,[],是一个将按原样传递给回调函数 init 的项。在这里,init 不需要任何输入数据,并忽略该参数。
-
第四个参数,[],是一个选项列表。有关可用选项,请参阅 gen_server(3) 手册页。
如果名称注册成功,则新 gen_server 进程将调用回调函数 ch3:init([])。预计 init 将返回 {ok, State},其中 State 是 gen_server 的内部状态。在这种情况下,状态是可用的通道。
init(_Args) -> {ok, channels()}.
gen_server:start_link 是同步的。它不会返回,直到 gen_server 初始化并准备接收请求为止。
如果 gen_server 是监控树的一部分,即由监控器启动,则必须使用 gen_server:start_link。还有另一个函数 gen_server:start 用于启动独立的 gen_server,即不是监控树一部分的 gen_server。
2.4 同步请求 - 调用
同步请求 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 将等待新的请求,现在具有更新的可用通道列表。
2.5 异步请求 - 投递
异步请求 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 已准备好接收新的请求。
2.6 停止
在监控树中
如果 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_Servers
如果 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),然后它将正常终止。
2.7 处理其他消息
如果 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}.