Using Websockets to Build a Chat

Preface

In this tutorial, we will use event functions and the Server module, along with some client side scripting to create a multi-user chat application.

You can download the complete project from our Tutorial Repository in the chat directory.

License

The code related to this tutorial is released under the MIT license.

Concepts

This module will demonstrate:

  • Using event functions to relay messages between clients.
  • Using the Server module to connect with clients using websockets.
  • Handling both text and files using websockets.
  • Using rampart-redis module to replace rampart.event so we can have a saved history.

In order to complete this tutorial, you should have basic knowledge of JavaScript and client side scripting using JavaScript (a passing knowledge of jQuery is also helpful). This tutorial will not provide an in-depth explanation of how client-side scripting works.

Getting Started

Web Server Layout

In the Rampart binary distribution is a sample web server tree. For our purposes here, we assume you have downloaded and unzipped the Rampart binary distribution into a directory named ~/downloads/rampart. We will use that for this project. The The rampart-server HTTP module is configured and loaded from the included web_server/web_server_conf.js script. It defines web_server/html as the default directory for static html, web_server/wsapps as the default directory for websocket scripts.

To get started, copy the web_server directory to a convenient place for this project. Also, for our purposes, we do not need anything in the web_server/apps/test_modules or web_server/apps/wsapps directories, so you can delete the copy of those files. We will also add an empty file at web_server/wsapps/wschat.js and web_server/html/websockets_chat/index.html.

user@localhost:~$ mkdir wschat_demo
user@localhost:~$ cd wschat_demo
user@localhost:~/wschat_demo$ cp -a ~/downloads/rampart/web_server ./
user@localhost:~/wschat_demo$ cd web_server/
user@localhost:~/wschat_demo/web_server$ rm -rf apps/test_modules wsapps/* html/index.html
user@localhost:~/wschat_demo/web_server$ touch ./wsapps/wschat.js
user@localhost:~/wschat_demo/web_server$ mkdir html/websockets_chat
user@localhost:~/wschat_demo/web_server$ touch ./html/websockets_chat/index.html
user@localhost:~/wschat_demo/web_server$ find .
./wsapps
./wsapps/wschat.js
./start_server.sh
./stop_server.sh
./web_server_conf.js
./logs
./apps
./html
./html/images
./html/images/inigo-not-fount.jpg
./html/websockets_chat
./html/websockets_chat/index.html
./data

The Client HTML and Script

As stated before, we assume that you have a basic understanding of the client-side scripting and therefore will dispense with a lengthy discussion about it. The main points are:

  • There is no security or user management. A new chat client merely types in a name and is connected via websockets to the server.
  • The start() function makes the initial connection to the server.
  • The procmess() receives messages from the server, decodes the JSON and displays the message for the user.
  • The send() function is called when the user presses <enter> in the text box at the bottom of the web page.
  • The showMessage() function is used to display incoming or echo outgoing messages in the chat div.

So we will begin with opening html/websockets_chat/index.html in an editor and pasting the following:

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>chat</title>
<style>
    html,body {
        height:100%;
        font-family:Arial, Helvetica, sans-serif;
        margin:0;
    }
    #container{
        border:5px solid grey;
        position: absolute;
        margin-top : 10px;
        padding:10px;
        bottom:30px;
        top: 30px;
        right:20px;
        left:20px;
    }
    #chatdiv{
        padding:5px;
        border:2px solid gray;
        height: calc(100% - 175px);
        overflow-y: scroll;
        margin:0;
        margin-top: 5px;
    }
    .event {
        color:#999;
    }
    .n {
        color:#393;
    }
    .i {
        vertical-align: top;
    }
    .s {
        color:#933;
    }
    #wrapper{
        height: 100%;
    }
    #name{
        width:220px;
    }
    #chatdiv.dropping {
        border: 2px blue dashed;
    }

    #chatin{
        width: calc(100% - 120px);
        height: 1.5em;
        margin-top: 7px;
    }
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
    var socket;
    var name;
    var reconnected=false;
    var cd = $('#chatdiv');
    var prot;
    var htmlEscape=true;

    // check our protocol, set matching websocket version
    if (/^https:/.test(window.location.href))
        prot='wss://'
    else
        prot='ws://'

    // check if connection is open
    function isOpen(ws) { return ws.readyState === ws.OPEN }

    function getcookie(cname){
        //https://www.30secondsofcode.org/js/s/parse-cookie
        var cookies = document.cookie
            .split(';').map(v => v.split('='))
            .reduce
            ( (acc, v) => {
                acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
                return acc;
                }, {}
            );
        return cookies[cname];
    }

    function showMessage(data){
        if(htmlEscape)
            data.msg = $('<div/>').text(data.msg).html();
        if(data.from=="System")
            cd.append('<span class="s">' + data.from + ":</span> " + data.msg +'<br>');
        else
            cd.append('<span class="n">' + data.from + ":</span> " + data.msg +'<br>');
        cd.scrollTop(cd.height());
    }

    function procmess (msg){
        var data;

        try{
            data = JSON.parse(msg.data);
        } catch (e) {
            cd.append('<span style="color:red;">error parsing message</span><br>');
        }
        // if reconnected, skip welcome message.
        if(reconnected){
            reconnected=false;
            return;
        }

        if(data){
            showMessage(data);
        }
    }

    function send(){
        var text=$('#chatin').val();

        if(text==""){
            return ;
        }

        var data= {
            msg: text,
            from: name
        };

        try{
            // attempt reconnect if discoonnected
            if(!isOpen(socket) && !reconnected) {
                socket = new WebSocket(prot + window.location.host + "/wsapps/wschat-s.json");
                socket.addEventListener('open', function(e){
                    socket.send(text);
                    reconnected=true;
                    $('#chatin').val("");
                    showMessage(data);
                    socket.onmessage = procmess;
                });
                return;
            }
            //send it
            socket.send(text);
            //echo it
            showMessage(data);
        } catch(e){
            showMessage({from:"System",msg:'error sending message'});
        }
        $('#chatin').val("");
    }

    function start() {
        if(socket)
            socket.close();
        socket = new WebSocket(prot + window.location.host + "/wsapps/wschat-s.json");
        socket.onmessage = procmess;
    }

    function setname() {
        name = $('#name').val();
        if(name=="")
        return;
        document.cookie = "username="+name + "; path=/; sameSite=Strict";
        start();
    }

    // send message to server when <enter> is pressed
    $('#chatin').keypress(function(event) {
        if (event.keyCode == '13') {
            send();
        }
    });

    // sign on
    $('#name').keypress(function(event) {
        if (event.keyCode == '13') {
            setname();
            $('#namemsg').text("You are logged in as ");
            $('#chatin').focus();
        }
    });

    // check if we signed on previously
    name = getcookie("username");

    if(name) {
        start();
        $('#name').val(name);
        $('#namemsg').text("You are logged in as ");
        $('#chatin').focus();
    }

});
</script>
</head>
<body>
    <div id="wrapper">
        <div id="container">
            <h2>wschat tutorial</h2>
            <span id="namespan">
                <span id="namemsg">Type Your Name and pres &lt;enter&gt; to begin:</span>
                <input placeholder="Type your name and press enter" id="name" type="text">
            </span>
            <div id="chatdiv">
            </div>
            <input id="chatin" type="text" />
        </div>
    </div>
</body>
</html>

Websockets Server Script

The Basics of Rampart-Server Websockets

As you probably know, server scripts are passed a req object when serving content via the http and https protocols. Each request from a client results in a call to the appropriate JavaScript callback, which receives a fresh req object with the details of the request.

Websocket connections differ in that the req object is long lived and is unique to the server-client connection rather than unique to a single request. More information on how this works can be found in the Server Websockets Section. However, here are the basics we need to know for this tutorial:

  • The module.exports function is run for every incoming websocket message.
  • req.count is the number of times function has been called since connection was made.
  • The first run (req.count==0) will have an empty body and represents the initial connection.
  • The req object is reused with req.body updated for each incoming message.
  • Sending data to the client is done with req.wsSend().
  • Any data printed or put using req.printf/req.put will also be sent when req.wsSend() is called or when the module.exports function returns.
  • req.wsOnDisconnect registers a function that is run when you disconnect or you are disconnected by the client.
  • req.wsEnd forces a disconnect (but runs callback first);
  • req.websocketId is a unique number to identify the current connection to a single client.
  • req.wsIsBin is true if the client sends binary data. Data will be in req.body.
  • req.body is always a Buffer. If req.wsIsBin is false, it can be converted to a String using rampart.utils.sprintf('%s',req.body) or rampart.utils.bufferToString(req.body).

With your favorite editor, open the wsapps/wschat.js file and paste this stub script to begin:

rampart.globalize(rampart.utils);
var ev = rampart.event;


function getuser(req){
    // no real user management here
    // just use the cookie set in client-side script
    // however here is where you could add an authentication scheme
    return req.cookies.username;
}

function receive_message() {
}

function setup_event_listen(req) {
}

function forward_messages(req) {
}

// exporting a single function
module.exports = function (req)
{
    if (req.count==0) {
        /* first run upon connect, req.body is empty
           Here is where we will set up the event to listen for
           incoming message from other users
         */
        setup_event_listen(req);
    } else {
        /* second and subsequent runs below.  Client has sent a message
           and we need to process and forward it to others who are
           listening via rampart.event.on above
         */
        forward_messages(req);
    }
    return null;
}

Setup on Connect

We will use the setup_event_listen() function to simulate authentication of the user, set up an event, register a function to handle a client disconnect and send a message letting other clients know a new client has joined.

function setup_event_listen(req) {

    /* check for username */
    req.user_name=getuser(req);
    if(!req.user_name){
        req.wsSend({from: "System", id:req.websocketId, msg: "No user name provided, disconnecting"});
        req.wsEnd();
        return;
    }

    /* what to do if we are sent a message from another user.  Here
       ``rampart.event.on`` registers a callback function to be
       executed.  The callback function will take two parameters: a
       variable provided by "event.on" and a provided by "trigger".  The
       function is registered with the event name "msgev" and the
       function name "userfunc_x" where x is the unique websocketId for
       this connection.  The varable "req" is passed to the
       proc_incoming_message as its first argument.  */
    ev.on("msgev", "userfunc_"+req.websocketId, receive_message, req);

    // set up function for when this user disconnects (either by browser disconnect or req.wsEnd() )
    req.wsOnDisconnect(function(){
        // msg -> everyone listening
        ev.trigger("msgev", {from:'System', id:req.websocketId, msg: req.user_name+" has left the conversation"});
        // remove our function unique to this use from the event
        ev.off("msgev", "userfunc_"+req.websocketId);
    });

    /* send a notification to all listening that we've joined the conversation */
    ev.trigger("msgev", {from:'System', id:req.websocketId, msg: req.user_name+" has joined the conversation"});

    // send a welcome message to client from the "System".
    req.wsSend( {from: "System", msg: `Welcome ${req.user_name}`} );
}

The setup_event_listen() function is run only upon first connecting. When it is done, the server sends any pending messages and then waits for either 1) a message from our client (which we will forward to other clients in the code below), 2) for some other client or script to “trigger” our event with rampart.event.trigger("msgev",...) or 3) some other event, HTTP request, other websocket connections and any other asynchronous function, such as setTimeout or asynchronous functions in the rampart-redis module). This all happens within the Rampart event loop.

Receiving from Client and Forward

Next we will use the forward_message() function to format and forward messages from the client and send them to other connected clients using rampart.event.trigger.

function forward_messages(req) {

    // only accepting text messages
    if(req.wsIsBin)
        return;

    if(req.body.length)
    {
        //send the plain text message to whoever is listening
        req.body = sprintf('%s',req.body);
        ev.trigger("msgev", {from:req.user_name, id:req.websocketId, msg:req.body});
    }
}

Here we trigger the msgev event. Every connected client, including the current one, and every script running, having registered the msgev event, will be triggered and run the registered functions.

Receiving from other Clients

Here we will fill in the function registered with rampart.event.on("msgev", ...). Its job is to forward messages sent by other clients (via rampart.event.trigger) to the browser for display. Since the client-side script does its own echo, we will filter out messages from our own client.

/* process incoming message as sent below in ev.trigger() with data set to
   { from: user_name, id: user_id, [ msg: "a message" | file: binary_file_data] }
*/
function receive_message(req, data) {
    if(data.id != req.websocketId) {//don't echo our own message
        req.wsSend({
            from: data.from,
            msg: data.msg
        });
    }
}

Full Script

Our script so far:

rampart.globalize(rampart.utils);
var ev = rampart.event;

function getuser(req){
    // no real user management here
    // just use the cookie set in client-side script
    // however here is where you could add an authentication scheme
    return req.cookies.username;
}

/* NOTES:
 * The module.exports function is run for every incoming websocket message.
 * The first run (req.count==0) will have an empty body and represents the initial connection.
 * The req object is reused with req.body updated for each incoming message.
 * Sending data back is done with req.wsSend().
 * Any data printed or put using req.printf/req.put will also be sent when
 *   req.wsSend() is called or when the module.exports function returns
 * req.count == number of times function has been called since connection was made.
 * req.wsOnDisconnect is a function that is run when you disconnect or you are disconnected by the client.
 * req.wsEnd forces a disconnect (but runs callback first);
 * req.websocketId is a unique number to identify the current connection to a single client.
 * req.wsIsBin is true if the client sends binary data.  Data will be in req.body
 * req.body is always a buffer.  If req.wsIsBin is false, it can be converted to a string.
 *   using rampart.utils.sprintf('%s',req.body) or rampart.utils.bufferToString(req.body)
 */


/* process incoming message as sent below in ev.trigger() with data set to
   { from: user_name, id: user_id, [ msg: "a message" | file: binary_file_data] }
*/
function receive_message(req, data) {
    if(data.id != req.websocketId) {//don't echo our own message
        req.wsSend({
            from: data.from,
            msg: data.msg
        });
    }
}

function setup_event_listen(req) {

    /* check for username */
    req.user_name=getuser(req);
    if(!req.user_name){
        req.wsSend({from: "System", id:req.websocketId, msg: "No user name provided, disconnecting"});
        req.wsEnd();
        return;
    }

    /* what to do if we are sent a message from another user.  Here
       ``rampart.event.on`` registers a callback function to be
       executed.  The callback function will take two parameters: a
       variable provided by "event.on" and a provided by "trigger".  The
       function is registered with the event name "msgev" and the
       function name "userfunc_x" where x is the unique websocketId for
       this connection.  The varable "req" is passed to the
       proc_incoming_message as its first argument.  */
    ev.on("msgev", "userfunc_"+req.websocketId, receive_message, req);

    // set up function for when this user disconnects (either by browser disconnect or req.wsEnd() )
    req.wsOnDisconnect(function(){
        // msg -> everyone listening
        ev.trigger("msgev", {from:'System', id:req.websocketId, msg: req.user_name+" has left the conversation"});
        // remove our function unique to this use from the event
        ev.off("msgev", "userfunc_"+req.websocketId);
    });

    /* send a notification to all listening that we've joined the conversation */
    ev.trigger("msgev", {from:'System', id:req.websocketId, msg: req.user_name+" has joined the conversation"});

    // send a welcome message to client from the "System".
    req.wsSend( {from: "System", msg: `Welcome ${req.user_name}`} );
}

function forward_messages(req) {

    // only accepting text messages
    if(req.wsIsBin)
        return;

    if(req.body.length)
    {
        //send the plain text message to whoever is listening
        req.body = sprintf('%s',req.body);
        ev.trigger("msgev", {from:req.user_name, id:req.websocketId, msg:req.body});
    }
}

// exporting a single function
module.exports = function (req)
{
    if (req.count==0) {
        /* first run upon connect, req.body is empty
           Here is where we will set up the event to listen for
           incoming message from other users
         */
        setup_event_listen(req);
    } else {
        /* second and subsequent runs below.  Client has sent a message
           and we need to process and forward it to others who are
           listening via rampart.event.on above
         */
        forward_messages(req);
    }
    return null;
}

Handling Binary Data

What we have so far works well for chatting. However, users might want to send files or images as well. So we will add that functionality.

Client-side Additions

We will alter our client-side script to handle drag-and-drop of files and send them to the server. Again, a full explanation of how this works is beyond the scope of this tutorial.

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>chat</title>
<style>
html,body {
    height:100%;
    font-family:Arial, Helvetica, sans-serif;
    margin:0;
}
#container{
    border:5px solid grey;
    position: absolute;
    margin-top : 10px;
    padding:10px;
    bottom:30px;
    top: 30px;
    right:20px;
    left:20px;
}
#chatdiv{
    padding:5px;
    border:2px solid gray;
    height: calc(100% - 175px);
    overflow-y: scroll;
    margin:0;
    margin-top: 5px;
}
.event {
    color:#999;
}
.n {
    color:#393;
}
.i {
    vertical-align: top;
}
.s {
    color:#933;
}
#wrapper{
    height: 100%;
}
#name{
    width:220px;
}
#chatdiv.dropping {
    border: 2px blue dashed;
}

#chatin{
    width: calc(100% - 120px);
    height: 1.5em;
    margin-top: 7px;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
    var socket;
    var name;
    var reconnected=false;
    var cd = $('#chatdiv');
    var prot;
    var htmlEscape=true;

    // check our protocol, set matching websocket version
    if (/^https:/.test(window.location.href))
        prot='wss://'
    else
        prot='ws://'

    // check if connection is open
    function isOpen(ws) { return ws.readyState === ws.OPEN }

    // display image, or link other types of binary files
    function displayfile(data, blob)
    {
        var finfo=data.file;
        var b = blob.slice(0, blob.size, finfo.type);
        var linkurl = URL.createObjectURL(b);
        if(/^image/.test(finfo.type))
        {
            cd.append('<span class="s i">' + data.from +
            ':</span> <img style="height: 300px"><br>');
            var img = cd.find('img').last();
            img.attr({'src': linkurl, 'alt': finfo.name});
        } else {
            cd.append('<span class="s">' + data.from +  ':</span> FILE: <a>'+finfo.name+'</a><br>');
            var a = cd.find('a').last();
            a.attr({"href":linkurl, "download":finfo.name});
        }
        cd.scrollTop(cd.height() + 300);
        data=false;
    }

    // what to do when a file is dropped on conversation div
    function handle_drop(e){
        e.preventDefault();
        e.stopPropagation();
        cd.removeClass("dropping");
        e = e.originalEvent;
        if(!isOpen(socket))
        return;//fix me.

        // send file to server, display it in conversation div
        function sendfile(file) {
            var reader = new FileReader()
            reader.onload = function (event) {
                socket.send(event.target.result);
            };
            reader.readAsArrayBuffer(file);
            // we get to see the file too
            displayfile({from:name,file:{name:file.name,type:file.type}}, file);
        }

        // depending on browser API, send file metadata, then send file
        if (e.dataTransfer.items) {
            // Use DataTransferItemList interface to access the file(s)
            for (var i = 0; i < e.dataTransfer.items.length; i++) {
                // If dropped items aren't files, reject them
                if (e.dataTransfer.items[i].kind === 'file') {
                    var file = e.dataTransfer.items[i].getAsFile();
                    var json = JSON.stringify({file:{name:file.name,type:file.type}});
                    socket.send(json); // first send metadata
                    sendfile(file);    // second send actual file
                }
            }
        } else {
            // Use DataTransfer interface to access the file(s)
            for (var i = 0; i < e.dataTransfer.files.length; i++) {
                var file = e.dataTransfer.files[i];
                socket.send(JSON.stringify({file:file}));
                sendfile(file);
            }
        }
    }

    function getcookie(cname){
        //https://www.30secondsofcode.org/js/s/parse-cookie
        var cookies = document.cookie
            .split(';').map(v => v.split('='))
            .reduce
            ( (acc, v) => {
                acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
                return acc;
                }, {}
            );
        return cookies[cname];
    }

    function showMessage(data){
        if(htmlEscape)
            data.msg = $('<div/>').text(data.msg).html();
        if(data.from=="System")
            cd.append('<span class="s">' + data.from + ":</span> " + data.msg +'<br>');
        else
            cd.append('<span class="n">' + data.from + ":</span> " + data.msg +'<br>');
        cd.scrollTop(cd.height());
    }

    var ExpectedFileData=[];

    function procmess (msg){
        var data;
        if(ExpectedFileData.length && msg.data instanceof Blob) {
            var fdata = ExpectedFileData.shift();
            displayfile(fdata, msg.data);
            return;
        }
        try{
            data = JSON.parse(msg.data);
        } catch (e) {
            cd.append('<span style="color:red;">error parsing message</span><br>');
        }
        // if reconnected, skip welcome message.
        if(reconnected){
            reconnected=false;
            return;
        }

        if(data){
            if (data.file)
                ExpectedFileData.push(data);
            else
                showMessage(data);
        }
    }

    function send(){
        var text=$('#chatin').val();

        if(text==""){
            return ;
        }

        var data= {
            msg: text,
            from: name
        };

        try{
            // attempt reconnect if discoonnected
            if(!isOpen(socket) && !reconnected) {
                socket = new WebSocket(prot + window.location.host + "/wsapps/wschat.json");
                socket.addEventListener('open', function(e){
                    socket.send(text);
                    reconnected=true;
                    $('#chatin').val("");
                    showMessage(data);
                    socket.onmessage = procmess;
                });
                return;
            }
            //send it
            socket.send(text);
            //echo it
            showMessage(data);
        } catch(e){
            showMessage({from:"System",msg:'error sending message'});
        }
        $('#chatin').val("");
    }

    function start() {
        if(socket)
            socket.close();
        socket = new WebSocket(prot + window.location.host + "/wsapps/wschat.json");
        socket.onmessage = procmess;
    }

    function setname() {
        name = $('#name').val();
        if(name=="")
        return;
        document.cookie = "username="+name + "; path=/; sameSite=Strict";
        start();
    }

    //drag and drop events
    cd.on("drop",handle_drop)
    .on("dragover",function(e){
        e.preventDefault();
        e.stopPropagation();
        cd.addClass("dropping");
    })
    .on("dragleave",function(e){
        e.preventDefault();
        e.stopPropagation();
        cd.removeClass("dropping");
    });

    // send message to server when <enter> is pressed
    $('#chatin').keypress(function(event) {
        if (event.keyCode == '13') {
            send();
        }
    });

    // sign on
    $('#name').keypress(function(event) {
        if (event.keyCode == '13') {
            setname();
            $('#namemsg').text("You are logged in as ");
            $('#chatin').focus();
        }
    });

    // check if we signed on previously
    name = getcookie("username");

    if(name) {
        start();
        $('#name').val(name);
        $('#namemsg').text("You are logged in as ");
        $('#chatin').focus();
    }

});
</script>
</head>
<body>
    <div id="wrapper">
        <div id="container">
            <h2>wschat tutorial</h2>
            <span id="namespan">
                <span id="namemsg">Type Your Name and pres &lt;enter&gt; to begin:</span>
                <input placeholder="Type your name and press enter" id="name" type="text">
            </span>
            <div id="chatdiv">
            </div>
            <input id="chatin" type="text" />
        </div>
    </div>
</body>
</html>

Receiving Files from Client and Forward

We will add to our forward_messages() function in order to receive files from the websocket client. When a file is dropped in the message area of the web page, the client-side script will first send some JSON metadata in a text message, then send the actual binary file.

We will receive the metadata and file in two separate messages (with two separate calls to forward_messages()). This means we will have to store the metadata somewhere, and have it accessible when we receive the file. We will then be able to assemble a single Object to pass to rampart.event.trigger(). Since the req Object is recycled upon every message, we will set req.files to a Array of Objects (one Object per file) for this purpose.

function forward_messages(req) {
    /* we are sent a file from the client in two parts: 1) JSON meta info, 2) binary data */
    var fileInfo;  //variable for JSON meta info
    var file;  // will hold object with meta data and binary file content

    /* STEP 1:  Check message type:  Either -
                   1) metadata for an incoming file
                   2) binary data for an incoming file
                   3) text message
    */

    // check non binary messages for JSON
    if(!req.wsIsBin && req.body.length)
    {
        /* messages are just plain text, but
           if it is a file, first we get the file meta info in JSON format */
        try{
            fileInfo=JSON.parse(req.body);
        } catch(e) {
            /* it is not binary, or json, so it must be a message */
            fileInfo = false;
        }
    }

    /* if it is binary data, we assume it is a file
       and the file info was already sent            */
    if(req.wsIsBin)
    {
        if(req.files && req.files.length)
        {
            //get the first entry of file meta info
            file = req.files.shift();
            // add the body buffer to it
            file.content = req.body;
            // tell everyone who it's from
            file.from=req.user_name;
        }
        else // handle unlikely case that metadata is not available.
            file = {from:req.user_name, name:"", type:"application/octet-stream", content:req.body};
    }
    else if(!fileInfo)
        req.body = sprintf("%s",req.body);//It's not binary, convert body buffer to a string


    /* STEP 2:  Process info from step 1 -
                   1) if it is file metadata, save it in the "req.files" var and wait for next message
                   2) if we have both metadata and file content, trigger event and send to all other users
                   3) if we have a text message, trigger event to send message to all other users
    */
    if(fileInfo && fileInfo.file)
    {
        if(!req.files)
            req.files = [];
        /* store file meta info in req where we will retrieve it next time */
        req.files.push(fileInfo.file);
        // do nothing and get the actual binary file in the next message
    }
    else if (file)
    {
        /* we received a file, reassembled its meta info.  Send it to all that are listening */
        ev.trigger("msgev", {from:req.user_name, id:req.websocketId, file: file});
    }
    else if(req.body.length)
    {
        //send the plain text message to whoever is listening
        ev.trigger("msgev", {from:req.user_name, id:req.websocketId, msg:req.body});
    }

    /* Step 3:
         We received data, but here no data is sent back to the client.  However,
         it is sent to others using trigger and receive by the rampart.event.on
         function which they registered in their own connections.  So we can just return
         here
    */
}

Receiving Files from other Clients

Now we alter the receive_message() function in order to send a file back to the client. Again, it is sent in two parts – metadata then the binary file.

/* process incoming message as sent below in ev.trigger() with data set to
   { from: user_name, id: user_id, [ msg: "a message" | file: binary_file_data] }
*/
function receive_message(req, data) {
    if(data.id != req.websocketId) {//don't echo our own message
        // is this a file?  Sent two messages.
        if (data.file)
        {
            //send file metadata, then ...
            req.wsSend({
                from: data.from,
                file: {name:data.file.name, type: data.file.type}
            });
            // ... send actual binary file
            req.wsSend(data.file.content);
        }
        else
        // it is a text message
        {
            req.wsSend({
                from: data.from,
                msg: data.msg
            });
        }
    }
}

Full Script Handling Files

rampart.globalize(rampart.utils);
var ev = rampart.event;


function getuser(req){
    // no real user management here
    // just use the cookie set in client-side script
    // however here is where you could add an authentication scheme
    return req.cookies.username;
}

/* NOTES:
 * The module.exports function is run for every incoming websocket message.
 * The first run (req.count==0) will have an empty body and represents the initial connection.
 * The req object is reused with req.body updated for each incoming message.
 * Sending data back is done with req.wsSend().
 * Any data printed or put using req.printf/req.put will also be sent when
 *   req.wsSend() is called or when the module.exports function returns
 * req.count == number of times function has been called since connection was made.
 * req.wsOnDisconnect is a function that is run when you disconnect or you are disconnected by the client.
 * req.wsEnd forces a disconnect (but runs callback first);
 * req.websocketId is a unique number to identify the current connection to a single client.
 * req.wsIsBin is true if the client sends binary data.  Data will be in req.body
 * req.body is always a buffer.  If req.wsIsBin is false, it can be converted to a string.
 *   using rampart.utils.sprintf('%s',req.body) or rampart.utils.bufferToString(req.body)
 */

/* process incoming message as sent below in ev.trigger() with data set to
   { from: user_name, id: user_id, [ msg: "a message" | file: binary_file_data] }
*/
function receive_message(req, data) {
    if(data.id != req.websocketId) {//don't echo our own message
        // is this a file?  Sent two messages.
        if (data.file)
        {
            //send file metadata, then ...
            req.wsSend({
                from: data.from,
                file: {name:data.file.name, type: data.file.type}
            });
            // ... send actual binary file
            req.wsSend(data.file.content);
        }
        else
        // it is a text message
        {
            req.wsSend({
                from: data.from,
                msg: data.msg
            });
        }
    }
}

function setup_event_listen(req) {

    /* check for username */
    req.user_name=getuser(req);
    if(!req.user_name){
        req.wsSend({from: "System", id:req.websocketId, msg: "No user name provided, disconnecting"});
        req.wsEnd();
        return;
    }

    /* what to do if we are sent a message from another user.  Here rampart.event.on
       registers a function to be executed.  The function takes a parameter from "on"
       and a parameter from "trigger". The function is registered with the event name
       "msgev" and the function name "userfunc_x" where x is the unique websocketId for
       this connection. The varable "req" is passed to the proc_incoming_message as its
       first argument.                                                                   */
    ev.on("msgev", "userfunc_"+req.websocketId, receive_message, req);

    // set up function for when this user disconnects (either by browser disconnect or req.wsEnd() )
    req.wsOnDisconnect(function(){
        // msg -> everyone listening
        ev.trigger("msgev", {from:'System', id:req.websocketId, msg: req.user_name+" has left the conversation"});
        // remove our function unique to this use from the event
        ev.off("msgev", "userfunc_"+req.websocketId);
    });

    /* send a notification to all listening that we've joined the conversation */
    ev.trigger("msgev", {from:'System', id:req.websocketId, msg: req.user_name+" has joined the conversation"});

    // send a welcome message to client from the "System".
    req.wsSend( {from: "System", msg: `Welcome ${req.user_name}`} );
}


function forward_messages(req) {
    /* we are sent a file from the client in two parts: 1) JSON meta info, 2) binary data */
    var fileInfo;  //variable for JSON meta info
    var file;  // will hold object with meta data and binary file content

    /* STEP 1:  Check message type:  Either -
                   1) metadata for an incoming file
                   2) binary data for an incoming file
                   3) text message
    */

    // check non binary messages for JSON
    if(!req.wsIsBin && req.body.length)
    {
        /* messages are just plain text, but
           if it is a file, first we get the file meta info in JSON format */
        try{
            fileInfo=JSON.parse(req.body);
        } catch(e) {
            /* it is not binary, or json, so it must be a message */
            fileInfo = false;
        }
    }

    /* if it is binary data, we assume it is a file
       and the file info was already sent            */
    if(req.wsIsBin)
    {
        if(req.files && req.files.length)
        {
            //get the first entry of file meta info
            file = req.files.shift();
            // add the body buffer to it
            file.content = req.body;
            // tell everyone who it's from
            file.from=req.user_name;
        }
        else // handle unlikely case that metadata is not available.
            file = {from:req.user_name, name:"", type:"application/octet-stream", content:req.body};
    }
    else if(!fileInfo)
        req.body = sprintf("%s",req.body);//It's not binary, convert body buffer to a string


    /* STEP 2:  Process info from step 1 -
                   1) if it is file metadata, save it in the "req.files" var and wait for next message
                   2) if we have both metadata and file content, trigger event and send to all other users
                   3) if we have a text message, trigger event to send message to all other users
    */
    if(fileInfo && fileInfo.file)
    {
        if(!req.files)
            req.files = [];
        /* store file meta info in req where we will retrieve it next time */
        req.files.push(fileInfo.file);
        // do nothing and get the actual binary file in the next message
    }
    else if (file)
    {
        /* we received a file, reassembled its meta info.  Send it to all that are listening */
        ev.trigger("msgev", {from:req.user_name, id:req.websocketId, file: file});
    }
    else if(req.body.length)
    {
        //send the plain text message to whoever is listening
        ev.trigger("msgev", {from:req.user_name, id:req.websocketId, msg:req.body});
    }

    /* Step 3:
         We received data, but here no data is sent back to the client.  However,
         it is sent to others using trigger and receive by the rampart.event.on
         function which they registered in their own connections.  So we can just return
         here
    */
}

// exporting a single function
module.exports = function (req)
{
    if (req.count==0) {
        /* first run upon connect, req.body is empty
           Here is where we will set up the event to listen for
           incoming message from other users
         */
        setup_event_listen(req);
    } else {
        /* second and subsequent runs below.  Client has sent a message
           and we need to process and forward it to others who are
           listening via rampart.event.on above
         */
        forward_messages(req);
    }
    return null;
}

Chat with History using Redis

There are many enhancements you might want to make when using the above examples as a template for a robust chat. However it will be difficult to allow users to refresh the page or log back and see old messages unless there is some databasing. Below we will use Redis to both save messages and replace rampart.event.

We will set up our new version by copying the files webserver/html/websockets-chat/index.html and webserver/wsapps/wschat.js to webserver/html/websockets-chat/index-redis-chat.html and webserver/wsapps/wschat-redis.js respectively.

Client-Side changes

The client-side script (now at webserver/html/websockets-chat/index-redis-chat.html) only needs a minimal update. Simply replace the two references to /wsapps/wschat.json to /wsapps/wschat-redis.json.

Starting Redis

We will start by editing the copied `webserver/wsapps/wschat-redis.js script.

Here, we will have Redis running on the same machine and started by the wschat-redis.js script as necessary. It is worth noting that Redis could be running on a different machine with our script potentially running on several machines, since Redis does all of its communication over a socket.

The startup function checks for an existing handle to Redis attached to the req handle. If it doesn’t exist, it makes the connection to Redis and saves the handle as a property of req. If that fails (i.e. Redis is not running), redis-server is located and started with a simple configuration that is passed to it via stdin. At any point, if there is an error, that error will be returned. If all is successful, it returns undefined.

So we add the following to our script:

var redis=require("rampart-redis");

function init(req) {
    if(req.rcl)
        return;

    var redisDir=serverConf.dataRoot + '/redis_chat';
    var redisConf = "bind 127.0.0.1 -::1\n"                      +
                    "port 23741\n"                               +
                    "pidfile " + redisDir + "/redis_23741.pid\n" +
                    "dir " + redisDir + "\n";

    /* make the directory for redis */
    var dirstat = stat(redisDir);
    if(!dirstat) {
        try {
            rampart.utils.mkdir(redisDir);
        } catch (e){
            return e;
        }
    }

    /***** Test if Redis is already running *****/
    try {
        req.rcl=new redis.init(23741);
        return;
    } catch(e){}

    /***** LAUNCH REDIS *********/
    var ret = shell("which redis-server");

    if (ret.exitStatus != 0) {
        return "Could not find redis-server in PATH";
    }

    var rdexec = trim(ret.stdout);

    ret = exec("nohup", rdexec, "-", {background: true, stdin:redisConf});
    var rpid = ret.pid;

    sleep(0.5);

    if (!kill(rpid, 0)) {
        return "Failed to start redis-server";
    }

    try {
        req.rcl=new redis.init(23741);
        return;
    } catch(e){
        return e;
    }
    return;
}

Replacing Event Functions

Redis has pub/sub commands. However they do not save the data for later use. It also has streams that save the data and can be listened to using the x commands (xadd, xread, etc.). Rampart’s redis client additionally includes the xread_auto_async command, which monitors one or more Redis Streams without having to reissue the xread command or track the last id seen. This makes it work more like pub/sub with the ability to save recent messages.

We will use xread_auto_async and xadd to replace rampart.event.on and rampart.event.trigger respectively.

First, unlike rampart.event, data encapsulated in Objects needs to be converted to and from JSON when sending it to the Redis server. Conversion to JSON is handled automatically.

However there are two caveats:

  • Since Redis can store data of many types, we will need to manually parse the received JSON data from Redis.
  • JSON is not the ideal format for binary buffer data, so we will encode the file to a base64 string. Note that an alternative would be to use CBOR encoding.

We’ll create a function to do just that when sending data back to the client with req.wsSend():

function sendWsMsg(req,data) {
   if (data.file) {
   // sending a file
        // JSON from redis must be decoded.
        try{
            data.file = JSON.parse(data.file);
        } catch(e){}
        if(!data.file.content)
        {
            return;
        }
        //send file metadata, then ...
        req.wsSend({
            from: data.from,
            file: {name:data.file.name, type: data.file.type}
        });
        // ... send actual binary file
        req.wsSend(bprintf('%!B',data.file.content));
    }
    else
    // it is a text message
    {
        req.wsSend({
            from: data.from,
            msg: data.msg
        });
    }
}

With rampart.event.on we could pass the req variable to be used in the registered callback function. Using Redis, we don’t have that option, so instead we will embed the receive_message() function inside the setup_event_listen function. That way req will be in scope and available.

Also, since we now have the sendWsMsg() function, we can use it in place of the req.wsSend() logic we were previously using.

function receive_message(strdata) {
    if(!strdata){ // undefined on disconnect
        req.wsSend({from:'System', id:req.websocketId, msg: "you are now disconnected."})
        req.wsEnd();
        return;
    }
    var data = strdata.data[0].value;
    if(data.id != req.websocketId)
        sendWsMsg(req,data);
 }

Now we need to replace rampart.event.trigger with xadd. We’ll make a function for that as well. We will assume that we want to limit the number of messages we save to around 2000. See the xadd capped strings discussion for more information on limiting the number of messages in a stream.

function rtrigger(req, obj) {
    try {
        req.rcl.xadd(stream, "MAXLEN", '~', '2000', "*", obj);
    } catch(e) {
        req.wsSend({
            from: "System",
            id:req.websocketId,
            msg: sprintf("Error sending msg: %s", e)
        });
    }
}

Note that if the Redis server cannot be reached, or some other error happens, the req.rcl.xadd command will throw an error. Here we merely send that error back to the client.

Listening for Messages

At this point, we are ready to make changed to the setup_event_listen function, using the changes above.

var stream = "mystream";

function setup_event_listen(req) {
    /* check for username */
    req.user_name=getuser(req);
    if(!req.user_name){
        req.wsSend({from: "System", id:req.websocketId, msg: "No user name provided, disconnecting"});
        setTimeout(function(){
            req.wsEnd();
        },5);
        return;
    }

    function receive_message(strdata) {
        if(!strdata){ // undefined on disconnect
            req.wsSend({from:'System', id:req.websocketId, msg: "you are now disconnected."})
            req.wsEnd();
            return;
        }
        var data = strdata.data[0].value;
        if(data.id != req.websocketId)
            sendWsMsg(req,data);
     }

    /* what to do if we are sent a message from another user. */
    var subscriptions = {};
    subscriptions[stream]='$'; //only listening for one stream
    req.rcl.xread_auto_async(subscriptions, receive_message);

    // set up function for when this user disconnects (either by browser disconnect or req.wsEnd() )
    req.wsOnDisconnect(function(){
        rtrigger(req,{from:'System', id:req.websocketId, msg: req.user_name+" has left the conversation"});
        req.rcl.close();
    });

    /* send a notification to all listening that we've joined the conversation */
    // msg -> everyone listening
    rtrigger(req, {from:'System', id:req.websocketId, msg: req.user_name+" has joined the conversation"});

    req.wsSend( {from: "System", msg: `Welcome ${req.user_name}`} );
}

However, when the client first connects, we also want to send a few of the old messages, so the client can see what they missed. We will use another Redis Stream command xrevrange and wrap it in a try{} catch(e){} block as well.

function setup_event_listen(req) {
    /* check for username */
    req.user_name=getuser(req);
    if(!req.user_name){
        req.wsSend({from: "System", id:req.websocketId, msg: "No user name provided, disconnecting"});
        setTimeout(function(){
            req.wsEnd();
        },5);
        return;
    }

    function receive_message(strdata) {
        if(!strdata){ // undefined on disconnect
            req.wsSend({from:'System', id:req.websocketId, msg: "you are now disconnected."})
            req.wsEnd();
            return;
        }
        var data = strdata.data[0].value;
        if(data.id != req.websocketId)
            sendWsMsg(req,data);
     }

    /* what to do if we are sent a message from another user. */
    var subscriptions = {};
    subscriptions[stream]='$'; //only listening for one stream
    req.rcl.xread_auto_async(subscriptions, receive_message);

    // set up function for when this user disconnects (either by browser disconnect or req.wsEnd() )
    req.wsOnDisconnect(function(){
        rtrigger(req,{from:'System', id:req.websocketId, msg: req.user_name+" has left the conversation"});
        req.rcl.close();
    });

    /* send a notification to all listening that we've joined the conversation */
    // msg -> everyone listening
    rtrigger(req, {from:'System', id:req.websocketId, msg: req.user_name+" has joined the conversation"});

    try {
        //send the last <=50 messages
        var msgRange = req.rcl.xrevrange(stream, '+', '-', "COUNT", 50);

        for (var i=msgRange.length-1; i>-1; i--) {
            var msg = msgRange[i];
            if(msg.value.from!='System')
                sendWsMsg(req,msg.value);
        }

        // send a welcome message to client from the "System".
        req.wsSend( {from: "System", msg: `Welcome ${req.user_name}`} );
    } catch(e) {
        req.wsSend( {from: "System", msg: sprintf("Error getting messages: %s",e) });
    }
}

Forwarding Messages

Using our rtrigger() function above, we can also alter our forward_messages() function. There are two changes from the rampart.event version : 1) ev.trigger is replaced with rtrigger and when sending a file, we base64 encode it using sprintf("%B").

function forward_messages(req) {
    /* we are sent a file from the client in two parts: 1) JSON meta info, 2) binary data */
    var fileInfo;  //variable for JSON meta info
    var file;  // will hold object with meta data and binary file content

    /* STEP 1:  Check message type:  Either -
                   1) metadata for an incoming file
                   2) binary data for an incoming file
                   3) text message
    */

    // check non binary messages for JSON
    if(!req.wsIsBin && req.body.length)
    {
        /* messages are just plain text, but
           if it is a file, first we get the file meta info in JSON format */
        try{
            fileInfo=JSON.parse(req.body);
        } catch(e) {
            /* it is not binary, or json, so it must be a message */
            fileInfo = false;
        }
    }

    /* if it is binary data, we assume it is a file
       and the file info was already sent            */
    if(req.wsIsBin)
    {
        if(req.files && req.files.length)
        {
            //get the first entry of file meta info
            file = req.files.shift();
            // add the body buffer to it
            file.content = sprintf("%B", req.body);
            // tell everyone who it's from
            file.from=req.user_name;
        }
        else // handle unlikely case that metadata is not available.
            file = {from:req.user_name, name:"", type:"application/octet-stream", content:req.body};
    }
    else if(!fileInfo)
        req.body = sprintf("%s",req.body);//It's not binary, convert body buffer to a string


    /* STEP 2:  Process info from step 1 -
                   1) if it is file metadata, save it in the "req.files" var and wait for next message
                   2) if we have both metadata and file content, trigger event and send to all other users
                   3) if we have a text message, trigger event to send message to all other users
    */
    if(fileInfo && fileInfo.file)
    {
        if(!req.files)
            req.files = [];
        /* store file meta info in req where we will retrieve it next time */
        req.files.push(fileInfo.file);
        // do nothing and get the actual binary file in the next message
    }
    else if (file)
    {
        /* we received a file, reassembled its meta info.  Send it to all that are listening */
        rtrigger(req, {from:req.user_name, id:req.websocketId, file: file});
    }
    else if(req.body.length)
    {
        //send the plain text message to whoever is listening
        rtrigger(req, {from:req.user_name, id:req.websocketId, msg:req.body});
    }

    /* Step 3:
         We received data, but here no data is sent back to the client.  However,
         it is sent to others using trigger and receive by the rampart.event.on
         function which they registered in their own connections.  So we can just return
         here
    */
}

Full Script with Redis

rampart.globalize(rampart.utils);
var redis=require("rampart-redis");

var stream = "mystream";

function init(req) {
    if(req.rcl)
        return;

    var redisDir=serverConf.dataRoot + '/redis_chat';
    var redisConf = "bind 127.0.0.1 -::1\n"                      +
                    "port 23741\n"                               +
                    "pidfile " + redisDir + "/redis_23741.pid\n" +
                    "dir " + redisDir + "\n";

    /* make the directory for redis */
    var dirstat = stat(redisDir);
    if(!dirstat) {
        try {
            rampart.utils.mkdir(redisDir);
        } catch (e){
            return e;
        }
    }

    /***** Test if Redis is already running *****/
    try {
        req.rcl=new redis.init(23741);
        return;
    } catch(e){}

    /***** LAUNCH REDIS *********/
    var ret = shell("which redis-server");

    if (ret.exitStatus != 0) {
        return "Could not find redis-server in PATH";
    }

    var rdexec = trim(ret.stdout);

    ret = exec("nohup", rdexec, "-", {background: true, stdin:redisConf});
    var rpid = ret.pid;

    sleep(0.5);

    if (!kill(rpid, 0)) {
        return "Failed to start redis-server";
    }

    try {
        req.rcl=new redis.init(23741);
        return;
    } catch(e){
        return e;
    }
    return;
}

function getuser(req){
    /* Here is where you can look at headers, cookies or whatever
       to find user name. */
    return req.cookies.username;
}

function sendWsMsg(req,data) {
   if (data.file) {
   // sending a file
        // JSON from redis must be decoded.
        try{
            data.file = JSON.parse(data.file);
        } catch(e){}
        if(!data.file.content)
        {
            return;
        }
        //send file metadata, then ...
        req.wsSend({
            from: data.from,
            file: {name:data.file.name, type: data.file.type}
        });
        // ... send actual binary file
        req.wsSend(bprintf('%!B',data.file.content));
    }
    else
    // it is a text message
    {
        req.wsSend({
            from: data.from,
            msg: data.msg
        });
    }
}

function rtrigger(req, obj) {
    try {
        req.rcl.xadd(stream, "MAXLEN", '~', '2000', "*", obj);
    } catch(e) {
        req.wsSend({
            from: "System",
            id:req.websocketId,
            msg: sprintf("Error sending msg: %s", e)
        });
    }
}

function setup_event_listen(req) {
    /* check for username */
    req.user_name=getuser(req);
    if(!req.user_name){
        req.wsSend({from: "System", id:req.websocketId, msg: "No user name provided, disconnecting"});
        setTimeout(function(){
            req.wsEnd();
        },5);
        return;
    }

    function receive_message(strdata) {
        if(!strdata){ // undefined on disconnect
            req.wsSend({from:'System', id:req.websocketId, msg: "you are now disconnected."})
            req.wsEnd();
            return;
        }
        var data = strdata.data[0].value;
        if(data.id != req.websocketId)
            sendWsMsg(req,data);
     }

    /* what to do if we are sent a message from another user. */
    var subscriptions = {};
    subscriptions[stream]='$'; //only listening for one stream
    req.rcl.xread_auto_async(subscriptions, receive_message);

    // set up function for when this user disconnects (either by browser disconnect or req.wsEnd() )
    req.wsOnDisconnect(function(){
        rtrigger(req,{from:'System', id:req.websocketId, msg: req.user_name+" has left the conversation"});
        req.rcl.close();
    });

    /* send a notification to all listening that we've joined the conversation */
    // msg -> everyone listening
    rtrigger(req, {from:'System', id:req.websocketId, msg: req.user_name+" has joined the conversation"});

    try {
        //send the last <=50 messages
        var msgRange = req.rcl.xrevrange(stream, '+', '-', "COUNT", 50);

        for (var i=msgRange.length-1; i>-1; i--) {
            var msg = msgRange[i];
            if(msg.value.from!='System')
                sendWsMsg(req,msg.value);
        }

        // send a welcome message to client from the "System".
        req.wsSend( {from: "System", msg: `Welcome ${req.user_name}`} );
    } catch(e) {
        req.wsSend( {from: "System", msg: sprintf("Error getting messages: %s",e) });
    }
}

function forward_messages(req) {
    /* we are sent a file from the client in two parts: 1) JSON meta info, 2) binary data */
    var fileInfo;  //variable for JSON meta info
    var file;  // will hold object with meta data and binary file content

    /* STEP 1:  Check message type:  Either -
                   1) metadata for an incoming file
                   2) binary data for an incoming file
                   3) text message
    */

    // check non binary messages for JSON
    if(!req.wsIsBin && req.body.length)
    {
        /* messages are just plain text, but
           if it is a file, first we get the file meta info in JSON format */
        try{
            fileInfo=JSON.parse(req.body);
        } catch(e) {
            /* it is not binary, or json, so it must be a message */
            fileInfo = false;
        }
    }

    /* if it is binary data, we assume it is a file
       and the file info was already sent            */
    if(req.wsIsBin)
    {
        if(req.files && req.files.length)
        {
            //get the first entry of file meta info
            file = req.files.shift();
            // add the body buffer to it
            file.content = sprintf("%B", req.body);
            // tell everyone who it's from
            file.from=req.user_name;
        }
        else // handle unlikely case that metadata is not available.
            file = {from:req.user_name, name:"", type:"application/octet-stream", content:req.body};
    }
    else if(!fileInfo)
        req.body = sprintf("%s",req.body);//It's not binary, convert body buffer to a string


    /* STEP 2:  Process info from step 1 -
                   1) if it is file metadata, save it in the "req.files" var and wait for next message
                   2) if we have both metadata and file content, trigger event and send to all other users
                   3) if we have a text message, trigger event to send message to all other users
    */
    if(fileInfo && fileInfo.file)
    {
        if(!req.files)
            req.files = [];
        /* store file meta info in req where we will retrieve it next time */
        req.files.push(fileInfo.file);
        // do nothing and get the actual binary file in the next message
    }
    else if (file)
    {
        /* we received a file, reassembled its meta info.  Send it to all that are listening */
        rtrigger(req, {from:req.user_name, id:req.websocketId, file: file});
    }
    else if(req.body.length)
    {
        //send the plain text message to whoever is listening
        rtrigger(req, {from:req.user_name, id:req.websocketId, msg:req.body});
    }

    /* Step 3:
         We received data, but here no data is sent back to the client.  However,
         it is sent to others using trigger and receive by the rampart.event.on
         function which they registered in their own connections.  So we can just return
         here
    */
}

// exporting a single function
module.exports = function (req)
{
    var err = init(req);
    if(err) {
        sendWsMsg(req, {from:'System', msg: sprintf('%s',err)});
        fprintf(stderr, '%s', err);  //this will go to error log if logging is set on
        return;
    }
    if (req.count==0) {
        /* first run upon connect, req.body is empty
           Here is where we will set up the event to listen for
           incoming message from other users
         */
        setup_event_listen(req);
    } else {
        /* second and subsequent runs below.  Client has sent a message
           and we need to process and forward it to others who are
           listening via rampart.event.on above
         */
        forward_messages(req);
    }


    return null;
}

Improvements

We purposely kept these examples very simple in order to clearly demonstrate the concepts we covered, so it has a long way to go to be used in production.

There are several ways you could improve the above scripts on your way to building a full app. Some of the more obvious ones include:

  • Create a user management system.
  • Improve the look and functionality of the client-side script.
  • Using multiple Redis Streams to add Direct Messaging and Channels.
  • Adding search by storing old messages in a rampart-sql database with a Full Text Index.

Enjoy!