2

以下代码来自“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?

4

1 回答 1

3

这个例子是对 Erlang 中使用的行为概念的介绍。它说明了如何分两部分构建服务器:

第一部分是模块 server1,它只包含任何服务器都可以使用的通用特性。它的作用是维护可用的一些信息(状态变量)并准备好回答一些请求。这就是 gen_server 行为所做的,具有更多功能。

第二部分是模块 name_server。这个描述了特定服务器的功能。它为服务器用户和内部函数(回调)实现接口,这些函数描述了对每个特定用户请求要做什么。

让我们遵循 3 个 shell 命令(见最后的图表):

server1:开始(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 并行,有一个名为 name_server 的新生命进程正在运行并等待请求。请注意,通常此步骤不是由用户完成,而是由应用程序完成。这就是为什么回调模块中没有执行此操作的接口,并且在通用服务器中直接调用 start 函数的原因。

名称服务器:添加(乔,“在家”)。用户在服务器中添加一条信息,调用 name_server 的 add 函数。这个接口是为了隐藏调用服务器的机制,它运行在客户端进程中。

1/ add 函数使用两个参数调用服务器的 rpc 例程rpc(name_server, {add, Name, Place}):回调模块和请求本身{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,这里就可以了。

名称服务器:查找(乔)。用户想从服务器获取信息。流程和之前一模一样,是通用服务器的兴趣所在。无论请求是什么,它都会做同样的工作。当您查看 gen_server 行为时,您会看到对服务器的访问有多种类型,例如 call、cast、info... 因此,如果我们查看此请求的流程:

1/ 使用回调模块和请求调用 rpcrpc(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/ 等待接收块的客户端得到消息{Name, Response},然后可以提取响应并将其返回给shell,现在它是joe所在的地方:“在家”。

下图显示了不同的消息交换:

前面描述的 3 个步骤的序列图

于 2013-11-04T07:38:17.200 回答