To continue on with my journey to Erlands I'm developing simple IM system using OTP.
There are two OTP applications: a server (one instance) and a client (multiple instances). A set-up is shown below:
╭── node1@host ──╮
│ Server │
│ └gen_server │
╰────────────────╯
╭── node2@host ──╮
│ Client │
│ └gen_server │
╰────────────────╯
╭── node3@host ──╮
│ Client │
│ └gen_server │
╰────────────────╯
...
Client functions
Using Erlang shell, we can issue next commands to the client application:
- Connect to the server and receive a random name from it (I'm fond of names like
turbo-octopus,miniature-octocatetc. :) - Get a list of other connected clients.
- Send a message to the client with specified name.
- Send a message to all clients (broadcast).
Also client should be able to print message in stdout upon receiving.
Implementation details
All messages go through server.
Both server and client applications contain gen_servers (chat_server.erl and chat_client.erl respectively) responsible for handling messages. Server's chat_server process registered as global and visible on all nodes:
%% chat_server.erl
start_link() ->
gen_server:start_link({global, ?SERVER}, ?MODULE, [], []).
When a client connects, it sends pid of its gen_server process. Doing this, we can store clients pids in server's state to distinguish them and sending/broadcasting messages.
%% 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).
Server connect handle:
%% chat_server.erl
handle_call({connect, Pid}, _From, State) ->
%% doing stuff like generating unique name,
%% adding client to list, etc.
{reply, {connected, Name}, UpdatedState}.
Messaging (pun intended)
Well, it's pretty straightforward. The server handles cast from a client, seeks recipient's pid by given name and cast message to it/broadcasts to everyone. And this is it.
A Question will be asked
While developing this system, I wondered if a chosen approach is appropriate. I mean,
- Passing around client's gen_server pid seems more or less acceptable at least because it allows uniquely identify clients and use all gen_server firepower on both ends. Is this the way you do it?
- I have read here and there that explicit interface (calling exported functions) is preferable to direct messaging (thing I do in my client with gen_server:calls). Is there any way to fix this (
rpcfor example), or it's okay? - Given the same set-up (a node with a server application and N nodes with clients), will you use the same approach with gen_servers, or there is a better approach I'm unaware of?
net_kernel:connect_node/1? - Nathaniel Waisbrot