Building a simple Socket.IO app

Before you add Socket.IO to your current PacktChat app, let's first build a couple of very simple apps. This will first allow us to understand what we are doing, and then build on it.

Installing the package

The first thing, of course, is to get the package from npm. We will do this exactly like we did in the last chapter, by adding all the packages to package.json and running npm install.

Socket.IO has quite a few dependencies, so this may take a minute or two. Once that is done, you can create your first app. Create a directory named first app, create an app.js file, and add the following code in it:

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

io.sockets.on('connection', function(socket){
  socket.emit('ping');

  socket.on('pong', function(data){
    console.log('pong');
  });
});

Since Socket.IO is event driven, we start off by listening for a connection event. Do not think of this in terms of HTTP actions as it does not map to any. While we will run them on the server, HTTP and Socket.IO respond to requests in different ways. This event gives us access to the socket that we will then use to communicate with the client.

When we want to send an event to a client from the server-side socket, we use the emit method. It will send a message over the socket to the client. On the client side, it needs to have a listener with the same event name.

For the opposite actions, we will need to listen for events sent from the client. This is where we use the on method. This will allow the client to send a message to the server.

Not much for now. Our app has no one to talk to. We need to build the client side now.

Building the client

Our Socket.IO server needs something to communicate with and that is what we will build now. The server is going to send a 'ping' event to the client and it will return a 'pong' event. Just as the server needs the Socket.IO server framework, our client needs the Socket.IO client framework. The Socket.IO client library is at the node_modules/socket.io/node_modules/socket.io-client directory. Inside this directory, there are the socket.io.js and socket.io.min.js files. We will create a symlink to the file by running the following command in the terminal:

ln –s node_modules/socket.io/node_modules/socket.io-client/socket.io.js

There are also a couple of flash objects. These flash objects are used to give older browsers that lack WebSocket abilities (mainly IE 8 and IE 9) the ability to use Socket.IO. If you are using a newer version of Chrome or Firefox, you can just use the JavaScript file.

We need to create our HTML page for the client to use. Create an index.html page alongside your app.js and socket.io.js. The following code is what the file should be like:

<!DOCTYPE html>
<html>
<head>
  <title>Ping Pong</title>
  <script type="text/javascript" src="socket.io.js"></script>
</head>
<body>
<script>
var socket = io.connect('http://localhost:4000');
socket.on('ping', function(data){
  console.log('ping');
  socket.emit('pong');
});
</script>
</body>
</html>

This is a pretty empty page; in fact, there is nothing in it. We are just using it to load our JavaScript. We include the socket.io.js file in head so we can get access to the io variable. The first thing we want to do is connect to our Socket.IO server. We have the server listening on port 4000, so we tell the client to connect to it.

I know I mentioned earlier that Socket.IO is nothing like HTTP, but the connection does use HTTP to start. The initial HTTP request is upgraded to a WebSocket connection.

Now that the socket has been created, we can start listening for events. Exactly like the server side, we use on to listen for socket events. We know the server will be sending out a ping event, so we will need to listen for that. We will then log that a ping happened and send an event back to the server. Again, this is the exact same method the server uses. We send off a pong event with the emit method.

When we start our Socket.IO server and load our webpage, we should see the log 'ping' browser to the console and then the log 'pong' server (this will actually happen very quickly). Let's go ahead and try this.

Using Python to serve our site

We have run into an issue. We can't serve our index.html page that we made. Python can help us out here. We will use Python later to build our deploy scripts, so hopefully we have it installed. I am using Mac OS X, which comes with Python already installed. This allows me to run a simple Python command to run an HTTP server in whichever directory I am in:

$ python -m SimpleHTTPServer

You don't have to write any code; just load a module, and it will run right in the console. By default, the server listens on port 8000. If you want to change this, just add the port number as the last parameter, as shown in the following command line:

$ python -m SimpleHTTPServer 8080

Python is a great glue language. It has a great standard library that allows you to build small scripts that do a lot.

We will just use the default port for now. Point your browser to http://localhost:8000, and you should see your blank page.

Ping-Pong

Technically, nothing should happen on the web page. You should see the console of the Node server log pong.

Let's now take a look at the browser. Open your console. I always get to it using Chrome by right-clicking and selecting Inspect Element. Then, click on Console all the way to the right. You should see the following screenshot:

Creating some interaction

At this point, we have sent an event to the client and responded back. Technically, we could have easily done this with an Ajax call, so let's add some interaction between browsers to highlight what Socket.IO can do. First of all, clean out the socket.on('connection') function. We are going to write all new code. The following is what our app.js should look like:

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

io.sockets.on('connection', function(socket){
  
socket.on('join', function(data){
    io.sockets.emit('userJoined', data);
    socket.username = data.username;
  });
  socket.on('ping', function(data){
    
    io.sockets.emit('ping', {username: socket.username});
  });
});

Let's look at each new event listener separately. The first listens for an event called join. The first thing it does is emit an event called userJoined. The socket.emit function will just send a message to the client in the connection. You will not have interaction between browsers. io.sockets.emit function will send a message to every single socket that is connected. With this, we can send some messages between browsers.

Next, the function saves some information about this socket on its connection. We are expecting an object with a username attribute. We then attach this to the socket object. The other function is listening for a ping event, much like our original app. We will pull the username off the socket and send it out with io.sockets.emit to ping all the clients.

Adding the browser side

You could restart Node and not much would happen. For every change we make on the server side, we have to make equal and opposite changes on the client side. Open your index.html and add jQuery to head, as shown in the following code:

<head>
  <title>Ping Pong</title>
  <script type="text/javascript" src="socket.io.js"></script>
  <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.0/jquery.js"></script>
</head>

We are using cdnjs, which hosts many of the commonly used JavaScript and CSS libraries on the Internet. It's a great way to include a library quickly, without having to download and drop it in your web server folder.

Now, we need to add some elements to the body, as shown in the following code:

<input type="text" id="username">
<button id="ping">Ping</button>
<ul id="info">
</ul>

It's pretty straightforward: a text box, a button, and a blank unordered list. We have given these IDs, as this makes it very easy and efficient to find them in JavaScript. Remember that this must come before our JavaScript code, otherwise the elements will not be available.

Finally, let's add the JavaScript we need. Add all our new elements as variables at the top of our script tag:

var socket = io.connect('http://localhost:4000');
var $username = $('#username'),
  $ping = $('#ping'),
  $info = $('#info');

If you haven't used jQuery before, the $ is jQuery object. jQuery also has a great selector engine that mimics CSS, so a # is reference to an ID. Each variable should now be connected to each element, respectively.

Now, let's add event handlers. Just like socket.io, jQuery allows you to listen for an event by using the on() function. We will take these one by one.

First, we will build a small utility function. It is just a quick and dirty way to add a list item to the list:

function addLi(message) {
  $info.append('<li>' + message + '</li>');
};

Next, our first event listener:

$username.on('change', function(){
  socket.emit('join', {username: $username.val()});
});

Anytime the username text box changes, we send a join event to the server. We will pass in the value of the text box as the username. If you remember, the server will listen for a join event and send a userJoined event out to everyone with the same data object. This brings us to our next listener:

socket.on('userJoined', function(data){
  addLi(data.username + ' has joined');
});

We are listening now for the event that comes back from the server when someone joins. We will then add a list item to our list:

$ping.on('click', function(){
  socket.emit('ping');
});

This is the listener for the click event on the Ping button. It just sends the event onto the server. Again, when we look back at the server, we see that it listens for a ping event. It then takes that event and sends it to all the socket connections, along with the username that was set for that connection. Here is the browser code that listens for the return ping. It pulls out the username that is passed in the data object and adds it to the list. The following code will replace what is currently in socket.on('ping'):

socket.on('ping', function(data){
  addLi(data.username + ' has pinged!');
});

Open up a browser and add your name to the text box. You should get the message, Josh has joined. Click on the Ping button and you will get the message, Josh has pinged!.

Open another tab and do the same thing (well, use a different name in this tab). Now, go back to the original tab. You will see that another person has joined and pinged. If you split the tabs into separate windows, you can see how quickly the events are sent. It is, for all intents and purposes, instantaneous. Open a few more tabs and see that the events are propagated to all the tabs this way. The following screenshot shows pings between two different tabs:

We have done all this in 14 lines of code on the server and 25 lines on the browser (this includes space between functions and the boilerplate code).

Acknowledgments

Sometimes we want to know if the last action had an error or not. Right now, we are currently working under the assumption that every event will fire off without a hitch. Luckily for us, Socket.IO has an acknowledgement system. On an emit event, we can add an extra function that the server can execute. Let's add this to the server side first in app.js. Update the socket.on('ping') listener, as shown in the following code:

socket.on('ping', function(data, done){
    socket.get('username', function(err, username){
        io.sockets.emit('ping', {username: username});
        done('ack');
    });
});

The acknowledgement function comes in as the second parameter in an on listener function. We can then execute it at any point we want. We are doing it here after we send the data back to all the other clients. In this example, we are just sending ack. We could use it to send back an error. For example, the function can connect to a database and run a query. If there is an error at that point, you can send it back:

done({error: 'Something went wrong'});

This is the key if you are running a required action. There is no worse user experience than actions failing silently. Users will never trust your app. They will always ask, "is it doing something? Should I click on the button again? Is there something else I should have clicked?"

Now, we will update our client. We are going to add a function that will keep track of how many acknowledged pings we have sent. We will only update our count when the acknowledgement returns. We only have to update the socket.emit('ping'), as that is the function we want the acknowledgement on.

In the body, add a div with the sent ID, as shown in the following code:

<input type="text" id="username">
<button id="ping">Ping</button>
<div id="sent"></div>
<ul id="info">
</ul>

In the script tag, we need to initialize another variable and update the on click listener attached to the Ping button:

//with the other initialized variables
Var pingSent = 0;
//further down in the script tag
$ping.on('click', function(){
  socket.emit('ping', null, function(message){
    if (message === 'ack')
    {
      pingSent++;
      $sent.html('Pings sent: ' + pingSent);
    }
  });
});

We have a third parameter on the emit function now. We are not sending data, so we pass null as our data object. Our final parameter is the callback (done()) that the server runs and then passes back our 'ack' message. We check this to make sure that 'ack' was passed, and if so, increment our pingSent counter. If not, we will not increment our counter. This is where we would put our error check. In our example, on the server side, we can do this but we won't. This is only a suggestion:

socket.emit('importantThing', importantData, function(ack){
  if (ack.error !== undefined){
    alert('Something went wrong');
  }else {
    //continue on
  }
});

The following screenshot is what it should look like:

Our app now is still very simple, but you should start to see what you can do with Socket.IO. We have completely real-time events that span multiple browsers. Acknowledgments are even sent on the ping requests. This is all done in 15 lines of code on the server. Guess what? We are not done yet. Let's add some more features to our little Ping-Pong app.