Long Polling with web.py

tutorial javascript python webpy webdev

All about long polling (Extra Reading)

Push technology, is a style of internet based communication where the request for a given transaction is initiated by the server, rather than the client. This is especially useful for applications that require real time updates.

Long polling is not a true push, but given the restricted web environment where incoming HTTP requests are restricted, it emulates the push function sufficiently well.

With long poll, the client requests information in exactly the same way. If the server does not have the information required, instead of sending an empty response, the server holds the request until the information becomes available. Once the response is sent to the client, another request is fired again until the server returns with the information requested.

Live Messageboard, or (what we will be doing)

The application will be done with web.py in the back end, serving the web pages for index / and the message submission page /add, alongside responses to http request /get for new updates.

The messages submitted will be stored on a sqlite database, chosen for its simplicity and size. The request send to /get will include a timestamp, whereby only messages sent after that timestamp will be return.

The JSON response will then be received by the client, after the page has been updated, another XHR will be fired to the server, awaiting new updates for the message board

Frontend request firing script

So, in this tutorial, we will be using a standard XMLHttpRequest to send request to the server to obtain any new updates on the message board. creating a generic function startConn function which passes the JSON information sent from the server into a callback function, then sends another request to the server using the link returned from the callback function.

Save all these into static/conn.js:

function startConn(link, callback){
    var conn = new XMLHttpRequest();
    conn.open('GET', link);
    conn.onreadystatechange = function(){
        if(conn.readyState==4){
            var link_new = callback(eval('('+conn.responseText+')'));
            if(!link_new){
                link_new = link;
            }

            startConn(link_new, callback);
        }
    };
    conn.send();
    return conn;
}

Setup Database

Using sqlite3, create a table called messages with a few fields, mainly the message content msg_content and the timestamp, for retrieval purposes, msg_time.

CREATE TABLE messages(
    msg_id INTEGER PRIMARY KEY,
    msg_content TEXT NOT NULL,
    msg_time INTEGER NOT NULL
);

I have saved the database in data.db (you could choose your own name). So, we can start on the main server file main.py:

import web, time, json
render = web.template.render('')

urls = [
    '/', 'Index'
]

class Index:
    def GET(self):
        return '<h1>Hello World!</h1>'

app = web.application(urls, globals())
db = web.database(dbn='sqlite', db='data.db')

if __name__ == '__main__':
    app.run()

This creates a web.py app, which is run on port 8080 (by default). In any browser, go to localhost:8080 and you will see a header welcoming you into the world.

Adding Messages

Next we will work on the interface for submitting messages to the server. This will just be a simple form where there is a textarea for typing messages and a simple submit button. Create this file as form.html under the same directory as the main server file.

<html>
    <body>
        <form action='/add' method='POST'>
            <textarea type='text' name='s' style='width:500px; height:400px;'></textarea>
            <input type='submit' value='Send It In' />
        </form>
    </body>
</html>

For the main server script, we will add a few lines to serve this form.html when users visit the page /add, then we will also add a POST function so as to retrieve the message content and then put it into our database.

import web, time, json
render = web.template.render('')

urls = [
    '/', 'Index',
    '/add', 'MsgAdd'
]

class Index:
    def GET(self):
        return '<h1>Hello World!</h1>

class MsgAdd:
    def POST(self):
        ## Check if the content is empty
        s = web.input().get('s')
        if not s:
            web.seeother('/add')

        ## Insert the message into the database
        db.insert('messages',
            msg_time = str(int(time.time()*1000)),
            msg_content = s
        )

        web.seeother('/add')

    def GET(self):
        ## Show contents of form.html
        return render.form()

app = web.application(urls, globals())
db = web.database(dbn='sqlite', db='data.db')

if __name__ == '__main__':
    app.run()

The Messageboard itself

The main page, when first visited will show all the messages, then connects to the server to see if there is any new messages to load. To do this, we make use of web.py’s templating system to create a index.html, and dynamically load all the message content onto the website when it loads.

$def with (msgs)

<html>
    <head>
        <title>Message Board</title>
        <script src='static/conn.js'></script>
    </head>
    <style>
body, div{
    margin:0;
    padding:0;
}

.msg{
    padding:5px 8px;
    border-bottom:1px solid #000000;
    cursor:pointer;
}

.msg:hover{
    background:#DDD;
}
    </style>
    <body>
        <div id='main'>
            $for msg in msgs:
                <div class='msg'>
                    $msg['content']<br />
                    <div style='text-align:right;'>$msg['time']</div>
                </div>
        </div>
    </body>
</html>

To retrieve the message content, we write a function inside main.py to retrieve the content and then parse it into an object:

def loadMsgs(msgs):
    payload = []
    for msg in msgs:
        payload.append({
            'content':msg['msg_content'],
            'time':time.strftime('%Y-%m-%d %H:%m:%S %p', time.gmtime(msg['msg_time']/1000))
        })

    return payload

Then to serve this page, we now make a few edits to the Index class of the server script:

class Index:
    def GET(self):
        msgs = db.select('messages')
        return render.index(loadMsgs(msgs)[::-1])

Getting new messages

To retrieve new messages, a request is fired to the server and it searches through the database for new messages. And here is the important part, when the server sees that there is no new message, it simplys wait and search again later. To relieve load, a staggering effect is used, where with each failure, the staggering time is increased, until a valid response is received This is how the long polling is achieved.

urls = [
    '/', 'Index',
    '/get', 'MsgGet',
    '/add', 'MsgAdd'
]

class MsgGet:
    def GET(self):
        t = web.input().get('t')
        if not t:
            raise web.notfound()

        msgs = []
        t_slp = 1
        t_add = 1
        t_max = 20
        while not len(msgs):
            if type(msgs) != 'list':
                time.sleep(t_slp)
                t_slp = min(t_slp+t_add, t_max)

            msgs = db.select('messages', where='msg_time>+t)
            msgs = [dict(msg) for msg in msgs]

        return json.dumps({
            'msgs':loadMsgs(msgs)
        })

Now we create a callback function to handle the request send back by the server and put it inside static/main.js, remembering to add it to index.html:

function loadMsgs(obj){
    // Format the information into html and add onto the page
    var html = '';
    for(var i=0; i<obj['msgs'].length; i++){
        var msg = obj['msgs'][i];
        var div = "<div class='msg'>" +
            msg['content'] + "<br />" +
            "<div style='text-align:right;'>" +
            msg['time'] + "</div></div>";
        html += div;
    }
    var old = document.getElementById('main').innerHTML;
    document.getElementById('main').innerHTML = html + old;

    // return an updated link with the current time
    return '/get?t=' + d.getTime().toString();
}

When the page starts, we will trigger the connection using the startConn() function that we have written in conn.js by editing the onload function:

<body onload='var d = new Date(); startConn("/get?t="+d.getTime().toString(), loadMsgs);'>

Improvements to make, places to go

Well, with everything done, save all the files, then run the server file. Add a message on localhost:8080/add and then watch the index page refreshes and updates all the messages automatically.

However, the limited webserver that web.py uses means that it is unlikely that it is capable of supporting multiple long poll request at a time. So next time, I will be teaching you how to setup web.py with lighttpd to handle these request smoothly.