Playing with WebSockets

I’ve never used the WebSockets API, so thought I’d try it. My goal: try to get arbitrary binary data chunks from a server as an ArrayBuffer. XMLHttpRequest can do this with the newish responseType property but I’m told it’s awkward, particularly if the data is gzip encoded. Also WebSockets sound like fun.

WebSocket support is sketchy: it’s supported in some modern browsers but not Microsoft’s; IE10 may support it. (To be fair, MS isn’t just dragging their feet, there’s security issues they were concerned about.) Also the Mozilla docs talk about things being prefixed like MozWebSocket; not sure if that’s still the case and if I need an API wrapper. I’m only using Chrome as a client for now.

Docs

Doc quality is a bit frustrating; the MDN docs are incomplete and the W3C Browser API is hard to read and takes a lot of filling in the gaps to understand. Feels like an incomplete spec to me.

Server

Lots of options for implementing the server. All the cool kids are doing Node.js no doubt. I decided to stick with Python and try ws4py (github) on top of gevent.

  1. brew install libevent  # MacOS: it’s half a Unix!
  2. pip install gevent
  3. pip install ws4py

The ws4py docs have a trivial EchoServer implementation in gevent. I started there and added some basic message handling for a couple of commands (Echo and bytes).

from gevent import monkey; monkey.patch_all()
import gevent

from ws4py.server.geventserver import WebSocketServer
from ws4py.websocket import WebSocket

# Construct an array of 256 bytes, 0x00 to 0xff
bytes = b""
for i in range(256):
    bytes += chr(i)

class MyWebSocket(WebSocket):
    def opened(self):
        print "Socket opened"

    def received_message(self, message):
        "Handle requests. Echo is echoed, bytes is a request for binary data"
        print "Received message of length %d" % len(message)
        if message.data.startswith("Echo"):
            self.send(message.data, message.is_binary)
        if message.data == "bytes":
            self.send(bytes, True)

    def closed(self, code, reason):
        print "Socket closed %s %s" % (code, reason)

server = WebSocketServer(('127.0.0.1', 9001), websocket_class=MyWebSocket)
server.serve_forever()

Client

The HTML5Rocks docs have some very simple client Javascript that talks to the server they host. I started there and added some special message logic. Basically the client requests a special byte array consisting of 0x00 .. 0xff and then verifies it
actually received those numbers in an ArrayBuffer.

var connection = new WebSocket('ws://127.0.0.1:9001');
connection.binaryType = "arraybuffer";

connection.onopen = function () {
    // When the socket opens, log it and send two messages"
    console.log("socket opened, sending echo request");
    connection.send('Echo this');
    connection.send('bytes');
};

connection.onerror = function (error) {
    console.log('WebSocket Error ' + error);
};

connection.onmessage = function (e) {
    console.log('Message from server');
    if (e.data instanceof ArrayBuffer) {
        // If it's ArrayBuffer it must be the binary array 0x00 .. 0xff we're expecting
        var byteArray = new Uint8Array(e.data);
        if (byteArray.length != 256) {
            console.log("Error; didn't get expected length 256");
            return;
        }
        for (var i = 0; i < byteArray.length; i++) {
            if (byteArray[i] != i) {
                console.log("Error; got " + byteArray[i] + " at position " + i);
                return;
            }
        }
        console.log("Received expected 256 byte array");
    } else {
        // Print out any other message from the server
        console.log(e.data);
    }
};

Followup

I’d take a long hard look at browser compatibility and the future of WebSockets before building a serious product on it. The docs and sample code don’t give me a warm fuzzy feeling, and there’s been definite issues with IE support, etc in the past.

I have no idea what the state of the art is with respect to gzip encoding on WebSockets. There’s nothing in the spec I can find; maybe it’s up to the server? That’d be fine except AFAIK there’s still no browser-friendly zlib.decompress implementation, no way to decompress standard gzip data. It’s baked in to the browser but not exposed in a Javascript API, unless something’s changed since I last looked.

Apparently WebSocket is not subject to same origin policy. Yay! My Javascript client file at http://192.168.0.5:8989/ is happily getting data from ws://127.0.0.1:9001 and Chrome isn’t complaining. I haven’t done any sort of CORS thing and I don’t think any is baked in to ws4py. Worth investigating more later, particularly in the context of all the WebSocket security tsurris.

For basic hacking it’d be nice if the same server can serve ordinary resources via HTTP and special stuff with a WebSocket server. I think the protocol can do it and there’s a way to get ws4py to allow it (see UpgradableWSGIHandler) but I haven’t tried. For a serious deployment there’s an argument for keeping the two kinds of serving separate, particularly if you’re using WebSocket for long lived persistent connections.