The Mongrel Comet

Comet is a javascript technology used to receive events without AJAX polling. You can read about Comet on Wikipedia.

Mongrel is an excellent HTTP server for Ruby. Have a look at its cute dog.

While I was playing around with a Ruby based DHTML turn based strategy game a few weeks ago, I found myself struggling with latency. I was using an AJAX polling mechanism, and with all the game logic on the server side, things felt laggy. There were any number of options I could have tried. And, in fact, in the end, I just ignored the problem.

However, in the process I read about Comet and began to wonder how hard it would be to add it to Mongrel.

It turned out to be suprisingly easy... after a fashion.

COMET_FORMAT = "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nTransfer-Encoding: chunke\
d\r\nConnection: close\r\n".freeze

class HttpResponse
  def comet(type,  status=200)
    write(Const::COMET_FORMAT % [status, HTTP_STATUS_CODES[status], type])
    write("\r\n")
    return HttpComet.new(self)
  end
end

Above, you can see the first thing we do is add a comet() method to our response object, this returns an object we can stream our data out through. We'd use the HttpComet object like this, writing each integer up to 100 to the connection:

class ExampleCometHandler < Mongrel::HttpHandler
  def process(request, response)
    comet = response.comet("text/plain")
    (0..100).each do |i|
      comet.write("#{i}");
      sleep 1
    end
    comet.close
  end
end

The code that makes this all work lives inside HttpComet. It uses "chunked" HTTP transfer to send portions of the data along at a time.

class HttpComet
  def initialize(response)
    @response = response
  end

  def write(data)
    size = data.size
    @response.write(sprintf("%x;\r\n", size))
    @response.write(data)
    @response.write("\r\n")
    @response.flush
  end

  def close
    @response.write("0;\r\n")
  end
end

Each chunk starts with a number of bytes in hex, followed by an \r\n, then the payload. A chunk of size 0 indicates the end of the file. And that's all there is.

Now, there's one problem. The way I'm doing this has the side effect that only one user can be Comet streaming from any particular handler instance. Registering another instance of the same handler at a different URL and using some kind of redirect to get each user to their own URL and handler can work around this, but it's ugly as sin.

But hey, I learned about HTTP chunked mode!

[Code Available Here]

Update: Oh, it's worth mentioning, getting events to actually trigger on receipt for all platforms is tricky, so instead of just sending data, often Javascript is loaded into an iframe. You can see an example of this in the source.

posted on: 08/09/2006 | path: /tech