Websocket Workshop

Working with WebSockets

In this tutorial, we’ll be building a little, real-time application using WebSockets. The application is called “Ask the Audience”. Basically, an instructor or someone else can pose a question to the class and the class can vote from one of four options.

Getting Your New Project Off the Ground

First things first, let’s make a new directory for our project and cd into it.

mkdir ask-the-audience && cd ask-the-audience

Let’s make a new file for our server and a directory and empty files for our for our static assets.

touch server.js
mkdir public
touch public/index.html public/client.js public/style.css

Next, we’ll run npm init to bootstrap a package.json and git init to get a git repository rocking and rolling. Set server.js as the “main” file when it asks you.

git init
npm init

Let’s also install some dependencies.

npm install --save express socket.io lodash

You’re ready to get started.

Setting Up Your Server

We’ll be using Express to create a simple web server. It will have three main jobs:

  1. Serve static assets
  2. Host our incoming Socket.io ws:// connections
  3. Route any request for / to /index.html

Recall that Express is a Node library for running basic HTTP servers.

Node actually provides an even more basic module out of the box: http. Express takes this library and adds some helpful features and convenience wrappers around it, similar to how Sinatra adds an additional layer on top of Ruby’s Rack library.

Let’s require our libraries.

// server.js
const http = require('http');
const express = require('express');

Next, we’ll instantiate Express:

// server.js
const http = require('http');
const express = require('express');

const app = express();

So far, so good. Let’s have Express serve our public directory.

// server.js
const http = require('http');
const express = require('express');

const app = express();

app.use(express.static('public'));

Okay, there is a little bit of a problem here: Express will happily serve /index.html, but it will send a 404 if we just visit the root URL (/).

Let’s set it up so that Express will also serve index.html if a user visits /.

// server.js
const http = require('http');
const express = require('express');

const app = express();

app.use(express.static('public'));

app.get('/', (req, res)=> {
  res.sendFile(__dirname + '/public/index.html');
});

This will be enough to cover our server’s basic behavior, but we still need a little work to get the server actually running.

Specifically, the app object we created using express needs to be passed to Node’s http module, which will actually produce a running server from it:

// server.js
const server = http.createServer(app);

Then we need to tell the server what port to listen on. If there is an environment variable set, then we’ll use that—otherwise, we’ll default to 3000.

Having some configuration like this in place can be useful if we need to run the app in a different environment, such as Heroku.

// server.js
const port = process.env.PORT || 3000;

const server = http.createServer(app);
server.listen(port, () => {
  console.log(`Listening on port ${port}.`);
});

We can also use chaining to shorten this up a bit.

// server.js
const server = http.createServer(app)
                 .listen(port, () => {
                    console.log(`Listening on port ${port}.`);
                  });

Finally, we’ll export our server so we can access it later on.

Recall that within npm’s module system, each module can export a single value which will form its “public” interface.

Other modules which require this module will then be able to access this object and use the functionality provided by the module.

// server.js
module.exports = server;

When all is said and done, your server should look something like this:

// server.js
const http = require('http');
const express = require('express');

const app = express();

app.use(express.static('public'));

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/public/index.html');
});

const port = process.env.PORT || 3000;

const server = http.createServer(app)
                 .listen(port, () => {
                    console.log(`Listening on port ${port}.`);
                  });

module.exports = server;

Now that our code is all set, start the server using npm start.

Check it out by visiting http://localhost:3000/. You may want to add something to your index.html file so you can see the changes taking effect.

Setting Up Socket.io

Socket.io is a popular Node library for working with websockets, and we’ll be using it for this purpose in our application.

Socket.io takes an existing http server (like the one we created using http.createServer) and uses it to host websocket connections.

We can set it up like this, below where we define the variable server:

// server.js
// const server = ...
const socketIo = require('socket.io');
const io = socketIo(server);

Our server now supports WebSockets! Woohoo!

So far nothing much will have visibly changed, but go ahead and reload your page just to make sure nothing is broken.

Set Up the Client

Socket.io is a somewhat interesting library in that it provides solutions for clients (i.e. browsers) as well as servers.

We’ve added the appropriate code to get the server-side portion working, so now let’s head over and configure the portion for the browser.

Socket.io adds a route to our server with its client-side library. Restart your server. If you visit http://localhost:3000/socket.io/socket.io.js you can see the source for the client-side library and verify that everything is wired up correctly.

Let’s pop some markup in our index.html to take advantage of our new found functionality.

<!doctype html>
<html>
  <head>
    <title>Ask the Audience</title>
  </head>
  <body>
    <!-- Make sure your JS is at the bottom of the body! -->
    <script src="/socket.io/socket.io.js"></script>
    <script src="/client.js"></script>
  </body>
</html>

Here we’re including a basic HTML document, sourcing the provided socket.io client-side Javascript, and sourcing a client.js file where we’ll keep all of our own client side code.

Communication Between the Client and Server

We have to initiate a WebSocket connection from the client. Let’s establish a connection from client.js.

// public/client.js
const socket = io();

That’s it. We have created a WebSocket connection between the browser and Node. Right now, this is a pretty pointless server.

Node uses an event driven model, which behaves much like mouse clicks and other user actions in the browser. When you initiated your WebSocket connection between the client and the server, a connection event was fired from the io object on the server.

But, if an event is fired and no one is listening, did it ever really happen?

Let’s set up an event listener for the connection event on the server.

// server.js
io.on('connection', (socket) => {
  console.log('A user has connected.');
});

The connection event passes the individual socket of the user that connected to the callback function. Once we have our hands on the individual socket connection, we can add further event listeners to a particular socket.

Keep in mind that WebSockets work around the model of “one socket, one user”. So whenever we’re working with a socket object, we can think of that as a connection to a specific user’s browser.

The io object that socket.io gave us provides several other useful functions as well.

For example, we can get a count of all of the clients currently connected with io.engine.clientsCount. Let’s update our little logger to display this count:

// server.js
io.on('connection', (socket) => {
  console.log('A user has connected.', io.engine.clientsCount);
});

Restart the server and open up a few tabs. You should see the client count increment on upon each connection.

We will also want to make note of when a user disconnects as well. That’s something that happens on the individual socket level. So, we’ll have to next it in our connection listener.

// server.js
io.on('connection', (socket) => {
  console.log('A user has connected.', io.engine.clientsCount);

  socket.on('disconnect', () => {
    console.log('A user has disconnected.', io.engine.clientsCount);
  });
});

Sending Messages to Every Client

We can now keep track of connections on the server, but what about the client? Let’s add the following HTML to index.html.

<div id="connection-count"></div>

Instead of logging the count to the console. We’ll emit an event to all of the connected clients alerting them to the new count of connections. We can emit an event to all connected users using the following method:

io.sockets.emit('usersConnected', io.engine.clientsCount);

Your code should look something like this:

// server.js
io.on('connection', (socket) => {
  console.log('A user has connected.', io.engine.clientsCount);

  io.sockets.emit('usersConnected', io.engine.clientsCount);

  socket.on('disconnect', () => {
    console.log('A user has disconnected.', io.engine.clientsCount);
    io.sockets.emit('usersConnected', io.engine.clientsCount);
  });
});

We’re now sending a custom usersConnected event to each connected browser. But, we have a similar problem as we had before. If we emit an event to the client and no one is listening on the client, it doesn’t really make much of a difference. So, let’s listen for an event:

// public/client.js
const socket = io();

const connectionCount = document.getElementById('connection-count');

socket.on('usersConnected', (count) => {
  connectionCount.innerText = 'Connected Users: ' + count;
});

Check it out in the browser. Open a few tabs and watch the count go up in each of them. Super cool: we’re now sending messages to every connected client.

Sending Messages to a Particular Client

So, we now know that io.sockets.emit will send a message every client. But, what about just one client? The process is roughly the same, but instead of emitting from io.sockets, we’ll emit from just a single socket.

socket.emit('statusMessage', 'You have connected.');

This is what the Socket.io portion of your server should look like at this point:

// server.js
io.on('connection', (socket) => {
  console.log('A user has connected.', io.engine.clientsCount);

  io.sockets.emit('usersConnected', io.engine.clientsCount);

  socket.emit('statusMessage', 'You have connected.');

  socket.on('disconnect', () => {
    console.log('A user has disconnected.', io.engine.clientsCount);
    io.sockets.emit('userConnection', io.engine.clientsCount);
  });
});

To review:

  • socket.emit emits to a single client
  • io.sockets.emit emits to all connected clients

Alright, so now we need to receive that message on the client-side. Let’s make another simple DOM node to store our status message.

<div id="status-message"></div>

We’ll also add the a listener on the client-side to deal with the new status message when it comes over the socket.

// public/client.js
const statusMessage = document.getElementById('status-message');

socket.on('statusMessage', (message) => {
  statusMessage.innerText = message;
});

Sending Messages from the Client to the Server

You could send messages back to the server using regular old AJAX (in the form of a POST request). But, a WebSocket is a two-way connection. This means that we can send messages back to the server over the WebSocket as well.

Let’s send a message from the client to the server, shall we? (This is where you awkwardly say “Yes, totally!” in an otherwise quiet room.)

Right now, we have nothing to send. Let’s add those four buttons to the HTML.

<div id="choices">
  <button>A</button>
  <button>B</button>
  <button>C</button>
  <button>D</button>
</div>

Let’s start by simply adding some event listeners to the buttons.

// client.js
const buttons = document.querySelectorAll('#choices button');

for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', () => {
    console.log(this.innerText);
  });
}

Click some buttons and look at the console to make sure everything works.

Now, let’s swap out that console.log and send some information back to the server.

// client.js
const buttons = document.querySelectorAll('#choices button');

for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', () => {
    socket.send('voteCast', this.innerText);
  });
}

Just like when we sent messages from the server to the client, we also need a listener on the other side deal with the messages sent from the client. Every call to socket.send on the client, triggers a message event on the server.

// server.js
socket.on('message', (channel, message) => {
  console.log(channel, message);
});

Cast a few votes and verify that it works on the server.

Now, we need a way to keep track of the votes that have been cast. Node can keep local variables in memory between requests. So, let’s skip the database and just store everything in memory. If the server crashes, we’ll lose all the data, but YOLO.

Let’s declare our empty object in the top of the scope.

// server.js
const votes = {};

votes will be a little key/value storage. We’ll use the socket.id as the key and the vote as the value.

// server.js
socket.on('message', (channel, message) => {
  if (channel === 'voteCast') {
    votes[socket.id] = message;
    console.log(votes);
  }
});

Let’s also remove a user’s vote when they disconnect.

// server.js
socket.on('disconnect', () => {
  console.log('A user has disconnected.', io.engine.clientsCount);
  delete votes[socket.id];
  console.log(votes);
  io.sockets.emit('usersConnected', io.engine.clientsCount);
});

Open some tabs and cast some votes. Then head over to Terminal to see the object populated with the current votes cast.

Additionally, verify that the votes are removed for closed tabs.

Counting Votes

The key/value object is useful for keeping track of votes. Let’s write a super simple function for counting votes. We’ll start out with a default counter where everything is 0 and then iterate through the votes object and increment the voteCount for each vote.

// server.js
const countVotes = (votes) => {
  const voteCount = {
      A: 0,
      B: 0,
      C: 0,
      D: 0
  };

  for (let vote in votes) {
    voteCount[votes[vote]]++
  }

  return voteCount;
}

Challenge: You installed lodash at the beginning of this tutorial. Can you write a better version of this function using lodash?

Now, that we can count up the votes, let’s emit an event from the server with a tally of all of the votes each time one is cast.

// server.js
io.on('connection', (socket) => {
  console.log('A user has connected.', io.engine.clientsCount);

  io.sockets.emit('userConnection', io.engine.clientsCount);

  socket.emit('statusMessage', 'You have connected.');

  socket.on('message', (channel, message) => {
    if (channel === 'voteCast') {
      votes[socket.id] = message;
      socket.emit('voteCount', countVotes(votes));
    }
  });

  socket.on('disconnect', () => {
    console.log('A user has disconnected.', io.engine.clientsCount);
    delete votes[socket.id];
    socket.emit('voteCount', countVotes(votes));
    io.sockets.emit('userConnection', io.engine.clientsCount);
  });
});

On the client, we’ll log this to the console for now:

// client.js
socket.on('voteCount', (votes) => {
  console.log(votes);
});

Open up a few tabs and cast some votes. Verify that the updated tally is correctly logging to the console.

Your Turn

This is where I leave you, padawan.

Right now, we’re logging to the console. But we’re not updating our interface. Implement the following:

Basic Functionality

  • Render the current tally of votes in the DOM.
  • Emit a event to the user’s individual socket that lets them know when their vote has been cast (and what vote they cast).
  • Update the DOM to show the user what vote they have currently cast (based on the previous step).

User Experience

  • Can you create an interface that is pleasant to use?
  • Can you visualize the votes that have been cast?

Deployment

With the following Procfile, can you deploy you application to Heroku?

web: node server.js

Testing

Can you get mocha tests up and running?

mkdir test
touch test/test.js

Now let’s install our dependencies.

npm install mocha chai --save-dev

Now, open the package.json file in the route directory and make sure within scripts and test that you point npm to use mocha:

//package.json
"scripts": {
  "test": "mocha",
  "start": "node server.js"
},
Extensions: Supertest for Request Testing

Supertest is a library for testing Node.js HTTP servers.

npm install supertest --save-dev

You can then write request tests like:

const expect = require('chai').expect;
const request = require('supertest');

const app = require('../server');

describe('GET /', () => {
  it('responds with success', function(done){
    request(app)
      .get('/')
      .expect(200, done);
  });
});

describe('undefined routes', () => {
  it('respond with a 404', function(done){
    request(app)
      .get('/not-real')
      .expect(404, done);
  });
});
Extensions: Mocking and Testing WebSockets

Socket.IO Client

Some blogs/resources:

Lesson Search Results

Showing top 10 results