Erlang:应用程序间通信

时间:2014-01-17 21:35:22

标签: erlang

继续我的Erlands旅程我正在使用OTP开发简单的IM系统。

有两个OTP应用程序:服务器(一个实例)和一个客户端(多个实例)。设置如下所示:

╭── node1@host ──╮
│  Server        │
│   └gen_server  │
╰────────────────╯

╭── node2@host ──╮
│  Client        │
│   └gen_server  │
╰────────────────╯
╭── node3@host ──╮
│  Client        │
│   └gen_server  │
╰────────────────╯
      ...

客户端功能

使用Erlang shell,我们可以向客户端应用程序发出下一个命令:

  1. 连接到服务器并从中接收随机名称(我喜欢turbo-octopusminiature-octocat等名称。)
  2. 获取其他已连接客户的列表。
  3. 使用指定的名称向客户端发送消息。
  4. 向所有客户发送消息(广播)。
  5. 客户端也应该能够在收到时在stdout中打印消息。

    实施细节

    所有消息都通过服务器。

    服务器和客户端应用程序都包含负责处理消息的gen_servers(chat_server.erl和chat_client.erl)。服务器的chat_server进程注册为全局并在所有节点上可见:

    %% chat_server.erl
    start_link() ->
        gen_server:start_link({global, ?SERVER}, ?MODULE, [], []).
    

    当客户端connects时,它发送其gen_server进程的pid。这样做,我们可以将客户端pids存储在服务器状态,以区分它们并发送/广播消息。

    %% chat_client.erl
    connect() ->
        Res = gen_server:call({global, ?REMOTE_SERVER}, {connect, client_pid() ...}),
    ...
    
    %% pid of the client's gen_server
    client_pid() -> whereis(?CLIENT_SERVER). 
    

    服务器connect句柄:

    %% chat_server.erl
    handle_call({connect, Pid}, _From, State) ->
        %% doing stuff like generating unique name, 
        %% adding client to list, etc.
        {reply, {connected, Name}, UpdatedState}.
    

    消息(双关语)

    嗯,这很简单。服务器处理来自客户端的强制转换,通过给定名称搜索收件人的pid并向其发送消息/广播给每个人。就是这样。

    将问一个问题

    在开发这个系统时,我想知道选择的方法是否合适。我的意思是,

    1. 传递客户端的gen_server pid似乎或多或少可以接受,至少因为它允许唯一标识客户端并在两端使用所有gen_server火力。这是你的方式吗?
    2. 我已经在这里和那里读过,显式接口(调用导出函数)比直接消息传递更好(我在客户端使用gen_server:calls做的事情)。有没有办法解决这个问题(例如rpc),或者没关系?
    3. 给定相同的设置(具有服务器应用程序的节点和具有客户端的N个节点),您是否将使用与gen_servers相同的方法,或者 是一种我不知道的更好的方法?

2 个答案:

答案 0 :(得分:4)

就我个人而言,我认为您的架构略有偏差。

如果您希望客户端接收传入消息(例如,当另一个客户端向您发送消息或正在进行广播时),那么当前似乎不是服务器可以向其发送消息的过程。 gen_server通常不是那个的载体;它主要用于服务器进程。

我认为应该为每个客户启动一个新流程。该过程将成为特定客户端的主循环。如果您(用户)想要执行某些操作,则会向该特定进程发送消息。这可以隐藏在函数调用之后。然后客户端的主循环将与服务器进行交互。

客户端的主循环 - 这是一个单独的进程,随时可以接收消息,因此如果有人发送给您,服务器可以向您的客户端发送消息。

BTW:我希望您的定义?SERVER和?REMOTE_SERVER是相同的,因为如果我理解正确,它们都会引用全局注册的聊天服务器。最好坚持使用一个独特的名称。

另一个问题是您通常不公开gen_server:call()方法。客户端只调用chat_server模块中的方法,而不知道服务器的名称或服务器的名称(这就是Erlang的优点!)。

在chat_server.erl中你输入这样的代码;基本上是客户端API。您会注意到,在chat_client.erl中,只会调用chat_server模块中的方法。非常干净透明!

%% let a new client connect, all we need is it's Pid
new_client(Pid) ->
   gen_server:call({global, ?SERVER}, {connect, Pid}).

send_msg(From, To) ->
   gen_server:call({global, ?SERVER}, {sendmsg, From, To}).

logout_client(Pid) ->
    gen_srver:call({global, ?SERVER), {exit_client, Pid}).

下面的客户端代码(故意)不会自动注册客户端的Pid,除非您将系统限制为每个节点只允许一个客户端。您不能在同一名称下注册多个Pid。 下面的代码不会将新的Pid注册为名称,但如果这是您想要或需要的话,可以将其作为琐碎的代码。

通常客户端的代码如下:

%% start a new client, we spawn a new process for this
%% particular client and return their Pid, to be used
%% when you want your client to do something
connect(Server) ->
   spawn( ?MODULE, start_client, [] ).

%% client startup code
start_client() ->
   %% Initialize client state, if you wish
   State = 42,
   %% Now connect to chat server
   chat_server:new_client( self() ),
   %% And fall into our own main loop
   client_loop( State ).

%% This is the client's main loop
client_loop( State ) ->
   %% Wait for stuff to happen ...
   receive
       %% chat server sends message to us
       {message , Msg, From} ->
             io:format("~p sais ~p~n", [From, Msg]),
             client_loop( State );
       %% message sending is delegated to the server - see your own protocol
       {send, Msg, To} ->
             chat_server:send_msg(Msg, To),
             client_loop( State );
       %% terminate?
       done ->
             %% de-register with server
             chat_server:logout_client(self())
    end.

现在所需要的只是一些实用程序功能,可以与您的客户端进程进行交互,如下所示。请注意,如果您通过在本地注册客户端的Pid来“每个Erlang节点是单个客户端”,则可以明确地删除传递Pid。但机制保持不变。

send_message(Pid, Msg, To) ->
    Pid ! {send, Msg, To}.

logout(Pid) ->
    Pid ! done.

%% If you force your client's Pid to be registered to e.g. 'registered_name'
%% it would look like
send_message(Msg, To) ->
    registered_name ! {send, Msg, To}.

答案 1 :(得分:3)

我同意haavee的说法,你的架构不是我所期望的,但那是因为我想象的是更低层次的TCP。

关于你的问题:

  

绕过客户端的gen_server pid似乎或多或少可以接受   至少是因为它允许唯一标识客户端并使用所有客户端   两端的gen_server火力。这是你的方式吗?

当然,我觉得这部分代码没有错。您的服务器在PID和客户端名称之间保持映射,这就像调用register/2,但只有服务器获取映射,您可以控制它的工作方式。


  

我已经在这里和那里读过显式接口(调用导出   函数)比直接消息传递更好(我在客户端做的事情   与gen_server:调用)。有没有办法解决这个问题,或者没关系?

如果您将客户端和服务器应用程序编译在一起(一个代码库,两个入口点),那么您可以这样做。而不是在客户端做

connect() ->
    Res = gen_server:call({global, ?REMOTE_SERVER}, {connect, client_pid() ...}),

你有

-module(client).
connect() ->
    server:client_connect(client_pid()).

-module(server).
client_connect(ClientPID) ->
    Res = gen_server:call({global, ?REMOTE_SERVER}, {connect, ClientPID ...}).

但是如果你想使用net_kernel来连接节点,并且你想独立编译源代码,那么你的方式就是这样做。


  

给定相同的设置(具有服务器应用程序和N个节点的节点)   与客户),你会使用相同的方法与gen_servers,或   还有一种我不知道的更好的方法吗?

您正在使用net_kernel正在构建分布式系统。如果你期待一些客户,那很好。如果您期望大量客户端,那么您必须记住分布式Erlang默认为完全连接的网格。因此,所有客户端实际上都是相互连接的,以及服务器。

当我查看您的说明时,我想象一个聊天服务器,为此,我会使用gen_tcp代替net_kernel进行联网。

net_kernel的优点:

  • 这是非常高级的。你不需要考虑连接丢弃,消息是非常纯粹的Erlang。
  • 调试更容易。您可以使用shell中的rpc模块在​​任何连接的节点上运行任何内容,这很酷。

gen_tcp的优点:

  • 服务器和客户端连接较少。您可以将客户端或服务器换成具有相同网络API的不同版本(包括交换非Erlang的内容),其他人也不会知道或关心。
  • 客户端未互连(您也可以使用隐藏节点执行此操作)
  • 您可以使用常用的端口号来通过哑巴防火墙

我将“客户端”和“服务器”模块都放在服务器上。您侦听TCP连接并为每个连接生成“客户端”。 “客户端”模块的工作是在通过网络通话的远程客户端和“服务器”模块之间进行转换,通过Erlang消息进行交谈。