Engine-IO and Rubris Client

Engine-IO is the basic connection transport library it provides for XHR binary requests, Websocket upgrading, Ping/Pong heartbeat data and timeouts, basic packetisation formats and the client side event emitter implementation.

Rubris provides a protocol for subscribe, RPC and conversation messaging types using the basic Engine-IO message type as an envelope.

The normal cycle for Engine-IO is (assuming the JS has been loaded already):

  1. Make an XHR initial request to the service path url (a GET) - e.g http://www.myfomain.com/PATH/?EIO=3&transport=polling&t=1441127273190-0
    • Note that the PATH is always constant and is defined as the servicePath in the module config . The transport provided is polling and the initial time identifier determines the sequence.
  2. The server then responds with the protocols available as a response: - {“sid”:”AQEBkpKTVlZWmpqbLy8vJycnZ”,”upgrades”:[“websocket”],”pingInterval”:30000,”pingTimeout”:30000}
    • this contains the SID for the Engine-IO session (sent on all other requests)
    • the upgrades protocols supported
    • the ping interval
    • the ping timeout
  3. Engine-IO then sends an initial ping message (as a POST) - http://www.mydomain.com/PATH/?EIO=3&transport=polling&t=1441127273209-1&sid=AQEBkpKTVlZWmpqbLy8vJycnZ
    • The sid is provided to identify the client
    • the protocol is still polling
  4. Engine then sends a poll message (a GET) - http://www.mydomain.com/PATH/?EIO=3&transport=polling&t=1441127273210-2&sid=AQEBkpKTVlZWmpqbLy8vJycnZ
    • Note 3 and 4 can be done in the other order
    • Engine-IO ignores all response data in a POST response so all responses are a result of the GET request.
  5. Providing the POST returns and Websocket protocol is returned in the initial params it will try and upgrade the connection: - ws://www.mydomain.com/login/?EIO=3&transport=websocket&sid=AQEBkpKTVlZWmpqbLy8vJycnZ

  6. If the upgrade handshakes correctly it will then send a probe text message on the websocket connection - TextFrame: 2probe

  7. The server must respond with a probe response on the websocket connection - TextFrame: 3probe

  8. If this works Engine then sends an upgrade message on the websocket as an upgrade completion notice: - TextFrame: 5

  9. The server should respond with the upgrade response and release any long polling GET request (if any) from step 4 - TextFrame: 5

Once the GET request from step 4 has returned the connection is considered upgraded.

If websocket is not returned by the server or is screened out in the client by means of a filter wrapper method then the handshake is assumed to be complete at step 4.

From then on all messaging is performed using EngineIO type 4 messages with Rubris controlling the payload and batching.

If the upgrade fails for some reason all messages are sent on a new connection (or the existing connection from step 3) and responses are returned via the long polling cycle set up in step 4. Apart from event emitter callbacks to inform the javascript of an upgrade failure the polling transport will continue making request replies without interruption.

Although this sounds slow it means that if the server is quick enough in its upgrade acceptance the upgrade succeeds generally very quickly and if it fails (firewalll or similar issues) the user sees no delay as the initial connections are used.

This is the opposite of say sockJS where the WS has to fail before a long polling socket is initiated.

XHR vs Websockets

The way XHR and websockets affect the packetisation and framing are reasonably simple but requires some explanation.

For Engine-IO XHR transports it uses its own packetisation batching to group outgoing requests together into the same POST request and can cope with the same batching on the response path. For Websockets each message is sent and received as an individual WS frame and Engine-IO does no grouping. This meant that for push data there is reasonable overhead for individual frame processing if the default Engine-IO mechanism is used.

For this reason Rubris defers to the Engine-IO on the client-> server path but will always take over the batching of responses irrespective of transport type into the same EngineIO message packet (either as a single HTTP response or websocket frame), and its own JS library takes care of splitting the batch up. This reduces the processing done by both the browser and the Engine library for frame/message passing on the server -> client path.

So once Engine-IO has upgraded in most cases it sees single messages as push/response messages even if in reality they are actually batches. The batch size is determined on the server code with the size of the writebuffer (by default 256k).

Binary data

In line with the direction of HTTP2.0 is traveling and with an eye to the WebAssembly standards, pretty much all the messages (apart from the upgrade handshaking) take place using binary data. For XHR this means the data is encoded as binary data on the HttpRequest/HttpResponse and for Websockets only binary frames are used.

The Rubris protocol intentionally has no JSON equivalent for its envelopes and client introspection in the browser will require a little tooling (although step through using chrome debug tools is easy enough).

Although this may seem a little strange, Rubris is strongly opposed to using formats such as JSON for its main use case of numeric data. JSON in the authors opinion is a hangover from the document-oriented HTTP legacy and with the advent of WebAssembly and HTTP2, it is expected that the binary processing in the browser will start to become the common approach. Indeed we can see evidence of this with the appearance of browser hosted games using primarily binary info.

This allows use of formats such as the excellent FlatBuffers (or any other low overhead format), which pays no serialisation costs to be used all the way through the stack from top to bottom - without each layer having to be a prisoner of some text format that needs encoding/decoding on each message.

If you would like a protocol built around REST and JSON the author suggest using a standard webserver which will be a far better fit for expectations.

In saying that the protocol places no restriction on the payload used in the messages. If JSON or FIX (or anyhting else for that matter) is required then it is a simple manner to use JSON.parse() or similar on the browser to turn the payload into javascript or text, and do the encoding/decoding in the application code. However, for binary type payload data there is no requirement to jump through the JSON representation of numbers and the overhead of digit string parsing, or of base64 encoding that binary data normally requires in web applications.

Rubris’ goal is to send the data as is (in many ways akin to the way network packet payload is opaque to the transport layer) so that developers who choose to only introspect the data at point of use, may develop JS parsers for the backend data protocols and send native format messages to the browser without translation penalties if so desired.

Rubris and events in the browser

The Rubris library does not wrap Engine-IO in the browser; instead, if anything, it is the other way around.

The Rubris library tries to be minimally invasive on the client and the client developer can use Engine-IO in its native manner to set up and control sockets as normally one would do with the Engine client. E.g:

 if(socket ==null || socket.readyState === 'closed'){
    socket = eio.Socket({ path : uri.path , port: uri.port, 
            protocol: "ws", host: uri.host });

    // set the binary type
    socket.binaryType = 'arraybuffer';
    
    // register the event callbacks
    socket.once('open', function(){
      ...
    });
    socket.on('close', function(){
    });
    socket.on('handshake', function(data){
      ...
    });
    socket.on('upgrade', function(data){
      ...
    });
    socket.on('upgrading', function(data){
      ...
    });
    socket.on('upgradeError', function(error){
      ...
    });
    socket.on('error', function(e){
      ...
    });
  }

In order to integrate Rubris client library one can simply set the socket once opened in to the rubris object which will then hook itself into the socket event emitting functions, which can be used to receive events in the same way as the with the socket. E.g

IMPORTANT: the “message” instance is reused (to prevent the creation of thousands of objects by the library) for each callback. You MUST extract the data you want from the message in the callback. It is NOT safe to cache the actual message itself unless a deep copy is made.

// set the socket into the Rubris instance
socket.once('open', function(){
    rubris.setSocket(socket)
 });


 // This must be done in order for Rubris to get the message callback
socket.on('message', function(data){
    rubris.parseMessage(data);
});

// set up callbacks for the message types
socket.on('channels', function(data){
    ...
});
socket.on('rpc', function(data){
   ...
});

socket.on('subscribeResponse', function(data){
    ...
});
socket.on('push', function(data){
  ...
});
socket.on('conversation', function(data){
    ...
});

// If used (optional may be removed)
socket.on('logout', function(data){
   ...
});
socket.on('login', function(message){
  ...
});

Similarly messages are sent using the socket as one would normally using Engine-IO sockets. the only difference is that Rubris provides methods to construct each message type. E.g

    socket.send(rubris.rpcMessage(body, endpointId, messageid++));

Further examples can be found in the main.js of the sample test application