首页 > 解决方案 > 如何实现可以成为任何特定服务器的通用 Erlang 服务器

问题描述

目前我正在试验 Erlang,并希望实现一种由 Joe Armstrong 描述的通用服务器(如这个)。总的想法是创建一个通用服务器,我们以后可以告诉它成为一个特定的服务器,如下所示:

universal_server() ->
    receive
        {become, F} ->
            F()
    end.

还有一些特定的服务器:

factorial_server() ->
    receive
        {From, N} ->
            From ! factorial(N),
            factorial_server()
    end.

factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).

最后向通用服务器发送“成为阶乘服务器”消息:

test() ->
    Pid = spawn(fun universal_server/0),
    Pid ! {become, fun factorial_server/0},
    Pid ! {self(), 50},
    receive
        X -> X
    end.

我想做的是实现一个可以接受多个后续“成为”消息的通用服务器(这样我就可以发送“成为阶乘服务器”消息,然后发送“成为其他类型的特定服务器”消息......) .

一种天真的方法是要求每个特定的服务器实现都将{become, F}模式包含在一个receive子句中。也许我可以有一个行为来定义所有特定服务器的一般形状(包含{become, F}子句)并将其他消息转发到回调。

我的问题是,如何以干净、聪明的方式实施这样的案例?

标签: erlang

解决方案


这是我的:

-module(myserver).
-export([start/0, init/0]).


start() ->
    erlang:spawn_link(?MODULE, init, []).


init() ->
    State = undefined, % You may want to do something at startup
    loop(State).
    % if something went wrong comment above line and uncomment below line:
    % exit(element(2, catch loop(State))).


loop(MyState) ->
    Msg =
        receive
            Any ->
                Any
        end,
    handle_message(Msg, MyState).

% We got a message for becoming something:
handle_message({become, Mod, InitArgument}, _) ->
    % Also our callback may want to do something at startup:
    CallbackState = Mod:init(InitArgument),
    loop({Mod, CallbackState});
% We got a message and we have a callback:
handle_message(Other, {Mod, CallbackState}) ->
    case Mod:handle_message(Other, CallbackState) of
        stop ->
            loop(undefined);
        NewCallbackState ->
            loop({Mod, NewCallbackState})
    end;
% We got a message and we Don't have a callback:
handle_message(Other, undefined) ->
    io:format("Don't have any callback for handling ~p~n", [Other]),
    loop(undefined).

我还为我的服务器编写了一个简单的counter程序:

-module(counter).
-export([init/1, handle_message/2]).


init(Start) ->
    Start.

handle_message(inc, Number) ->
    Number + 1;
handle_message(dec, Number) ->
    Number - 1;
handle_message({From, what_is}, Number) ->
    From ! Number;
handle_message(stop, _) ->
    stop;
handle_message(Other, Number) ->
    io:format("counter got unknown message ~p~n", [Other]),
    Number.

让我们测试一下:

Eshell V10.1  (abort with ^G)
1> S = myserver:start().
<0.79.0>

2> S ! hello.
Don't have any callback for handling hello
hello

3> S ! {become, counter, 10}.
{become,counter,10}

4> S ! hi.
counter got unknown message hi
hi

5> S ! inc.
inc
6> S ! dec.
dec
7> S ! dec.
dec

8> S ! {self(), what_is}.
{<0.77.0>,what_is}

9> flush().
Shell got 9
ok

10> S ! stop.             
stop

11> S ! inc.              
Don't have any callback for handling inc
inc

我们应该怎么做才能完成它?

如您所见,这不是生产就绪代码,我们应该:

  • 有办法为初始化设置超时。
  • 有办法设置进程生成选项。
  • 有办法在本地或全局注册流程或使用自定义流程注册表。
  • 调用回调函数try catch
  • 确保消息回复是针对当前消息传递的,而不是针对我们的进程之前发送的其他消息!(什么gen模块提供为call)。
  • 当我们的 starter 进程死亡时杀死我们自己,如果 starter 链接到我们,就不要成为僵尸进程!
  • 在每个回调的最后调用一个函数,如果有的话,让他们清理这些东西(你可以命名它terminate)。
  • 与OTP模块兼容sys,所以我们应该定义它的回调函数。请参阅sys 回调函数。然后我们可以将我们的进程切换到调试模式,查看它的 I/O,在重新加载代码时改变它的状态,等等。

请注意,proc_libgen模块可以帮助您完成大部分工作。


推荐阅读