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.
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
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.
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.
socket.io
builds upon the standard WebSocket API. Let's explore some of the additional functionality of socket.io
.
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.
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.
3.141.165.180