难以理解 Erlang Gen_Server 架构

Posted

技术标签:

【中文标题】难以理解 Erlang Gen_Server 架构【英文标题】:Trouble Understanding Erlang Gen_Server Architecture 【发布时间】:2017-10-14 10:26:40 【问题描述】:

我正处于学习 Erlang 的早期阶段,我需要一些进一步的帮助。不确定这是否会得到任何阳光,但它就在这里......我正在寻找关于该示例如何工作的流程图。

示例代码: https://github.com/erlware/Erlang-and-OTP-in-Action-Source/blob/master/chapter_03/tr_server.erl

让我解释一下我的问题......

1> tr_server:start_link().

我明白这一点,它调用 start_link(?DEFAULT_PORT) 来调用 gen_server:start_link -- 这实际上得到了对 tr_server(?MODULE) init([Port]) 的调用。

init([Port]) ->
    ok, LSock = gen_tcp:listen(Port, [active, true]),
    ok, #stateport = Port, lsock = LSock, 0.

这也是可以理解的。您将数据发送到服务器, gen_server:handle_info/2 得到处理,因此调用 ?MODULE:handle_info/2 - 这是一个案例,并且由于我们在 ?MODULE:init 中返回了超时,它将与 handle_info(超时,#statelsock = LSock = 状态)。

好的,这是有道理的。

这就是我开始对 Erlang 的流程感到困惑的地方

这几天我一直在阅读这方面的在线资源(包括 Erlang-and-OTP-in-action)——这个例子的来源——还有:http://learnyousomeerlang.com/clients-and-servers

我不确定 Erlang 服务器的流程是如何工作的。我的理解是,发送到服务器的任何消息都会由 gen_server:handle_info/2 处理,如果它们超出范围 - 意味着如果它们没有配置或匹配任何其他 gen_server:handle_call/3?这意味着,任何 TCP 数据都由 gen_server:handle_info/2 自动处理——它会回调 ?MODULE:handle_info?

我不明白handle_call、handle_cast 如何以及在何处进入服务器架构——我也不了解服务器从客户端到服务器架构的流程(直到我感到困惑)。我认为这对于说明流程图非常重要,就像电路图一样。

这里是主要问题: 当客户端发送以下内容时,服务器的流程是什么:

lists:reverse([1,2,3]).

在纯文本中,最好获得流程图以了解其工作原理。从文本和示例来看,它不是很清楚它是如何工作的。目前还不清楚我们为什么需要:

get_count() ->
    gen_server:call(?SERVER, get_count).

stop() ->
    gen_server:cast(?SERVER, stop).

感谢任何答案,我知道解释起来可能很累!如有语法错误,请见谅!

【问题讨论】:

【参考方案1】:

在数据来自 tcp 端口和服务器通过 handle_info 回调处理的情况下,您似乎对流程有了很好的了解。这是一种客户端/服务器交互,在 Erlang 代码和连接到端口的一些外部客户端之间。但是在 Erlang 系统中,Erlang 进程之间也存在客户端/服务器关系,双方都在运行 Erlang 代码。 (即使只是 gen_server 进程和 Erlang 命令 shell 进程。)

当您使用 gen_server:call/cast 客户端函数时,它们会以您从未见过的方式包装您的消息,但接收的 gen_server 进程会识别这一点并使用它对消息进行分类,然后将解包的消息传递给对应的handle_call/handle_cast。除此之外,流程与 tcp 端口上的传入数据相同:在这两种情况下,它只是到服务器的异步消息,被接收并分派到正确的函数。同时在客户端,gen_server:call() 函数将等待回复(发送者的 Pid 包含在包装器中),而 gen_server:cast() 立即执行。

这些实际上只是便利功能。原则上, gen_server 可能只有一个回调来处理各种消息,让你来编码它是调用还是强制转换以及如何做出反应。但是通过提供这些库函数并为您分类消息,它可以降低将调用视为强制转换或反之亦然的风险,或者将带外消息与正确的调用/强制转换混淆。流程在所有情况下都是相同的:客户端 -> 服务器 -> 回调 [ -> 服务器回复 -> 客户端 ]。

因此,您可以使用?SERVER ! get_count, self() 实现get_count() 函数,在handle_info() 回调中而不是在handle_call() 中处理该消息。 (只是不要忘记将回复发送回消息中包含的 Pid,否则客户端将永远卡住。)

或者您可以完全跳过实现 get_count() 之类的用户 API 函数,并告诉您的用户只需将 get_count, self() 发送到服务器进程并等待回复(其形状也必须记录在案)。但是,您以后无法更改这些消息在后台的外观细节。 gen_server:call/cast 函数可帮助您隐藏这些杂乱的实现细节,并降低您搞砸客户端/服务器通信的可能性。

希望这会有所帮助。

【讨论】:

谢谢!当我对每个函数进行一些 io:format 调用以查看流程时,这更有意义。【参考方案2】:

我正处于学习 Erlang 的早期阶段,我需要一些进一步的帮助

    看一些简单的非 gen_server 客户端-服务器示例。尝试为您自己的客户端-服务器想出一个简单的想法并编写代码。 了解如何使用模块名称参数化简单服务器。 了解 gen_server 和行为。 练习将简单服务器转换为 gen_server。使用带有拆分窗口的文本编辑器非常方便。 了解 gen_tcp 和套接字。 查看将 gen_tcp 和套接字与 gen_server 相结合的示例。

看这里:

http://erlang.org/doc/design_principles/des_princ.html

我不会从第 6 步开始,您似乎正在这样做。

这意味着,任何 TCP 数据都由 gen_server:handle_info/2 -- 回调到 ?MODULE:handle_info?

没有回调。 TCP 数据绕过整个 gen_server 架构。 TCP 数据与其他入侵者一起通过后门进入。所以 gen_server:handle_info() 可以处理它们。 handle_info() 检查服务器邮箱中是否存在与指定为 handle_info() 参数的模式匹配的任何消息。

需要对 TCP 数据做的任何事情都在 handle_info() 中完成。当然,如果你需要在handle_info()中做一些复杂的数据处理,你可以随时调用辅助函数来计算一些结果:

handle_info(tcp, Socket, RawData, State) ->
    Result1 = computerInGermanyProcess(RawData),
    Result2 = computerInFranceProcess(RawData),
    Result3 = computerInRussiaProcess(RawData),    
    Result4 = State#state.stored_val,

    gen_tcp:send(Socket, [Result1, Result2, Result3, Result4]),
    noreply, State;  %%Because a TCP message landed in the mailbox with no From value, 
                       %%do not reply to From, and do not mess with the value in State.

 computerInGermanyProcess(RawData) ->
          %% Possibly use gen_tcp and sockets again to send a message
          %% to another computer to get some data in order to
          %% calculate Result1:
          Result1.
 computerInFranceProcess(RawData) ->
          ...
          Result2.
 computerInRussiaProcess(RawData) ->
          ...
          Result3.         

我不明白的是handle_call,handle_cast如何以及在哪里播放 进入服务器架构——我也不了解 来自客户端->服务器架构的服务器(直到我得到 使困惑)。我认为这对于说明图表非常重要 流程很像电路图。

Client:                                                                    
+------------------------------------------------------+------------------------------------------------------+
| my_request() ->                                      |   handle_call(dostuff, Val, ClientPid, State) ->   |
|     Request = dostuff, 10,                         |       %%Do something with Val, State                 |
|     Server = ?MODULE,                                |       Response = result, 45,                       |
|     Response = gen_server:call(Server, Request).     |       NewState = ....,                               |
|                            |                         |       Response, NewState.                          |
|                            |       from gen_server:  |                                                      |
|                            |            start_link() |                                     ^                |
|                            |                 |       |                                     |                |
+----------------------------+-----------------+-------+-------------------------------------+----------------+
                             |                 |                                             |
                             |                 |                                             |
+----------------------------+-----------------+-------+                                     |
|-module(gen_server).        |                 |       |                                     |
|-export([call/2,....]).     V                 |       |                                     |
|                                              |       |                                     |
|call(Server, Request) ->                      V       |                                     |
|  Server ! request, Request, call, self(), Module --+-->+                                 |                     
|  receive                                             |   |                                 ^                
|      reply, Response, Server ->                    |   |                                 |
|          Response      ^                             |   V                                 |
|  end.                  |                             |   |                                 |
+------------------------+-----------------------------+   |                                 |
|   Mailbox              |                             |   |                                 |
|                        |                             |   |                                 |
|       reply, Response, Server  <----------<--------+---+--------------<--------------+   |
|                                                      |   V                             ^   ^      
+------------------------------------------------------+   |                             |   |
                                                           |                             |   |
                                                           |                             |   |
Server:                                                    |                             |   |
+------------------------------------------------------+   |                             |   |
|    Mailbox                                           |   |                             |   |
|                                                      |   V                             ^   ^
|        request, Request, call, ClientPid, Module <-+---+                             |   |
|                            |                         |                                 |   |
+----------------------------+-------------------------+-----------------------------+   |   |                 
|                            |                                                       |   |   |
|loop(State) ->              |                                                       |   |   |
|    receive                 V                                                       |   ^   ^
|        request, Request, call, ClientPid, Module  ->                             |   |   |           ^
|            Response, NewState = Module:handle_call(Request, ClientPid, State ---+---|-->+           |
|            ClientPid ! reply, Response, self(), ----------->---------------------+-->+            To Client
|            loop(NewState);                                                         |                   ^
|        request, Request, cast, ClientPid, Module ->                              |                   |
|            NewState = Module:handle_cast(Request, State), ------->---------->------|----->------------>+
|            loop(NewState);                                                         |
|        ...                                                                         |
|        ...                                                                         |                                      
|    end.                                                                            |
+------------------------------------------------------------------------------------+

客户端调用gen_server:call()时的流程:

    客户端调用gen_server:start_link() 至少指定定义handle_call/handle_cast 函数的模块。

    客户端调用gen_server:call(ServerName, Request),通常封装在接口函数中。

    gen_server:call(ServerName, Request) 被定义为send a message to the server,类似这样:

     ServerName ! request, Request, call, self(), ModName.
    

    ModName 之前绑定到在 gen_server:start_link() 中指定的原子:第二个参数是您指定包含函数 handle_call()、handle_cast() 等定义的模块名称的位置。

    当服务器接收到该消息时,服务器调用ModName:handle_call(),并且您的 ModName:handle_call() 代码对请求执行一些操作:

    handle_call(Request, ClientPid, ServerLoopValue) ->
        %%Compute some result using information in Request/ServerLoopValue
    

    您的 ModName:handle_call() 函数的最后一行告诉服务器将什么作为a response 发送回客户端:

      Response, NewServerLoopValue.
    

    然后服务器会做这样的事情:

      From ! reply, Response, ServerPid.
      loop(NewServerLoopValue).
    

    并且NewServerLoopValue 成为服务器loop() 的新全局 变量。每个服务器都有一个 loop() 函数,看起来像这样:

    loop(ServerLoopValue) ->
        receive
            request, dothis, From ->
                Result1 = ...SomeValue + 5....,
                From ! Result1, self(),
                loop(NewServerLoopValue);
            request, dothat, 10, From ->
                Result2 = ... SomeValue - 10...,
                From ! Result2, self(),
                loop(NewServerLoopValue);
            request, stop, From
                %%do not call loop()
        end.
    

    ServerLoopValue 就像一个 global 变量,所有不同的请求都可以看到。各种 gen_server 请求处理程序可以使用存储在 ServerLoopValue 中的信息来计算响应,或者它们可以将信息添加到其他请求处理程序将来可以使用的 ServerLoopValue。

使用带有active, true, packet, 4 的 TCP 套接字进入 gen_server 后门的流:

    客户端调用gen_tcp:send()

    在服务器端的socket,Erlang从socket中读取数据,构造一个消息元组,将消息元组放入server's mailbox

    服务器从邮箱中检索tcp, ...消息并调用handle_info()

    handle_info() 调用gen_tcp:send(Socket, Response) 将响应发送回客户端。

    handle_info() 的最后一行告诉服务器在调用服务器的 loop() 函数时使用什么值:

    noreply, SomeValue   =>  loop(SomeValue)
    

使用带有active, false, packet, 0 的 TCP 套接字进入 gen_server 后门的流:

Erlang gen_tcp not receiving anything

【讨论】:

哇,这张图是你自己画的吗? @IlyaVassilevsky,是的!这有意义吗?

以上是关于难以理解 Erlang Gen_Server 架构的主要内容,如果未能解决你的问题,请参考以下文章

如何直观地描述 gen_server?

Erlang 源码分析之 Gen_Server

Erlang:无法创建 gen_server:call()

在 Erlang 的 gen_server 中实现代码交换

Erlang,尝试制作 gen_server: 调用有很多响应

Erlang:gen_server 还是我自己的自定义服务器?