继续我的Erlands旅程我正在使用OTP开发简单的IM系统。
有两个OTP应用程序:服务器(一个实例)和一个客户端(多个实例)。设置如下所示:
╭── node1@host ──╮
│ Server │
│ └gen_server │
╰────────────────╯
╭── node2@host ──╮
│ Client │
│ └gen_server │
╰────────────────╯
╭── node3@host ──╮
│ Client │
│ └gen_server │
╰────────────────╯
...
使用Erlang shell,我们可以向客户端应用程序发出下一个命令:
turbo-octopus
,miniature-octocat
等名称。)客户端也应该能够在收到时在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并向其发送消息/广播给每个人。就是这样。
在开发这个系统时,我想知道选择的方法是否合适。我的意思是,
rpc
),或者没关系?答案 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
的优点:
rpc
模块在任何连接的节点上运行任何内容,这很酷。 gen_tcp
的优点:
我将“客户端”和“服务器”模块都放在服务器上。您侦听TCP连接并为每个连接生成“客户端”。 “客户端”模块的工作是在通过网络通话的远程客户端和“服务器”模块之间进行转换,通过Erlang消息进行交谈。