Jabberwocky

snicker-snack!

HTML5, Pubsub and Browser Push

| Comments

I’m currently part of the team working on the prototype of an interesting web application. One of the features of this application is a constant message stream (think Twitter) pushed to the user.

The browser

In a first phase of the project we can count on a HTML5 enabled browser, so that allows us to use one of the options HTML5 offers for browser push, basically functionalities built in the browser which can be accessed with javascript. These are:

  • websockets: these sockets allow bidirectional communication
1
2
3
4
5
6
7
8
9
10
11
12
13
 var ws = new WebSocket("ws://websocket_host/websocket");

 ws.onmessage = function(evt) {
   doSomethingWithMessage(evt);
 }

 ws.onopen = function() {
   subscribe();
 }

 ws.onclose = function() {
   alert('whoops !  lost connection');
 }

obviously, the events callbacks should be modified to the desired action.

  • eventsource: one directional, push only
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var source = new EventSource('/eventsource');

source.onmessage = function (event)
{
  doSomethingWithMessage(evt);
};

source.onerror = function (event)
{
  alert('whoops !  lost connection');
};

source.onopen = function (event)
{
  subscribe();
};

For older browsers (anything that is not Chrome, Firefox 4, Safari or Opera at the time of writing) the push mechanism needs to be replaced by long polling, i.e. a regular check for the resource.

The server

The interaction between browser and back-end is different from our usual client-server relationship. Essentially, the browser establishes a subscribe relationship to the server. Websockets create a bidirectional relationship, in that they both subscribe to something on server side but also publish back to the server (hence pubsub).

Your standard apache is not equipped for this, because it only handles classic request-response (as far as I can tell, I might be missing something). A server needs to be able to react to events: what is called an evented server. Added to that a publish-subscribe mechanism should be used, to dialogue with the browser.

I investigated two possible configurations, in Ruby: mongrel2 with ZeroMQ, and thin with cramp and redis. Node.js would certainly have been another option, but I stuck with Ruby since it might be easier for maintenance, for the time being at least (node might still be an alternative later).

Mongrel2 with 0MQ

I fell for 0MQ immediately, and will certainly be considering it for future projects, because it has a nice bare-bones feel to it. It aims to supercharge existing sockets and connections. It also has easy ruby bindings.

A 0MQ socket is what you get when you take a normal TCP socket, inject it with a mix of radioactive isotopes stolen from a secret Soviet atomic research project, bombard it with 1950-era cosmic rays, and put it into the hands of a drug-addled comic book author with a badly-disguised fetish for bulging muscles clad in spandex.

Mongrel2 is another story. While I like what it promises, the documentation is a bit sparse, and I had to dig through the internets to get my configuration right. Mongrel2 integrates with 0MQ. It offers handlers, which can be managed with Ruby. Mongrel2 is language agnostic, and stores its configuration in sqlite3. People who have looked at the code (I haven’t yet) tell me it’s clean and well-structured.

Mongrel2’s basic config offers publish and subscribe handler, to have the bidirectional communication with the websocket. Good examples here.

So I got it running, but it took a certain amount of pain, especially in getting routing right.

But it works: mongrel2 creates 2 handlers (0mq queues with pubsub) to interact with the websocket. These 0mq sockets can be connected to from any other process, and that other process can publish any data it needs to push the browser, and handle any messages that come in. Excerpt of mongrel configuration:

1
2
3
4
5
6
7
8
9
10
root_dir = Dir(base='html/', index_file='index.html', default_ctype='text/plain')

esupdates = Handler(send_spec='tcp://127.0.0.1:9999', send_ident='54c6755b-9628-40a4-9a2d-cc82a816345e',
                    recv_spec='tcp://127.0.0.1:9998', recv_ident='')

routes = {
    '/websocket': esupdates,
    '/': root_dir
}
(...)

Since the code of the corresponding ruby handler is a bit long, I put a bare-bones commented handler in this gist.

Redis with Cramp and thin

In the meantime, I’d found another, pure ruby example in the Redis documentation.

Basically, cramp is a layer on top of sinatra which adds the functionality to talk with websockets. It makes writing a controller for the websocket blindingly simple, by adding a couple of handy callbacks. Sinatra (well, Rack) can then run on top of thin, which is an evented server. You need to use the edge version of cramp at the moment, because older versions have MySQL dependencies. The ORM has been removed in newer versions to just leave the controller DSL.

This choice ended up winning, because Redis is also used in another local project so the know-how is there. The fact that configuration and use were extremely simple and in ruby contributed to the choice. It also turned out that having named pubsub channels and the ability to subscribe based on a regular expression fitted well with our problem domain.

Redis has a lot of good functionality combined with a friendly programmer interface: we also use the sets, sorted sets and the key-value store. It’s blazingly fast. Check out the interactive tutorial if you haven’t played with it yet. The maintainer, Salvatore Sanflippo is a nice guy (being paid by VMWare to work on his stuff full time), which always helps.

The principle is basically the same as for the mongrel2 setup: starting a sinatra-cramp app on Thin with eventmachine, which will listen to the browser and publish to it. Cramp provides a number of callbacks to handle incoming messages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
require 'sinatra/base'
require 'cramp'
require 'yajl'
require 'daemons'

class ApplicationStreamController < Cramp::Websocket
  on_start :create_redis
  on_finish :destroy_redis
  on_data :received_data

  def create_redis
    @pub = EventedRedis.connect
    @sub = EventedRedis.connect
  end
  def destroy_redis
    @pub.close_connection_after_writing
    @sub.close_connection_after_writing
  end

  def received_data(data)
    msg = parse_json(data)

    # handle received data here
    case msg[:action]
    when 'join'
      handle_join(msg)
    end
  end

  # message channel is the redis channel which the socket subscribes to
  def handle_join
    @sub.subscribe(msg[:channel]) do |type, channel, message|
       render message
    end
  end

end

Redis is used in this example as the publish-subscribe pipe to make any other process send information to this process, which is then pushed to the browser. I’d like to add the functionality to talk with Eventsource to Cramp (instead of websockets), since we only push to the browser – I’ll see if I find the time.

Update: people suggest using em-hiredis for non-blocking redis, and the slimmer em-websocket instead of going through the layers of cramp. Sounds interesting, and I’ll definitely investigate. socket.io is rumored to provide graceful degradation from the html5 features.

So

Ingredients for browser push:

  • javascript websocket or eventsource in the browser
  • back-end evented server to interact/react to pubsub
  • back-end mechanism that speaks the right http to push messages to the browser

Two possible configurations were presented here, one with mongrel2 and zeromq, the other with sinatra/cramp on thin and redis.

Comments