Simple chat system over websockets with reconnection feature-Collection of common programming errors

  • What i do is the folowing :

    When an user connects to the site, i swpawn a gen_server reprensenting the user. Then, the gen server registers itself in gproc as {n,l, {user, UserName}}. (It can register properties like {p,l, {chat, ChannelID}} to listen to chat channels. (see gproc pub/sub))

    Ok so now the user websocket connection starts the cowboy handler (i use Bullet). The handlers asks gproc the pid() of the user’s gen_server and registrers itself as a receiver of messages. So now, when the user gen_server receives messages, it redirects them to the websocket handler.

    When the websocket connexion ends, the handler uregister from the user gen_server, so the user gen_server will keep messages until the next connection, or the next timeout. At the timeout, you can simply terminate the server (messages will be lost but it is ok).

    See : (not tested)

    -module(user_chat).
    
    -record(state, {mailbox,receiver=undefined}).
    
    -export([start_link/1,set_receiver/1,unset_receiver/1]).
    %% API
    
    start_link(UserID) ->
        gen_server:start_link(?MODULE,[UserID],[]).
    
    set_receiver(UserID) ->
        set_receiver(UserID,self()).
    
    unset_receiver(UserID) ->
        %% Just set the receiver to undefined
        set_receiver(UserID,undefined).
    
    set_receiver(UserID, ReceiverPid) ->
        UserPid = gproc:where({n,l,UserID}),
        gen_server:call(UserPid,{set_receiver,ReceiverPid}).
    
    
    %% Gen server internals
    
    init([UserID]) ->
        gproc:reg({n,l,{user,UserID}}),
        {ok,#state{mailbox=[]}}.
    
    handle_call({set_receiver,ReceiverPid},_From,#state{mailbox=MB}=State) ->
        NewMB = check_send(MB,State),
        {reply,ok,State#state{receiver=ReceiverPid,mailbox=NewMB}}.
    
    handle_info({chat_msg,Message},#state{mailbox=MB}=State) ->
        NewMB = check_send([Message|MB],State),
        {noreply, State#state{mailbox=NewMB}}.
    
    %% Mailbox empty
    check_send([],_) -> [];
    %% Receiver undefined, keep messages
    check_send(Mailbox,#state{receiver=undefined}) -> Mailbox
    %% Receiver is a pid
    check_send(Mailbox,#state{receiver=Receiver}) when is_pid(Receiver) ->
        %% Send all messages
        Receiver ! {chat_messages,Mailbox},
        %% Then return empty mailbox
        [].
    
  • With the solution you propose you may have many processes pending and you will have to write a “process cleaner” for all user that never come back. Anyway it will not support a shutdown of the chat server VM, all messages stored in living FSM will vanish if the node is down.

    I think that a better way should be to store all messages in a database like mnesia, with sender, receiver, expiration date… and check for any stored message at connection, and have a message cleaner process to destroy all expired messages from time to time.