Sunday, August 07, 2011

WebSockets to Socket.io

Update July 16, 2012 There have been several updates to gevent-socketio and socket.io itself. For an updated intro to these libraries, please see m y new post on Realtime Web Chat with Socket.io and Gevent


In a previous blog post, I showed how you can use Gevent, ZeroMQ, WebSockets, and Flot to create a nice asynchronous server that graphs events in real time to multiple web consumers. Unfortunately, Chrome is just about the only browser that my demo worked in due to some issues with the WebSockets standard. So I've updated the example to use Socket.io, a WebSockets-like Javascript library that will work across multiple browsers.



Prerequisites

The best place to start is my last post; you'll need all the libraries I mentioned there. In addition, you need to install gevent-socketio for Socket.io support in gevent.

Changes from WebSockets server

The first thing we need to change is we need to create a SocketIOServer to replace our WSGIServer with a WebSocketHandler. While we're at it, we'll go ahead and merge our little static file server with the SocketIOServer. Our new main method now looks like this:

def main(): 
    '''Set up zmq context and greenlets for all the servers, then launch the web 
    browser and run the data producer''' 
    context = zmq.Context() 
 
    # zeromq: tcp to inproc gateway 
    gevent.spawn(zmq_server, context) 
    # WSGI server: copies inproc zmq messages to websocket 
    sio_server = SocketIOServer( 
        ('', 8000), SocketIOApp(context), 
        resource="socket.io") 
    # Start the server greenlets 
    sio_server.start() 
    # Open a couple of webbrowsers 
    webbrowser.open('http://localhost:8000/graph.html') 
    webbrowser.open('http://localhost:8000/graph.html') 
    # Kick off the producer 
    zmq_producer(context) 

Now let's take a look at our SocketIOApp:

class SocketIOApp(object): 
    '''Funnel messages coming from an inproc zmq socket to the socket.io''' 
 
    def __init__(self, context): 
        self.context = context 
        self.static_app = paste.urlparser.StaticURLParser( 
            os.path.dirname(__file__)) 
 
    def __call__(self, environ, start_response): 
        if not environ['PATH_INFO'].startswith('/socket.io'): 
            return self.static_app(environ, start_response) 
        socketio = environ['socketio'] 
        sock = self.context.socket(zmq.SUB) 
        sock.setsockopt(zmq.SUBSCRIBE, "") 
        sock.connect('inproc://queue') 
        while True: 
            msg = sock.recv() 
            socketio.send(msg) 

What we've done here is overlay the StaticURLParser provided by paste with our own little ZeroMQ-to-Socket.io gateway. So if a request hits any path starting with /socket.io, it gets routed to our gateway. Otherwise it gets handled as a request for a static resource. We could have had a separate WSGIServer dedicated to handling static requests, but it seemed simpler (to me) to go ahead and combine them here.

Client Updates

Now, for our client, we've made a couple of changes as well. For one, we need to include the socket.io.js library. I've included the one from the socket.io CDN to simplify development:

<script src="http://cdn.socket.io/stable/socket.io.js"></script> 

In our javascript, I've also made a few changes to handle the Socket.io protocol better as well as make the graph update a little less choppy. First, we'll set up our socket and some basic variables we'll use later:

$(function() { 
    socket = new io.Socket('localhost'); 
    socket.connect(); 
 
    var $placeholder = $('#placeholder'); 
    var datalen = 100; 
    var plot = null; 
    var series = { 
        label: "Value", 
        lines: { 
            show: true, 
            fill: true 
        }, 
        data: [] 
    }; 

Next, we need to handle the protocol events from socket.io to update our status:

socket.on('connect', function() { 
        $('#conn_status').html('<b>Connected: ' + socket.transport.type + '</b>'); 
    }); 
    socket.on('error', function() { 
        $('#conn_status').html('<b>Error</b>'); 
    }); 
    socket.on('disconnect', function() { 
        $('#conn_status').html('<b>Closed</b>'); 
    }); 

Finally, we will handle the actual messages coming from the server:

socket.on('message', function(msg) { 
        var d = $.parseJSON(msg); 
        series.data.push([d.x, d.y]); 
        while (series.data.length > datalen) { 
            series.data.shift(); 
        } 
        if(plot == null && series.data.length > 10) { 
            plot = $.plot($placeholder, [series], { 
                xaxis:{ 
                    mode: "time", 
                    timeformat: "%H:%M:%S" 
                }, 
                yaxis: { min: 0, max: 5 }, 
                hooks: { 
                    draw: function(plot, canvascontext) { 
                        // Redraw the graph in 50ms 
                        setTimeout(function() { 
                            plot.setData([series]); 
                            plot.setupGrid(); 
                            plot.draw();}, 50); 
                    } 
                } 
            }); 
        } 
    }); 
}); 

What we're doing here is pushing the value from the server on an array, shifting off the old values. Then we set up a hook for Flot to redraw the graph every 50ms when drawing completes. Altogether, there aren't too many changes from our WebSockets demo, and that demo itself wasn't too complex. If you'd like to see all the code, you can at my SourceForge Repo. Hope you've enjoed these posts as much as I've enjoyed playing around with these libraries. Happy hacking!

2 comments:

  1. great program to start with gevent + websocket! but how would you solve it if you want to send something (json) from the client back to the server?

    ReplyDelete
  2. Well, if you want an interactive app, I'd use the socket.emit() method, possibly also spinning up a greenlet to handle the messages. If all you want is something that sends data to the server, you can use plain old XMLHTTPRequest. (The socket.emit() advice should be taken with a grain of salt; I have not tried this.)

    ReplyDelete