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.