Seamless fallbacking with socket.io

Older browsers don't support WebSockets. So in order to provide a similar experience, we need to fall back to various browser/plugin-specific techniques to emulate WebSocket functionality to the best of the deprecated browser's ability.

Naturally, this is a mine field, requiring hours of browser testing and in some cases highly specific knowledge of proprietary protocols (for example, IE's Active X htmlfile object).

socket.io provides a WebSocket-like API to the server and client to create the best-case real-time experience across a wide variety of browsers, including old (IE 5.5+) and mobile (iOS Safari, Android) browsers.

On top of this, it also provides convenience features, such as disconnection discovery allowing for auto reconnects, custom events, namespacing, calling callbacks across the wire (see the next recipe Callbacks over socket.io transport), as well as others.

In this recipe, we will re-implement the previous task for a high compatibility WebSocket-type application.

Getting ready

We'll create a new folder with new client.html and server.js files. We'll also install the socket.io module:

npm install socket.io

How to do it...

Like the websocket module, socket.io can attach to an HTTP server (though it isn't a necessity with socket.io). Let's create the http server and load client.html. In server.js we write:

var http = require('http'),
var clientHtml = require('fs').readFileSync('client.html'),

var plainHttpServer = http.createServer(function (request, response) {
    response.writeHead(200, {'Content-type' : 'text/html'});
    response.end(clientHtml);
  }).listen(8080);

Now for the socket.io part (still in server.js):

var io = require('socket.io').listen(plainHttpServer);

io.set('origins', ['localhost:8080', '127.0.0.1:8080']) ;

io.sockets.on('connection', function (socket) {
  socket.on('message', function (msg) {
    if (msg === 'Hello') {
      socket.send('socket.io!'),
    }
  });
});

That's the server, so let's make our client.html file:

<html>
<head>
</head>
<body>
<input id=msg><button id=send>Send</button>
<div id=output></div>

<script src="/socket.io/socket.io.js"></script>
<script>
(function () {
  var socket = io.connect('ws://localhost:8080'),
    output = document.getElementById('output'),
    send = document.getElementById('send'),

  function logStr(eventStr, msg) {
    return '<div>' + eventStr + ': ' + msg + '</div>';
  } 
  socket.on('connect', function () {
    send.addEventListener('click', function () {
      var msg = document.getElementById('msg').value;
      socket.send(msg);
      output.innerHTML += logStr('Sent', msg);
    });

    socket.on('message', function (msg) {
      output.innerHTML += logStr('Recieved', msg);
    });

  });

}());
</script>
</body>
</html>

The final product is essentially the same as the previous recipe, except it will also work seamlessly in older browsers that aren't WebSocket compatible. We type Hello, press the Send button, and the server says socket.io! back.

How it works...

Instead of passing the HTTP server in a options object, we simply pass it to a listen method.

We use io.set to define our origins whitelist and socket.io does the grunt work for us.

Next, we listen for the connection event on io.sockets, which provides us with a socket to the client (much like request.accept generates our WebSocket connection in the previous recipe).

Inside connection, we listen for the message event on socket, checking that the incoming msg is Hello. If it is we respond with socket.io!.

When socket.io is initialized, it begins to serve the client-side code over HTTP. So in our client.html file we load the socket.io.js client script from /socket.io/socket.io.js.

The client-side socket.io.js provides a global io object. By calling its connect method with our server's address, we acquire the relevant socket.

We send our Hello msg to the server, and say we have done so via the #output div element.

When the server receives Hello it replies socket.io!, which triggers our message event callback on the client side.

Now we have the msg parameter (different to our msg Hello variable) containing the message from the server, so we output it to our #output div element.

There's more...

socket.io builds upon the standard WebSocket API. Let's explore some of the additional functionality of socket.io.

Custom events

socket.io allows us to define our own events, other than message, connect, and disconnect. We listen to custom events after the same fashion (using on), but initiate them using the emit method.

Let's emit a custom event from the server to the client, then respond to the client by emitting another custom event back to the server.

We can use the same code as in our recipe, the only parts we'll change are the contents of the connection event listener callback in server.js (which we'll copy as custom_events_server.js) and the connect event handler in client.html (which we'll copy as custom_events_client.html).

So for our server code:

//require http, load client.html, create plainHttpServer
//require and initialize socket.io, set origin rules

io.sockets.on('connection', function (socket) {
  socket.emit('hello', 'socket.io!'),
  socket.on(''helloback, function (from) {
    console.log('Received a helloback from ' + from);
  });
});

Our server emits a hello event saying socket.io! to the newly connected client and listens out for a helloback event from the client.

So we modify the JavaScript in custom_events_client.html accordingly:

//html structure, #output div, script[src=/socket.io/socket.io.js] tag
socket.on('connect', function () {
  socket.on('hello', function (msg) {
    output.innerHTML += '<div>Hello ' + msg + '</div>';
    socket.emit('helloback', 'the client'),
  });
});

When we receive a hello event, we log to our #output div (which will say Hello socket.io!) and emit a helloback event to the server, passing the client as the intended from parameter.

Namespaces

With socket.io, we can describe namespaces, or routes, and then access them as a URL through io.connect on the client:

io.connect('ws://localhost:8080/namespacehere'),

A namespace allows us to create different scopes while sharing the same context. In socket.io, namespaces are used as a way to share a single WebSocket (or other transport) connection for multiple purposes. See http://en.wikipedia.org/wiki/Namespace and http://en.wikipedia.org/wiki/Namespace_(computer_science).

Using a series of io.connect, calls we are able to define multiple WebSocket routes. However, this won't create multiple connections to our server. socket.io multiplexes (or combines) them as one connection and manages the namespacing logic internally on the server, which is far less expensive.

We'll demonstrate namespacing by upgrading the code from the recipe Transferring data between browser and server via AJAX discussed In Chapter 3, Working with Data Serialization, to a socket.io-based app.

First, let's create a folder, call it namespacing, and copy the original index.html, server.js, buildXml.js, and profiles.js files into it. Profiles.js and buildXml.js are support files, so we can leave those alone.

We can strip down our server.js file, taking out everything to do with routes and mimes and reducing the http.createServer callback to it's last response.end line. We no longer need the path module, so we'll remove that, and finally wrap our server in the socket.io listen method:

var http = require('http'),
var fs = require('fs'),
var profiles = require('./profiles'),
var buildXml = require('./buildXml'),
var index = fs.readFileSync('index.html'),
var io = require('socket.io').listen(
    http.createServer(function (request, response) {
      response.end(index);
    }).listen(8080)
  );

To declare our namespaces with their connection handlers we use of as follows:

io.of('/json').on('connection', function (socket) {
  socket.on('profiles', function (cb) {
    cb(Object.keys(profiles));
  });

  socket.on('profile', function (profile) {
    socket.emit('profile', profiles[profile]);
  });
});

io.of('/xml').on('connection', function (socket) {
  socket.on('profile', function (profile) {
    socket.emit('profile', buildXml(profiles[profile]));
  });
});

In our index.html file we include socket.io.js, and connect to the namespaces:

<script src=socket.io/socket.io.js></script>
<script>
(function () {  // open anonymous function to protect global scope
  var formats = {
    json: io.connect('ws://localhost:8080/json'),
    xml: io.connect('ws://localhost:8080/xml')
  };
formats.json.on('connect', function () {
  $('#profiles').html('<option></option>'),
   this.emit('profiles', function (profile_names) {
      $.each(profile_names, function (i, pname) {
       $('#profiles').append('<option>' + pname + '</option>'),
      });
   });
});

$('#profiles, #formats').change(function () {
  var socket = formats[$('#formats').val()];  
  socket.emit('profile', $('#profiles').val());
});

formats.json.on('profile', function(profile) {
    $('#raw').val(JSON.stringify(profile));
    $('#output').html(''),
    $.each(profile, function (k, v) {
          $('#output').append('<b>' + k + '</b> : ' + v + '<br>'),
        });
});

formats.xml.on('profile', function(profile) {
      $('#raw').val(profile);
      $('#output').html(''),
       $.each($(profile)[1].nextSibling.childNodes,
          function (k, v) {
            if (v && v.nodeType === 1) {
              $('#output').append('<b>' + v.localName + '</b> : '
		     + v.textContent + '<br>'),
            }
          });  
}());

Once connected, the server emits a profiles event with an array of profile_names, our client picks it up and processes it. Our client emits custom profile events to the relevant namespace, and each namespace socket listens for a profile event from the server, handling it according to its format (which is determined by namespace).

Namespaces allow us to separate our concerns, without having to use multiple socket.io clients (thanks to multiplexing). In similar fashion to the sub-protocol concept in WebSockets, we can restrict certain behaviors to certain namespaces giving us more readable code, and easing the mental complexity involved in a multifaceted real-time web app.

See also

  • Creating a WebSocket server discussed in this chapter
  • Callbacks over socket.io transport discussed in this chapter
  • Creating a real-time widget discussed in this chapter
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.165.180