SockJS on steroids

SockJS is a good alternative to Socket.IO, the main difference between the two is the simplicity level.

Socket.IO provide many features while SockJS keep things as simple as possible.

In this tutorial, we will create a SockJS version which handles some important parts of Socket.IO: auto-reconnect, message channel, room support.

Goal

We issue strong problem with Socket.IO (losing connection -and not reconnecting easily-, skipping message sometimes…) and, according to github issue tracker, we are not alone at all. On the other side, after testing SockJS, almost all trouble disappears in few seconds. But Socket.IO do provide useful concepts, so we decide to keep SockJS, with idea from Socket.IO.
You will see here they are quite easy to re-create…

installation

For this tutorial, we will use sockjs-tornado: we need a flexible language for some parts of the system. And the class system will be useful, so Python is the language of choice here.
It should not be so difficult to adapt it in node.js for example.

So we start with a working instance of python, including setuptools and pip.

pip install tornado sockjs-tornado

Quite simple, now we have our python instance setup, we can start to create base system: the message channel.

Server: message channel

By default, SockJS only provide on_open, on_close and the most important on_message. It’s not enough on real projects, and it can be easily extend to a more complete version (close to Socket.IO system):

import json
from sockjs.tornado import SockJSConnection

class SockJSDefaultHandler(SockJSConnection):
    """ Default SockJS handler with message type support """
    def on_message(self, data):
        """ Parsing data, and try to call related handler """
        # Trying to parse response
        data = json.loads(data)
        if data["name"] is not None:
            fct = getattr(self, "on_" + data["name"])
            fct(data["data"])
        else:
            print("SockJSDefaultHandler error: data.name was null")

    def publish(self, name, data, userList):
        """ Publish data """
        # Publish data to all room users
        self.broadcast(userList, {
            "name": name,
            "data": json.dumps(data)
        })

If you follow the code structure, you will see this system create a base structure for on_message:

{
  "name": "string | the handler to call",
  "data": "mixed  | the data linked to message"
}

So on client side, we will add a simple layer to map this behavior. Let’s continue with room support.

Server: multi room support

Now we have a base structure for handling more complex exchange, we can add the multi room support. Here we take the « official » chat example, and extend it a little:

class SockJSRoomHandler(SockJSDefaultHandler):
    """ Room handler """
    _room = {}

    def _gcls(self, _id = None):
        """ Get the classname """
        if _id is None:
            return self.__class__.__name__
        else:
            return self.__class__.__name__ + _id

    def join(self, _id):
        """ Join a room """
        roomId = self._gcls(_id)
        if not SockJSRoomHandler._room.has_key(roomId):
            SockJSRoomHandler._room[roomId] = set()
        SockJSRoomHandler._room[roomId].add(self)

    def leave(self, _id):
        """ Leave a room """
        roomId = self._gcls(_id)
        if SockJSRoomHandler._room.has_key(roomId):
            SockJSRoomHandler._room[roomId].remove(self)
            if len(SockJSRoomHandler._room[roomId]) == 0:
                del SockJSRoomHandler._room[roomId]

    def getRoom(self, _id):
        """ Retrieve a room from it's id """
        roomId = self._gcls(_id)
        if SockJSRoomHandler._room.has_key(roomId):
            return SockJSRoomHandler._room[roomId]
        return None

    def publishToRoom(self, roomId, name, data, userList=None):
        """ Publish to given room data submitted """
        if userList is None:
            userList = self.getRoom(roomId)

        # Publish data to all room users
        self.broadcast(userList, {
            "name": name,
            "data": json.dumps(data)
        })

    def publishToMyself(self, roomId, name, data):
        """ Publish to only myself """
        self.publishToRoom(roomId, name, data, [self])

    def isInRoom(self, _id):
        """ Check a given user is in given room """
        roomId = self._gcls(_id)
        if SockJSRoomHandler._room.has_key(roomId):
            if self in SockJSRoomHandler._room[roomId]:
                return True
        return False

We provide few functions here to manipulate room system: join (to add user into room), leave (to remove a user from room), getRoom and isInRoom (for checking or retrieving), and finally publishToRoom and publishToMyself to get a more easy way to publish content instead of default broadcast function.

So to summarize output: in both class, we define the same response way:

{
  "name": "string | the handler to call",
  "data": "mixed  | the data linked to message"
}

Now SockJS is close to some basic Socket.IO system, with a more simple and clear way than Socket.IO!

Of course, you need to use SockJSDefaultHandler or SockJSRoomHandler instead of SockJSConnection to get support of those system. You should be able to switch without any trouble from your original code if you have one.

Let’s make a classic chat room example:

class ChatSocketHandler(SockJSRoomHandler):
    """ Simple multi room chat system """
    def on_open(self, info):
        """ Open connection """
        pass
 
    def on_close(self):
        """ Close connection """
        if self.roomId is not None:
            self.on_leave(None) 
 
    def on_join(self, data):
        """ Join a room """
        self.roomId = str(data["roomId"])
        self.username = str(data["username"])
        self.join(self.roomId)
        # Publish user just connect
        self.publishToRoom(self.roomId, "join", {
            "user": self.username
        })
 
    def on_leave(self, data):
        """ Leave a room """
        if self.roomId is not None:
            self.publishToRoom(self.roomId, "leave", {
                "user": self.username
            })
            self.leave(self.roomId)
            self.roomId = None
            self.username = None
 
    def on_chat(self, data):
        """ Broadcast new chat message """
        self.publishToRoom(self.roomId, "chat", {
            "message": data["message"],
            "username": self.username
        })

The last part (create route to link with Tornado) is the same as sockjs-tornado official documentation…
You can see here how easy to read the code is… We need to implement a basic Client side now, to support the « message type » feature, and also auto-reconnect.

client: auto-reconnect and message type support

Now our server is ready to handle great message exchange, we miss the corresponding client:

/**
 * Create a new SockJS instance
 *
 * @param namespace {String | null} The namespace to link SockJS with (the route)
*/
var socket = function(namespace) {
	// Store events list
	this._events    = {};
	// The base url
	this._url       = "//"+window.location.hostname;
	// The base port (if there is)
	this._port      = 80;
	// Store the SockJS instance
	this._socket    = null;
	// Store the namespace
	this._namespace = namespace || "";
	// Should reconnect or not
	this.reconnect = true;
};

/**
 * Bind a function to an event from server
 *
 * @param name {String} The message type
 * @param fct {Function} The function to call
 * @param scope {Object | null} The scope to apply for given function
*/
socket.prototype.on = function(name, fct, scope) {
	var fn = fct;
	if(scope) {
		// We bind scope
		fn = function() {fct.apply(scope, arguments);};
	}
	// If it's not existing, we create
	if(!this._events[name]) {
		this._events[name] = [];
	}
	// Append event
	this._events[name].push(fct);
};

/**
 * Send data to server
 *
 * @param name {String} The message type
 * @param data {Object} The linked data with message
*/
socket.prototype.emit = function(name, data) {
	this._socket.send(
		JSON.stringify({
			name: name,
			data: data
		})
	);
};

/**
 * Connect to server
*/
socket.prototype.connect = function() {
	// Disconnect previous instance
	if(this._socket) {
		// Get auto-reconnect and re-setup
		var p = this.reconnect;
		this.disconnect();
		this.reconnect = p;
	}

	// Start new instance
	var base = (this._port != 80) ? this._url + ":" + this._port : this._url;
	var sckt = new SockJS(base + "/" + this._namespace, null, {
		debug : false,
		devel : false
	});

	var _this = this;

	/**
	 * Parse event from server side, and dispatch it
	 *
	 * @param response {Object} The data from server side
	*/
	function _catchEvent(response) {
		var name = (response.type) ? response.data.name : response.name,
			data = (response.type) ? response.data.data : response.data;
		var events = _this._events[name];
		if(events) {
			var parsed = (typeof(data) === "object" && data !== null) ? data : JSON.parse(data);
			for(var i=0, l=events.length; i<l; ++i) {
				var fct = events[i];
				if(typeof(fct) === "function") {
					// Defer call on setTimeout
					(function(f) {
						setTimeout(function() {f(parsed);}, 0);
					})(fct);
				}
			}
		}
	};

	// Catch open
	sckt.onopen = function() {
		_catchEvent({
			name: "open",
			data : {}
		});
	};
	sckt.onmessage = function(data) {
		_catchEvent(data);
	};

	// Catch close, and reconnect
	sckt.onclose = function() {
		_catchEvent({
			name: "close",
			data : {}
		});
		if(_this.reconnect) {
			_this.connect();
		}
	};

	// Link to server
	this._socket = sckt;
};

/**
 * Disconnect from server
*/
socket.prototype.disconnect = function() {
	this.reconnect = false;

	if(!this._socket) {
		return;
	}

	this._socket.close();
	this._socket = null;
};

Tests

You can download this simple chat room example here.

You can also use Pypi to install a ready-to-use version

pip install sockjsroom

It will auto include tornado and sockjs-tornado during installation.

You can found example usage and/or manual installation here

Final words

This is a small introduction to SockJS python version, here you have almost everything needed to build a strong system, the only option missing, is the « retrieve on fail »: get the latests messages when user was disconnected and reconnect.
As this is usually specific to database architecture, we don’t put it here…

Have fun with!

Publicités

4 Commentaires

  1. Watcher

    I’m not seeing the reconnect logic, is there none ? At https://github.com/knowitnothing/sockjs_reconnect there is a much more robust way to reconnect, you should consider that.

    • deisss

      You’re right I forgot to provide a real usage example here, but the reconnect logic is setup here! You just need to bind the event « open », it will be fired on any reconnect.

      I personally do like this: on a « open » event, the client send a « join » event, on « join », the server send all current state data. Doing a better version, is really hard stuff, and all system I could see pretending doing the job, I could fail them in less than a day (on a « standard » case of course)… But I didn’t know that one, so I will take a look 😉

      For a more « deep » inspect: this code replace a previous buggy socketio code in the main application i’m working with, that’s now around 6 months this code is in use, I still didn’t find any lost message due to connection trouble, so I would say it’s pretty much robust one on reconnect!
      Of course, it could be a better solution to detect not sended packet, but I didn’t find any good implementation of that, as the « best » way should be to exchange kind of request id + timestamp, which is by default, a fail case, because server and client often have different timestamp…

  2. deisss

    I wrote down real example case (creating a multi-room chat system):

    You can found it here:
    server: https://gist.github.com/Deisss/7941149

    client: https://gist.github.com/Deisss/7941180

    Also I update the github page to understand how to use those:

    https://github.com/Deisss/python-sockjsroom#full-usage

  3. Thank you very much Deiss
    Have you a mail where I can contact you ?
    I would like to use your library in my own project,

    Regards

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :