Eat at Joe's

Last few days I spent integrating SockJS transport library into Ejabberd and Strophe. And here are some thoughts and impressions.

Just in case:

  1. Ejabberd is XMPP (Jabber) server written in Erlang;
  2. BOSH is "Bidirectional-streams Over Synchronous HTTP", basically custom protocol over HTTP which allows browsers to talk to XMPP server. Defined here;
  3. Strophe is XMPP protocol library written in JavaScript;
  4. SockJS is websocket emulation protocol. It works over real websockets if there's browser support or uses one of the fallback transports (long polling, etc).

Why SockJS?

Actually, it is better to rephrase the question: why websockets? Because of latency and cost to maintain active connection. It is much more efficient to use persistent TCP connection than bunch of short-lived HTTP requests.

Why SockJS instead of raw websockets and BOSH as fallback? Three reasons:

  1. SockJS provides websocket-like API, so using SockJS on the client is as simple as creating instance of SockJS class instead of Websocket class;
  2. No need to hack Strophe to support both BOSH and websocket at the same time - SockJS already provides fallback transports;
  3. There are ready-to-use server-side websocket libraries for Erlang (like Cowboy). Instead of writing yet another websocket protocol implementation using Ejabberd HTTP framework, I thought it should be easier to run Cowboy in a separate Erlang process and use its websocket module. With this in mind, why not use sockjs-erlang, as it already runs on top of Cowboy?

Protocol

Instead of using custom handshake (like in BOSH), client sends and receives "normal" XMPP stream header. So, SockJS is used as TCP replacement with exactly same protocol. It is sort of compatible with xmpp-over-websocket draft, except of the websocket handshake (Sec-WebSocket-Protocol header) and using SockJS protocol. However, with minor server-side modification you can connect websocket-compatible XMPP library to SockJS server, as SockJS also exposes "raw" websocket endpoint.

Ejabberd integration

Unfortunately, there's no ready-to-use websocket module for Ejabberd. There's pretty old fork, which you can find here. It is quite hackish (parsing XML with regexps, creating new process for each incoming stanza, etc) and does not support latest websocket spec.

There's supposedly websocket module developed by ProcessOne, but it is not released, so I can't say anything about it.

SockJS integration module was developed as ordinary Ejabberd module, which spawns worker process, which hosts Cowboy with SockJS route. For every incoming SockJS connection, it spawns child process which holds some state, xml_stream and child c2s connection. Whenever something received from the SockJS, it will be fed to the xml_stream. Whenever c2s wants to send something, it will be sent using SockJS API, etc.

Unfortunately, I can not share code, but if you want to do integration yourself - it is fairly easy to do.

Strophe.js integration

I used this gist as basis for the Strophe.js integration. Essentially, it is same Strophe.Connection class as in this gist, but I used slightly newer Strophe.js version and used SockJS class instead of Websocket class.

Latency tests

After trying out this integration, I saw significant latency improvement when using websocket transport. But what struck me as well - even with SockJS polling transports, it seemed like latency is lower than BOSH!

So, I created small HTML page, which does following:

  1. Gets current time stamp
  2. Connects to jabber server and authenticates
  3. Sends ping
  4. Waits for pong
  5. Repeats steps #3 and #4 one hundred times
  6. Calculates time delta

Basically, it won't send next ping before receiving pong. Higher latency - longer it'll take to complete.

Test results

Legend:

Results:

Transport Localhost 1 Localhost 2 Localhost 3 Remote 1 Remote 2 Remote 3
websocket 98 101 104 19126 18418 19478
xhr-streaming 3190 3193 3173 24270 23753 24071
xhr-polling 3181 3184 3192 37980 37750 37811
jsonp-polling 10884 10407 10522 42173 43012 42771
BOSH 21471 21495 21503 47905 48331 48122

Quick analysis

  1. Looks like Ejabberd BOSH implementation agressively buffers outgoing messages to send them in one response. SockJS also does this for polling transports, but doesn't have any internal delays - if there's data in queue, if will be dumped immediately;
  2. For remote Ejabberd instance with pretty high network latency, results are still in favor of SockJS, even though SockJS did 223 requests with polling transport and BOSH did only 118 requests;
  3. SockJS streaming transport worked very well in remote server test with almost websocket-like latency;
  4. JSONP-polling transport appears to be a decent alternative for BOSH;
  5. Some transports "scale" better with higher latency. For example, even though xhr-polling and xhr-streaming had same latency against local server, with remote server xhr-streaming is much more efficient.

Conclusion

I'm pretty happy with the switch. Not sure how sockjs-erlang will handle increased load or what's memory footprint is like, but I'm already seeing much better application responsiveness with SockJS.

We'll see how it goes.

blog comments powered by Disqus