Erlang Central

Chatserver

Revision as of 20:42, 31 July 2007 by FernandaBufford882 (Talk | contribs)

Problem

You want a simple chatserver. You might just want it for easy family chatting, you might want it just to start.

Solution

'Just to start' has few requirements, so you can do something with no authentication or control or privileged users or special commands, and no client more involved than netcat. This solution is more useful than you'd think: in Erlang given such a kernel server, you can -- without dropping even your first connections -- rapidly build up in functionality.

-module(chatserver).
-export([start/1]).
-export([init/1,loop/1,accepter/2,client/2,client_loop/2]).

-record(chat,{socket,accepter,clients}).
-record(client,{socket,name,pid}).

start(Port) -> spawn(?MODULE, init, [Port]).

init(Port) ->
    {ok,S} = gen_tcp:listen(Port, [{packet,0},{active,false}]),
    A = spawn_link(?MODULE, accepter, [S, self()]),
    loop(#chat{socket=S, accepter=A, clients=[]}).

loop(Chat=#chat{accepter=A,clients=Cs}) ->
    receive
        {'new client', Client} ->
            erlang:monitor(process,Client#client.pid),
            Cs1 = [Client|Cs],
            broadcast(Cs1,["new connection from ~s\r\n",
                          Client#client.name]),
            loop(Chat#chat{clients=Cs1});
        {'DOWN', _, process, Pid, _Info} ->
            case lists:keysearch(Pid, #client.pid, Cs) of
                false -> loop(Chat);
                {value,Client} -> 
                    self() ! {'lost client', Client},
                    loop(Chat)
            end;
        {'lost client', Client} ->
            broadcast(Cs,["lost connection from ~s\r\n",
                          Client#client.name]),
            gen_tcp:close(Client#client.socket),
            loop(Chat#chat{clients=lists:delete(Client,Cs)});
        {message,Client,Msg} ->
            broadcast(Cs,["<~s> ~s\r\n", Client#client.name, Msg]),
            loop(Chat);
        refresh ->
            A ! refresh,
            lists:foreach(fun (#client{pid=CP}) -> CP ! refresh end, Cs),
            ?MODULE:loop(Chat)
    end.

accepter(Sock, Server) ->
    {ok, Client} = gen_tcp:accept(Sock),
    spawn(?MODULE, client, [Client, Server]),
    receive
        refresh -> ?MODULE:accepter(Sock, Server)
    after 0 -> accepter(Sock, Server)
    end.

client(Sock, Server) ->
    gen_tcp:send(Sock, "Please respond with a sensible name.\r\n"),
    {ok,N} = gen_tcp:recv(Sock,0),
    case string:tokens(N,"\r\n") of
        [Name] ->
            Client = #client{socket=Sock, name=Name, pid=self()},
            Server ! {'new client', Client},
            client_loop(Client, Server);
        _ ->
            gen_tcp:send(Sock, "That wasn't sensible, sorry."),
            gen_tcp:close(Sock)
    end.

client_loop(Client, Server) ->
    {ok,Recv} = gen_tcp:recv(Client#client.socket,0),
    lists:foreach(fun (S) -> Server ! {message,Client,S} end,
                  string:tokens(Recv,"\r\n")),
    receive
        refresh -> ?MODULE:client_loop(Client, Server)
    after 0 -> client_loop(Client, Server)
    end.

broadcast(Clients, [Fmt|Args]) ->
    S = lists:flatten(io_lib:fwrite(Fmt,Args)),
    lists:foreach(fun (#client{socket=Sock}) ->
                          gen_tcp:send(Sock,S)
                  end, Clients).

Phew! That gets the job done, with optimistic hooks for improvement (the checks for 'refresh') and the tiniest bit of supervisorship (the erlang:monitor/2 and 'DOWN' business in loop/1). There's lots of room for improvement, and there are a few bits which may annoy you -- but that's the point.

A quick test of hackability: have the server log all chatter to a file. First, compile this module, start the server and get a few connections.

1> c(chatserver).
{ok,chatserver}
2> Server = chatserver:start(4125).
<0.1866.0>
3> % wait for connections...

and then redefine broadcast/2, adding a call to log/2.

broadcast(Clients, [Fmt|Args]) ->
    S = lists:flatten(io_lib:fwrite(Fmt,Args)),
    lists:foreach(fun (#client{socket=Sock}) ->
                          gen_tcp:send(Sock,S)
                  end, Clients),
    log(get(logfile),S).

log(undefined,S) ->
    {ok,F} = file:open("/var/chat/log",[write,append]),
    put(logfile,F),
    log(F,S);
log(F,S) -> file:write(F,S).

This tries to limit changes by only adding one line to broadcast/1, but it works by storing the filehandle in the process dictionary, bad for various reasons, and it also lacks a way to explicitly close that file. A better solution might open the file as the server starts, and also pass the filehandle in Chat -- this might seem harder to do with a running server, but with a slightly improved refreshing mechanism (one which can be easily added, using the existing mechanism), even this kind of update would be easy.

For now, use this. You can always change your mind.

3> c(chatserver).
4> Server ! refresh.
5> % wait for activity, and the check the logs.