Thursday, August 18, 2011

Project: Basic Web Application List with Real-time Updating Part 6 (More Robust Connection Between Client and Server)

Introduction

In the last post, we made the sending of messages robust by setting up a simple client acknowledgement system and message resending system. To further improve the robustness of the application, in this blog post, we will add to the client-side a way for it to reconnect to the server when it is disconnected. The client can be disconnected due to network issues or even the expiration of the client token.

Preliminaries: Add html Container for Displaying Connection Status

It would be easier to appreciate the connection and reconnection of the client to the server if there is a way for the user to see the status of the connection. This would also improve the usability of the application as it will indicate to the user when the client is disconnected to the server.

This will involve simple changes to the main.html template and the main.css files.

{% extends "base.html" %}

{% block header %}
<div id="statusmessage">
</div>
{% endblock %}

{% block content %}
<div id="itemform" title="Item Form">
  <form id ="itemformmain" action="/save" method="post">
    <ul>
      <li>
        <div class="formlabel">
          <label for="itemtitle">Title</label>
        </div>
        <div class="formwidget">
          <input name="itemtitle"
                 type="text"
                 placeholder="Place Title Here"
                 size="50"
                 required>
        </div>
      </li>
      <li>
        <div class="formlabel">
          <label for="itemtext">Main Text</label>
        </div>
        <div class="formwidget">
          <textarea name="itemtext"
                    cols="50"
                    rows="5"
                    required></textarea>
        </div>
      </li>
    </ul>
  </form>
</div>
<input type="button" name="additem" value="Add New Item"/>
<div id="mainlist">
</div>
{% endblock %}

{% block addscript %}
<script src="/js/main.js"></script>
{% endblock %}

In the 'main.html' file, we inserted a div element inside the header block.

{% block header %}
<div id="statusmessage">
</div>
{% endblock %}

In the 'main.css' file, we added some style directives for the statusmessage div container we added in 'main.html'.

body {
    font-size: 10px;
}

#itemform ul {
    padding-left: 0;
}

#itemform li {
    display: block;
}

#itemform .formlabel {
    float: left;
    text-align: right;
    width: 60px;
}

#itemform .formwidget {
    margin-left: 70px;
}

#statusmessage {
    text-align: center;
    font-size: 12px;
}

Adding the Reconnection Logic

The client reconnection logic is accomplished in the main Javascript file, 'main.js'.

$(function () {
    var INTERVAL_DELAY_OPEN_CONNECTION = 10000;
    
    // some code omitted

    var setupChannel = function () {
        console.info("trying to open connection");
        $("#statusmessage").text("opening connection");
        
        var messagemap = {};
        $.ajax('/gettoken', {
            type: 'GET',
            dataType: 'json',
            success: function (data) {
                var channel = new goog.appengine.Channel(data['token']);
                var socket = channel.open();
                
                socket.onopen = function () {
                    console.info("connection opened");
                    $("#statusmessage").text("");
                };
                
                socket.onclose = function () {
                    console.info("connection closed");
                };
                
                socket.onerror = function () {
                    console.info("connection error");
                    $("#statusmessage").text("no connection...");
                    setTimeout(setupChannel,
                           INTERVAL_DELAY_OPEN_CONNECTION);
                };
                
                socket.onmessage = function (message) {
                    console.info("message received " \
                             + message.data);
                    var d = $.parseJSON(message.data);
                    
                    var clientid = d['clientid'];
                    var messageid = d['messageid'];
                    
                    if (! messagemap[messageid]) {
                        $("#mainlist").trigger('prependitem',
                                       [d]);
                        messagemap[messageid] = true;                
                    }
                    
                    $.post('/removemessageidfromqueue',
                        {clientid: clientid,
                        messageid: messageid});
                    
                }
            },
            error: function () {
                $("#statusmessage").text("no connection...");
                setTimeout(setupChannel,
                       INTERVAL_DELAY_OPEN_CONNECTION);
            }
        });
    };

    setupChannel();    
});

The changes in the above code is the addition of the INTERVAL_DELAY_OPEN_CONNECTION variable at the top and a minor rewrite of the setupChannel function.

Notice in the setupChannel function, we replaced the '$.getJSON' function call to '$.ajax'. The reason for this is that '$.ajax' allows more options. This allowed us to put an error handler function in case the AJAX request fails; this is also possible with '$.getJSON' but is a lot harder. Another reason for this is that according tot he yayquery podcast, the use '$.ajax' is a better practice than using helper functions such as '$.getJSON'.

When the client is disconnected to the server, the socket.onerror callback function is called. Thus to reconnect to the server, code fro reconnecting to the server must be added to the callback function.

$("#statusmessage").text("no connection...");
                    setTimeout(setupChannel,
                           INTERVAL_DELAY_OPEN_CONNECTION);

In the callback function, the statusmessage div container is first set to the contain the text, "no connection...", to indicate to the user that the connection to the server is not available. Then delayed call to the setupChannel function is setup. The delay is indicated by the value of INTERVAL_DELAY_OPEN_CONNECTION variable. We set a short delay so as to allow the network to stabilize first in case that the disconnection is caused by a temporary network problem.

It is not avoidable that an attempt by the client to reconnect to the server fails. This may happen when the network connection has been totally loss or a temporary network problem takes longer to be resolved. An indication of this problem is the inability of the client to receive a response from a request to the path, '/gettoken'. Thus our AJAX request to '/gettoken' would fail and the error callback function of the $.ajax would be called.

error: function () {
                $("#statusmessage").text("no connection...");
                setTimeout(setupChannel,
                       INTERVAL_DELAY_OPEN_CONNECTION);
            }

The error callback function has similar code to the socket.onerror callback function, meaning it will try again to reconnect to the server after a short delay. If the server remains to be accessible, the client would end up to keep on trying to reconnect after an interval of time. In the case that the server becomes accessible a new connection is established to the server.

Testing the Reconnect Functionality

To test the above logic, I run the App Engine application using the SDK and bound it to the address 0.0.0.0 to make it accessible from another machine.

$ python2.5 ./dev_appserver.py -a 0.0.0.0 basicrtlist/

I then accessed the application from a browser running inside an Ubuntu VM. To simulate the loss of connection, I manually disconnected and reconnected the Ubuntu VM from the virtual network.

Next Post

In the next post, we would cover the case of the client being disconnected from the server for quite sometime that it has already failed to receive some messages from the server. Simply reconnecting to the server does not mean that the client's list of items would be automatically updated to the latest state.  Thus a mechanism should be established to allow the client to update its list to the latest state. This is what is going to be covered in the next post.

No comments:

Post a Comment