WebSockets

Efficient two-way communication for your web applications.

Unit Goals

To be able to write web applications with WebSockets—and hopefully to know when they should and should not be used.

Overview

The web was originally designed to store and retrieve static resources, and HTTP still kind of reflects that. It isn’t great for interactive games and other applications requiring server notifications.

WebSockets were created to provide:

The websocket protocol runs on top of TCP and exposes a nice API. We can build highly communicative applications on top of the protocol when writing clients in a browser. We’ll cover the details of the websocket handshake and all the other implementation details later. For now, let’s just build apps with the basic WebSocket API.

A Simple WebSocket Server and Web Client

All server side languages (JavaScript, Python, Ruby, Java, C#, Go, etc.) provide libraries to help you write websocket servers. To use web sockets on a Node-based server, npm install ws (Read the docs).

Here’s a simple server, with a little bit of logging:

ws_capitalizeserver.js
const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 59898 });

server.on('connection', (socket, req) => {
  console.log('Connection from', req.connection.remoteAddress);
  socket.on('message', (message) => {
    socket.send(message.toUpperCase());
  });
});

console.log('The capitalization server is running...');

Here’s a client, all shoved into a single HTML page (for now):

ws_capitalizeclient.html
<html>
  <head>
    <meta charset="utf-8">
    <title>Capitalizer Web Client</title>
  </head>
  <body>
    <p>Hello, this is your first websocket example. It is a painfully simple
    hack. It is such a hack that the host is hardcoded. Please don't do this
    in real life. Subsequent example will show you the right way to specify
    a host.</p>
    <p><input type="text"><button>Capitalize</button></p>
    <p id="response"></p>
    <script>
      addEventListener('load', () => {
        const socket = new WebSocket('ws://localhost:59898');
        document.querySelector('button').addEventListener('click', () => {
          socket.send(document.querySelector('input').value);
        });
        socket.addEventListener('message', (event) => {
          document.querySelector('#response').textContent = event.data;
        });
      });
    </script>
  </body>
</html>

Run the server on a command line, and open the client in a browser.

Notice that the programming styles differ a lot between the client and server:

Server SideClient Side
const WebSocket = require('ws');
 WebSockets built in
const server = new WebSocket.Server({ port: p })
const socket = new Websocket('ws://xxx.example.com:p')
server.on('connection', (socket, req) => {
  ...
})
 Connection established in WebSocket constructor
socket.on('message', (data) => {
  ... data ...
})
socket.addEventListener('message', (event) => {
  ... event.data ...
})
socket.send(data)
socket.send(data)
 Runs as separate process  Runs in the browser
You should really write secure servers.

When secure, the client will specify wss in the url scheme instead of ws.

A Tic Tac Toe WebSocket Server and Web Client

Here’s a more complex example. Notice how similar it is to classic TCP server we covered earlier.

ws_tictactoeserver.js
// A server for a multi-player tic tac toe game. Loosely based on an example in
// Deitel and Deitel’s “Java How to Program” book. For this project I created a
// new application-level protocol called TTTP (for Tic Tac Toe Protocol), which
// is entirely plain text. The messages of TTTP are:
//
// Client -> Server
//     MOVE <n>
//     QUIT
//
// Server -> Client
//     WELCOME <char>
//     VALID_MOVE
//     OTHER_PLAYER_MOVED <n>
//     OTHER_PLAYER_LEFT
//     VICTORY
//     DEFEAT
//     TIE
//     MESSAGE <text>

const WebSocket = require("ws")

const server = new WebSocket.Server({ port: 58901 })

;(() => {
  // When null, we are waiting for the first player to connect, after which we will
  // create a new game. After the second player connects, the game can be fully set
  // up and played, and this variable immediately set back to null so the future
  // connections make new games.
  let game = null

  server.on("connection", (ws, req) => {
    console.log("Connection from", req.connection.remoteAddress)
    if (game === null) {
      game = new Game()
      game.playerX = new Player(game, ws, "X")
    } else {
      game.playerO = new Player(game, ws, "O")
      game = null
    }
  })
  console.log("The Tic Tac Toe server is running...")
})()

class Game {
  // A board has nine squares. Each square is either unowned or it is owned by a
  // player. So we use a simple array of player references. If null, the corresponding
  // square is unowned, otherwise the array cell stores a reference to the player that
  // owns it.
  constructor() {
    this.board = Array(9).fill(null)
  }

  hasWinner() {
    const b = this.board
    const wins = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
    ]
    return wins.some(([x, y, z]) => b[x] !== null && b[x] === b[y] && b[y] === b[z])
  }

  boardFilledUp() {
    return this.board.every((square) => square !== null)
  }

  move(location, player) {
    if (player !== this.currentPlayer) {
      throw new Error("Not your turn")
    } else if (!player.opponent) {
      throw new Error("You don’t have an opponent yet")
    } else if (this.board[location] !== null) {
      throw new Error("Cell already occupied")
    }
    this.board[location] = this.currentPlayer
    this.currentPlayer = this.currentPlayer.opponent
  }
}

class Player {
  constructor(game, socket, mark) {
    Object.assign(this, { game, socket, mark })
    this.send(`WELCOME ${mark}`)
    if (mark === "X") {
      game.currentPlayer = this
      this.send("MESSAGE Waiting for opponent to connect")
    } else {
      this.opponent = game.playerX
      this.opponent.opponent = this
      this.send("MESSAGE Your opponent will move first")
      this.opponent.send("MESSAGE Your move")
    }

    socket.on("message", (buffer) => {
      const command = buffer.toString("utf-8").trim()
      console.log(`Received ${command}`)
      if (command === "QUIT") {
        socket.close()
      } else if (/^MOVE \d+$/.test(command)) {
        const location = Number(command.substring(5))
        try {
          game.move(location, this)
          this.send(`VALID_MOVE ${location}`)
          this.opponent.send(`OPPONENT_MOVED ${location}`)
          if (this.game.hasWinner()) {
            this.send("VICTORY")
            this.opponent.send("DEFEAT")
          } else if (this.game.boardFilledUp()) {
            ;[this, this.opponent].forEach((p) => p.send("TIE"))
          }
        } catch (e) {
          console.trace(e)
          this.send(`MESSAGE ${e.message}`)
        }
      }
    })

    socket.on("close", () => {
      try {
        this.opponent.send("OTHER_PLAYER_LEFT")
      } catch (e) {}
    })
  }

  send(message) {
    try {
      this.socket.send(`${message}\n`)
    } catch (e) {
      console.error(e)
    }
  }
}
Exercise: Enumerate the differences between the TCP server we saw earlier and this one.

Here’s the client, put together in CodePen:

See the Pen Tic Tac Toe WebSocket Client by Ray Toal (@rtoal) on CodePen.

Building More Complex Web Applications

In practice, CodePen is wonderful for developing, but you will eventually need to deploy a single webapp which has endpoints for getting your HTML pages as well as web sockets for your bi-directional, full-duplex, communication needs. Once you are ready for this, you might as well start using an HTTP framework.

Let's build an app where all users move around a canvas. Every time they move, they notify the server, which broadcasts the “state” to all participants. We’ll have an HTTP server in which the endpoint GET / returns the HTML page. The server application, at startup, sets up a web socket listener. We’ll build this with the Express framework so create a new folder and:

$ npm init
$ npm install --save ws express

The app should have the following structure (not including the README, the git-related files, and the linter-related files, package.json, node_modules, etc., which should all be present, but for simplicity we’re focusing only on the app itself):

├── app.js
└── public
    ├── party.html
    ├── party.css
    └── party.js

Here is the main application:

app.js
const path = require('path');
const express = require('express');
const WebSocket = require('ws');

const app = express();
app.use(express.static('public'));
app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'party.html')));
app.listen(50000, () => console.log('Party server is running'));

const state = new Map();

function randomColor() {
  const [r, g, b] = Array(3).fill(0).map(() => Math.floor(Math.random() * 200));
  return `rgb(${r}, ${g}, ${b})`;
}

new WebSocket.Server({ port: 50001 }).on('connection', (socket) => {
  state.set(socket, { location: [0, 0], color: randomColor() });
  socket.on('message', (data) => {
    state.get(socket).location = JSON.parse(data);
    const renderData = JSON.stringify(Array.from(state.values()));
    Array.from(state.keys()).forEach(sock => sock.send(renderData));
  });
});

And the HTML file:

party.html
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="party.css">
  </head>
  <body>
    <h1>Party</h1>
    <canvas width="500" height="500"></canvas>
    <script src="party.js"></script>
  </body>
</html>

And the css:

party.css
body {
  margin: 20px;
}

canvas {
  background-color: #dfd;
}

And finally, the JavaScript client:

party.js
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const socket = new WebSocket(`ws://${location.hostname}:50001`);

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  const [x, y] = [e.clientX - rect.left, e.clientY - rect.top];
  socket.send(JSON.stringify([x, y]));
});

socket.addEventListener('message', (event) => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  JSON.parse(event.data).forEach(({ location, color }) => {
    ctx.fillStyle = color;
    ctx.fillRect(...location, 10, 10);
  });
});

To use the app, just visit the main page in your browser. Everyone that joins can see each other. Enjoy.

Exercise: Add random computer players to the application that look and act like zombies. They slowly move toward the closest participant.
Exercise: Write a Chat application that features web sockets.

Socket.io

There’s this really popular library called socket.io which is built on top of WebSockets, providing a bunch of features.

Socket.io documentation

A little game I wrote with Socket.io and Express

Summary

We’ve covered:

  • Why WebSocket technology was created
  • What WebSockets can do
  • Programming details of simple WebSocket servers and clients
  • Building a webapp with WebSockets
  • A little bit about Socket.io