以下代码来自“Programming Erlang, 2nd Edition”。它是如何在 Erlang 中实现通用服务器的示例。
-module(server1).
-export([start/2, rpc/2]).
start(Name, Mod) ->
register(Name, spawn(fun() -> loop(Name, Mod, Mod:init()) end)).
rpc(Name, Request) ->
Name ! {self(), Request},
receive
{Name, Response} -> Response
end.
loop(Name, Mod, State) ->
receive
{From, Request} ->
{Response, State1} = Mod:handle(Request, State),
From ! {Name, Response},
loop(Name, Mod, State1)
end.
-module(name_server).
-export([init/0, add/2, find/1, handle/2]).
-import(server1, [rpc/2]).
%% client routines
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
find(Name) -> rpc(name_server, {find, Name}).
%% callback routines
init() -> dict:new().
handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({find, Name}, Dict) -> {dict:find(Name, Dict), Dict}.
server1:start(name_server, name_server).
name_server:add(joe, "at home").
name_server:find(joe).
我非常努力地理解消息的工作流程。请您帮我理解在执行函数server1:start、name_server:add 和name_server:find 过程中这个服务器实现的工作流程吗?
最佳答案
这个例子是对 Erlang 中使用的行为概念的介绍。它分两部分说明了如何构建服务器:
第一部分是模块 server1,它只包含任何服务器都可以使用的通用功能。它的作用是维持一些可用的
信息(状态变量)并准备好回答一些请求。这就是 gen_server 行为所做的,具有更多功能。
第二部分是模块name_server。这描述了特定服务器的作用。它为服务器的用户和内部函数(回调)实现接口(interface),这些函数描述了对每个特定用户请求执行的操作。
让我们遵循 3 个 shell 命令(见最后的图表):
server1:start(name_server, name_server)。 用户调用通用服务器的启动例程,提供 2 个信息(带有保存值)、他想要启动的服务器的名称以及包含回调的模块的名称。有了这个通用启动例程
1/回调name_server的init例程,获取服务器状态Mod:init()
,可以看到通用部分不知道会保留哪种信息;状态由 name_server:init/0 例程创建,这是第一个回调函数。这是一本空字典dict:new()
.
2/产生一个调用通用服务器循环的新进程,带有 3 个信息(服务器名称、回调模块和初始服务器状态)spawn(fun() -> loop(Name, Mod, Mod:init())
.循环本身只是开始并在接收块中等待 {,} 形式的消息。
3/注册名为 name_server register(Name, spawn(fun() -> loop(Name, Mod, Mod:init()) end))
的新进程.
4/返回 shell 。
此时,与 shell 并行的是,有一个名为 name_server 的新存活进程正在运行并等待请求。请注意,通常这一步不是由用户完成的,而是由应用程序完成的。这就是为什么在回调模块中没有执行此操作的接口(interface),并且在通用服务器中直接调用启动函数的原因。
name_server:add(joe, “在家”)。 用户在服务器中添加信息,调用 name_server 的添加函数。这个接口(interface)在这里隐藏了调用服务器的机制,它运行在客户端进程中。
1/add函数用2个参数rpc(name_server, {add, Name, Place})
调用服务器的rpc例程:回调模块和请求本身 {add, Name, Place}
. rpc 例程仍然在客户端进程中执行,
2/它为服务器构建一条消息,由 2 个信息组成:客户端进程的 pid(这里是 shell)和请求本身,然后将其发送到指定的服务器:Name ! {self(), Request},
3/客户端等待响应。请记住,我们让服务器在循环例程中等待消息。
4/发送的消息符合预期格式 {From, Request}
的服务器,所以服务器进入消息处理。首先它用 2 个参数回调 name_server 模块:请求和当前状态 Mod:handle(Request, State)
.目的是要有一个通用的服务器代码,所以它不知道如何处理请求。在name_server:handle/2函数中,正确的操作就完成了。由于模式匹配,子句 handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
被调用并创建一个新的字典来存储键/值对 Name/Place(这里是 joe/"at home")。新字典与元组 {ok,NewDict} 中的响应一起返回。
5/现在通用服务器可以构建答案并将其返回给客户端 From ! {Name, Response},
使用新状态重新进入循环 loop(Name, Mod, State1)
并等待下一个请求。
6/等待接收块的客户端得到消息{Name, Response},然后可以提取响应并将其返回给shell,这里就可以了。
name_server:find(joe)。 用户想从服务器获取信息。过程和之前完全一样,是通用服务器的兴趣所在。无论请求是什么,它都做同样的工作。当您查看 gen_server 行为时,您将看到对服务器的几种访问方式,例如调用、转换、信息……因此,如果我们查看此请求的流程:
1/使用回调模块调用 rpc 并请求 rpc(name_server, {find, Name}).
2/使用客户端pid和请求向服务器发送消息
3/等待答案
4/服务端收到消息并用请求回调name_server Mod:handle(Request, State),
它从句柄 handle({find, Name}, Dict) -> {dict:find(Name, Dict), Dict}.
得到响应它返回字典搜索的结果和字典本身。
5/服务器构建答案并发送给客户端 From ! {Name, Response},
并以相同的状态重新进入循环,等待下一个请求。
6/等待接收块的客户端得到消息{名称,响应},然后可以提取响应并将其返回给shell,现在是joe所在的地方:“在家”。
下图显示了不同的消息交换:
关于concurrency - 了解 Erlang 中通用服务器实现中消息的工作流程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19757020/