Sending Messages from Web Server to Web Client using Websockets, Redis and Webdis

Sometimes your web application will initiate some server-side code that runs for an extended period. This can be a problem for a user left staring at a web page with no feedback – the temptation is to re-submit, go back or even close the application. I found this to be particularly relevant while working on network automation applications that involve a lot of relatively slow interactions with network devices.
What is needed is a way to continuously update the user with messages so that they can see that the application is functioning correctly.
This post will describe how to use WebSockets, Redis, and Webdis to send messages from the server-side of your web application to the client.

The web client “subscribes” to the message queue and the server “publishes” to the queue. Every message published by the server is seen by the client. The client does not have to request the message.

Websockets, Redis and Webdis

Websockets allow a permanent TCP connection to be established between a web server and a web client. This connection can then be used by a messaging application to send messages independent from the request/response cycle of a traditional web page. In other words, the receiver does not have to request messages or periodically poll for messages. Furthermore, the web client does not have to reconstruct the entire web page each time a message is received, the message is just added to the page.

Redis is an open-source, in-memory data structure store that can be used as a message broker using the publish/subscribe paradigm. Message receivers subscribe to a channel and message senders publish to the channel. Messages are sent to all receivers as soon as they are published by the sender.

Webdis is an open-source, lightweight web server that exposes Redis PUB/SUB channels to HTTP clients. Webdis is required because Redis does not have an HTTP interface.

Putting it Together

Client

When the client-side web page (that you need to receive messages) is loaded a JS script is run to establish a WebSocket connection to the Webdis/Redis server and to subscribe to a Redis channel.

<script type="text/javascript">

    var socket = new WebSocket("ws://192.168.199.85:7379/.json");

    var theplace = $("#results")

    var ident = 666

    socket.onopen = function() {
        // subscribe to channel using identifier
        socket.send(JSON.stringify(["SUBSCRIBE", ident]));
    };
    socket.onmessage = function(evt){
        // Extract data string from MessageEvent array and convert to JSON
        var data = JSON.parse(evt.data)
        console.log(evt)

        // Extract the actual message text from the JSON
        var message = data.SUBSCRIBE[2]

        // The server side sends a message of "1" to signal successful connection
        // We do not want this to trigger a message on the client, 
        // so we catch it with an if statement and just print to console
        if (message == 1){
            console.log("Websocket connected.")
        }
        // Any other messages must be rendered to the client
        else{
            theplace.append("<div class=\"alert alert-info\" role=\"alert\"><p>" + message + "</p></div>");
            $(".alert").show()
        }
    };
</script>

Server

When the server-side code (Python in this example) is activated from the web page the Python Redis library is used to establish a TCP connection to the Redis server. With the connection established the Python function can run through its steps and publish messages at appropriate times to keep the user updated. The messages will appear on the client-side web page.


from flask import Flask, render_template, request
import time
import redis

app = Flask(__name__)

@app.route("/test_msg", methods=['GET', 'POST'])
def test_msg():
    if request.method == 'POST':
		    # connect to redis server
        r = redis.client.StrictRedis(host='192.168.199.85', port=6379, db=0, password=None)
        time.sleep(2)

	    # publish message to redis using group name passed in via ident argument
        r.publish(666, 'You have started a server side function')

        time.sleep(2)

	    # publish message to redis using group name passed in via ident argument
        r.publish(666, 'Function running, please wait......')

        time.sleep(2)

         # publish message to redis using group name passed in via ident argument
        r.publish(666, 'Function still running, almost done.......')

        time.sleep(2)
        return render_template("func_finish.html")

    elif request.method == 'GET':
    	return render_template("layout.html")

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True)

Demo Application

In order to demonstrate this functionality, I created a simple web application using Flask running in a virtual environment on my Mac.
Redis and Webdis are installed and running on a VirtualBox Ubuntu 18.04 VM also running on my Mac. The VM is set up in bridged mode so that there is IP connectivity between the VM and the Flask app running in the virtual environment.

This Demo Application is for demonstration purposes only. It is not production-ready.

Set Up the Demo

Run the Demo

With the Flask application running in the virtual environment you should be able to connect to http://localhost:5000/test_msg

In the example below I am using Chrome and have the Developer Tools enabled to see the console. You can see that the websocket has connected successfully.

When the Run button is clicked the Server-side Python function should run and the 3 messages should be displayed.
You can see the 3 message events in the console.

When the Python function is finished, it returns the Flask render_template function and the function_finished.html page is displayed.

Channel Identifier

You may have noticed that in the demo we used a statically defined identifier of 666. The identifier determines the PUB/SUB channel and hence who gets to see the messages. I chose statically defined in the demo to keep things simple but this is not practical in most circumstances. If all the users of your web app used the same identifier they would all see each other’s messages.
It would be better to have a dynamically defined identifier that is unique to each user. This will be the subject of my next post.

Leave a Reply

Your email address will not be published. Required fields are marked *