Socket.IO

Socket.IO is a simple little library that’s a lot like Node’s core net library. Socket.IO allows you to send messages back and forth with browser clients that connect with your Node server, using an efficient, low-level socket mechanism. One of the nice things about the module is that it provides a shared interface between the browser and the server. That is, you can write the same JavaScript on both in order to do messaging work once you have a connection established.

Socket.IO is so named because it supports the HTML5 WebSockets standard on browsers that support it (and have it enabled). Fortunately, the library also supports a number of fallbacks:

  • WebSocket

  • WebSocket over Flash

  • XHR Polling

  • XHR Multipart Streaming

  • Forever Iframe

  • JSONP Polling

These options ensure that you’ll be able to have some kind of persistent connection to the browser in almost any environment. The Socket.IO module includes the code to power these connection paths on both the browser and the server side with the same API.

Instantiating Socket.IO is as simple as including the module and creating a server. One of the things that’s a little different about Socket.IO is that it requires an HTTP server as well; see Example 7-16.

Example 7-16. Creating a Socket.IO server

    var http = require('http'), 
    io = require('socket.io');
    
server = http.createServer();
server.on('request', function(req, res){
  //Typical HTTP server stuff
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World');
});

server.listen(80);

var socket = io.listen(server);

socket.on('connection', function(client){
  console.log('Client connected');
});

The HTTP server in this example could do anything. In this case, we simply return “Hello World.” However, Socket.IO doesn’t care what the HTTP server will do; it simply wraps its own event listener around all requests to your server. This listener will look for any requests for Socket.IO’s client libraries and service these requests. It passes on all others to be handled by the HTTP server, which will function as usual.

The example creates a socket.io server by calling io.listen(), which is a factory method for the Listener class. listen() takes a function as an argument, which it uses as a callback when a client connects to the server. Because the sockets are persistent connections, you aren’t dealing with a req and res object as you do with an HTTP server. As with net, you need to use the passed client object to communicate with each browser. Of course, it’s also important to have some code in the browser (Example 7-17) to interact with the server.

Example 7-17. A small web page to interact with a Socket.IO server

<!DOCTYPE html>
<html>
  <body>
    <script src="/socket.io/socket.io.js"></script> 
    <script> 
      var socket = io.connect('http://localhost:8080');
      socket.on('message', function(data){ console.log(data) }) 
    </script> 
  </body>
</html>

This simple page starts by loading the necessary Socket.IO client library directly from the Node server, which is localhost on port 8080 in this case.

Note

Although port 80 is the standard HTTP port, port 8080 is more convenient during development because many developers run web servers locally for testing that would interfere with Node’s work. In addition, many Linux systems have built-in security policies preventing nonadministrator users from using port 80, so it is more convenient to use a higher number.

Next, we create a new Socket object with the hostname of the Socket.IO server we are connecting to. We ask the Socket to connect with socket.connect(). Then we add a listener for the message event. Notice how the API is like a Node API. Whenever the server sends this client a message, the client will output it to the browser’s console window.

Now let’s modify our server to send this page to clients so we can test it (Example 7-18).

Example 7-18. A simple Socket.IO server

    var http = require('http'), 
    io = require('socket.io'),
    fs = require('fs');

var sockFile = fs.readFileSync('socket.html');

server = http.createServer();
server.on('request', function(req, res){
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(sockFile);  
});

server.listen(8080);

var socket = io.listen(server);

socket.on('connection', function(client){
  console.log('Client connected');
  client.send('Welcome client ' + client.sessionId);
});

The most striking change in this example is the addition of the fs.readFileSync function, which brings the web page’s external file into the socket server. Now instead of responding to web browser requests with “Hello World,” the Node server will respond with the contents of socket.html. Because readFileSync is a synchronous function, it will block Node’s event loop until the file is read, ensuring that the file is ready to be delivered to clients immediately when the server becomes available for connections.

Now whenever anyone requests anything from the server, unless it is a request to the Socket.IO client library, he will get a copy of socket.html (which might be the code in Example 7-17). The callback for connections has been extended to send a welcome message to clients, and a client running the code from Example 7-18 might get a message in its console like Welcome client 17844937089830637. Each client gets its own sessionId. Currently, the ID is an integer generated using Math.random().

Namespaces

Creating websockets as shown is fine when you are in full control of your application and architecture, but this will quickly lead to conflicts when you are attaching them to an existing application that uses sockets or when you are writing a service to be plugged into someone else’s project. Example 7-19 demonstrates how namespaces avoid this problem by effectively dividing Socket.IO’s listeners into channels.

Example 7-19. A modified web page to interact with Socket.IO namespaces

<!DOCTYPE html>
<html>
  <body>
    <script src="/socket.io/socket.io.js"></script>
    <script>
      var upandrunning = io.connect('http://localhost:8080/upandrunning');
      var weather = io.connect('http://localhost:8080/weather');
      upandrunning.on('message', function(data){
        document.write('<br /><br />Node: Up and Running Update<br />');
        document.write(data);
      });
      weather.on('message', function(data){
        document.write('<br /><br />Weather Update<br />');
        document.write(data);
      });
    </script>
  </body>
</html>

This updated socket.html makes two Socket.IO connections, one to http://localhost:8080/upandrunning and the other to http://localhost:8080/weather. Each connection has its own variable and its own .on() event listener. Apart from these differences, working with Socket.IO remains the same. Instead of logging to the console, Example 7-20 displays its message results within the web browser window.

Example 7-20. A namespace-enabled Socket.IO server

var sockFile = fs.readFileSync('socket.html');

server = http.createServer();
server.on('request', function(req, res){
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(sockFile);
});

server.listen(8080);

var socket = io.listen(server);

socket.of('/upandrunning')
  .on('connection', function(client){
    console.log('Client connected to Up and Running namespace.');
    client.send("Welcome to 'Up and Running'");
});

socket.of('/weather')
  .on('connection', function(client){
    console.log('Client connected to Weather namespace.');
    client.send("Welcome to 'Weather Updates'");
});

The function socket.of splits the socket object into multiple unique namespaces, each with its own rules for handling connections. If a client were to connect to http://localhost:8080/weather and issue an emit() command, its results would be processed only within that namespace, and not within the /upandrunning namespace.

Using Socket.IO with Express

There are many cases where you would want to use Socket.IO by itself within Node as its own application or as a component of a larger website architecture that includes non-Node components. However, when it’s used as part of a full Node application using Express, you can gain an enormous amount of efficiency by writing the entire software stack—including the client-facing views—in the same language (JavaScript).

Save Example 7-21 as socket_express.html.

Example 7-21. Attaching Socket.IO to an Express application: client code

<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost:8080');
socket.on('news', function(data) {
  document.write('<h1>' + data.title + '</h1>' );
  document.write('<p>' + data.contents + '</p>' );
  if ( data.allowResponse ) {
    socket.emit('scoop', { contents: 'News data received by client.' });
  }
});
</script>

This example starts by connecting to the Socket.IO on port 8080. Whenever the Socket.IO server sends a “news” event, the client writes the new item’s title and contents to the browser page. If the news item allows a response, the client socket emits a “scoop” event. The scoop wouldn’t be very interesting to a real reporter; it only contains an acknowledgment that the client received the original news.

This being an example press, the news server responds to the “scoop” event by emitting another news story. The client will receive this new story and print it to the screen also. To prevent this cycle from continuing out of control, an allowResponse parameter is sent with the news story. If it is false or not present at all (see Example 7-22), the client will not send a scoop.

Example 7-22 shows the Express server.

Example 7-22. Attaching Socket.IO to an Express application: server code

var app = require('express').createServer(),
     io = require('socket.io').listen(app);

app.listen(8080);

app.get('/', function(req,res) {
  res.sendfile(__dirname + '/socket_express.html');
});

io.sockets.on('connection', function(socket) {
  socket.emit('news', {
    title: 'Welcome to World News',    contents: 'This news flash was sent from Node.js!',
    allowResponse: true
  });
  socket.on('scoop', function(data) {
    socket.emit('news', {
      title: 'Circular Emissions Worked',
      contents: 'Received this content: ' + data.contents
    });
  });
});

The Express server is created first and then passed into Socket.IO as a parameter. When the Express application is started with the listen() function, both the web server and socket server are activated. Next, a route for the base path (/) is defined as a pass-through for sending the client-side file created in Example 7-21.

The server-side code for the news broadcaster looks very similar to the client-side code for good reason. The same events (emit, on message, connection) behave similarly in Node and in the web browser, making connectivity straightforward. Because data is passed as JavaScript objects between both endpoints, no additional parsing or serialization is needed.

Clearly, we can very quickly gain a lot of power by plugging Socket.IO into Express, but astute programmers will realize that this is one-way communication of limited value, unless the connection initiated by the user’s web browser is represented in the socket stream. Any changes (logout, profile settings, etc.) should be reflected in any socket actions, and vice versa. How to accomplish that? Sessions.

To illustrate the use of a session for authentication, let’s look first at the client-side code, views/socket.html, shown in Example 7-23.

Example 7-23. Client HTML (Jade template): Socket.IO sessions

!!! 5
html(lang='en')
  head
    script(type='text/javascript', src='/socket.io/socket.io.js')
    script(type='text/javascript')
      var socket = io.connect('http://localhost:8080');
      socket.on('emailchanged', function(data) {
        document.getElementById('email').value = data.email;
      });
      var submitEmail = function(form) {
        socket.emit('emailupdate', {email: form.email.value});
        return false;
      };
  body
    h1 Welcome!

    form(onsubmit='return submitEmail(this);')
      input(id='email', name='email', type='text', value=locals.email)
      input(type='submit', value='Change Email')

When rendered in a web browser, this page will display a form text box with a “Change Email” call to action whose default value comes from Express’s session data through the locals.email variable. Upon user input, the application performs these actions:

  1. Create a Socket.IO connection and send all of the user’s email updates as an emailupdate event.

  2. Listen for emailchanged events and replace the contents of the text box with the new email from the server (more on this soon).

Next, have a look at the Node.js portion of Example 7-24.

Example 7-24. Sharing session data between Express and Socket.IO

var io = require('socket.io');
var express = require('express');
var app = express.createServer();
var store = new express.session.MemoryStore;
var utils = require('connect').utils;
var Session = require('connect').middleware.session.Session;

app.configure(function() {
  app.use(express.cookieParser());
  app.use(express.session({secret: 'secretKey', key: 'express.sid', store: store}));
  app.use(function(req, res) {
    var sess = req.session;
    res.render('socket.jade', {
      email: sess.email || ''
    });
  });
});

// Start the app
app.listen(8080);

var sio = io.listen(app);

sio.configure(function() {
  sio.set('authorization', function (data, accept ) {
    var cookies = utils.parseCookie(data.headers.cookie);
    data.sessionID = cookies['express.sid'];
    data.sessionStore = store;
    store.get(data.sessionID, function(err, session) {
      if ( err || !session ) {
        return accept("Invalid session", false);
      }
      data.session = new Session(data, session);
      accept(null,true);
    });
  });

  sio.sockets.on('connection', function(socket) {
    var session = socket.handshake.session;
    socket.join(socket.handshake.sessionId);
    socket.on('emailupdate', function(data) {
      session.email = data.email;
      session.save();
      sio.sockets.in(socket.handshake.sessionId).emit('emailchanged', {
        email: data.email
      });
    });
  });
});

This example uses Connect, a middleware framework that simplifies common tasks such as session management, working with cookies, authentication, caching, performance metrics, and more. In this example, the cookie and session tools are used to manipulate user data. Socket.IO is not aware of Express and vice versa, so Socket.IO is not aware of sessions when the user connects. However, both components need to use the Session object to share data. This is an excellent demonstration of the Separation of Concerns (SoC) programming paradigm.[19]

This example demonstrates using Socket.IO’s authorization, even after connection, to parse the user’s headers. Because the session ID is passed to the server as a cookie, you can use this value to read Express’s session ID.

This time, the Express setup includes a line for session management. The arguments used to build the session manager are a secret key (used to prevent session tampering), the session key (used to store the session ID in the web browser’s cookie), and a store object (used to store session data for later retrieval). The store object is the most important. Instead of letting Express create and manage the memory store, this example creates a variable and passes it into Express. Now the session store is available to the entire application, not just Express.

Next, a route is created for the default (/) web page. In previous Socket.IO examples, this function was used to output HTML directly to the web browser. This time, Express will render the contents of views/socket.jade to the web browser. The second variable in render() is the email address stored in the session, which is interpreted and used as the default text field value in Example 7-23.

The real action happens in Socket.IO’s 'authorization' event. When the web browser connects to the server, Socket.IO performs an authentication routine to determine whether the connection should proceed. The criteria in this case is a valid session, which was provided by Express when the user loaded the web page. Socket.IO reads the session ID from the request headers using parseCookie (part of the Connect framework), loads the session from the memory store, and creates a Session object with the information it receives.

The data passed to the authorization event is stored in the socket’s handshake property. Therefore, saving the session object into the data object makes it available later in the socket’s lifecycle. When creating the Session object, use the memory store that was created and passed into Express; now both Express and Socket.IO are able to access the same session data—Express by manipulating the req.session object, and sockets by manipulating the socket.handshake.session object.

Assuming all goes well, calling accept() authenticates the socket and allows the connection to continue.

Now suppose the user accesses your site from more than one tab in his web browser. There would be two connections from the same session created, so how would you handle events that need to update connected sockets? Socket.IO provides support for rooms, or channels if you prefer. By initiating a join() command with sessionId as the argument in Example 7-24, the socket transparently created a dedicated channel you can use to send messages to all connections currently in use by that user. Logging out is an obvious use for this technique: when the user logs out from one tab, the logout command will instantly transmit to all the others, leaving all of the user’s views of the application in a consistent state.

Warning

Always remember to execute session.save() after changing session data. Otherwise, the changes will not be reflected on subsequent requests.



[19] SoC refers to the practice of breaking down software into smaller single-purpose parts (concerns) that have as little overlapping functionality as possible. Middleware enables this style of design by allowing totally separate modules to interact in a common environment without needing to be aware of each other. Although, as we have seen with modules such as bodyParser(), it remains up to the programmer to understand how the concerns ultimately interact and use them in the appropriate order and context.

Get Node: Up and Running now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.