In this blog post we’ll be discussing WebSockets and how they can be used to create performant, multi-user web applications. We will be covering:
I have always wondered how instant messaging and notifications worked on my phone. Do phones constantly ping some server to ask if there is a new message? That could work, but it seems inefficient for both the server and my phone; although it would certainly explain my phone’s battery life... Alternatively, is my phone always listening for someone to send it some data? That raises some security concerns; can anyone send data to my phone? Before WebSockets were introduced in 2011, the primary approach for maintaining live data was long polling.
Before we jump into WebSockets, we need to understand the issues surrounding long-polling. Pre-WebSockets, web applications were built around the premise that servers receive requests and send corresponding responses. Note that there is nothing in place for the server to initiate the sending of data to the client. Servers were meant to be purely responsive to requests coming from the client. With this client-server model in place, long-polling was used as a solution to maintaining up-to-date data on the client.Long-polling is the cycle of sending an HTTP request from the client to the server, and the server not sending a response until there is some new data. When the client receives that response from the server, it immediately sends another request to the server.This infinite loop works for maintaining live data, but also taxes the client/browser and the server, as each HTTP request and response also sends a non-negligible amount of metadata, headers, and cookies. For applications that require frequently sending updated data in very short intervals, such as online games or financial tickers, the overhead of HTTP requests is unacceptable. This is where WebSockets come in.
The WebSocket API, more commonly referred to as WebSockets, is a communication protocol that provides a mechanism for sending data between the client and server without the overhead of HTTP requests and long-polling.
WebSocket connections provide three major benefits over long-polling.
So how exactly do WebSockets pull all of that off? It starts with the client sending a HTTP request to the server called the WebSocket handshake request. The handshake request contains a header to upgrade the connection to a WebSocket connection. When the server accepts the handshake, a WebSocket connection is established between the client and server. The newly established WebSocket connection is bidirectional, so the client and server can send messages to each other directly. Remember that with HTTP requests, the server had to wait for an incoming request before sending a response, now either party can send data to the other!
Handshake request opens a bi-directional, WebSocket connection.
In addition to reducing overhead, WebSockets allow servers to connect to multiple clients. Now that the server can send messages without waiting for a client’s request, we can use the server to relay information to all connected clients. This opens up possibilities to creating responsive, multi-user applications that require each client to receive live data. For example, chat rooms, online games, financial tickers, and (for people obsessed like me) live sports scores and stats.
A WebSockets server relaying messages between clients
Now that we have an understanding of how WebSocket connections are made and how they can be used, let’s implement them in a small application. We’ll be going through the following steps:
Setup
Before we start writing code, let’s set up our project’s folder and file structure. We’ll be using a couple Node.js packages in our server, so run the following terminal commands in this project’s folder:
npm init -y
npm install express ws
The final file structure will look like this:
project_folder/
client/
index.html
index.js
styles.css
server/
server.js
package.json
Start by using HTML, CSS and JavaScript to make a simple tic-tac-toe game in the browser. In the body of our index.html file, create three rows that have three buttons in each. Give the buttons the starting text of a dash:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tic-Tac-Toe</title>
</head>
<body>
<div>
<button id="11">-</button>
<button id="12">-</button>
<button id="13">-</button>
</div>
<div>
<button id="21">-</button>
<button id="22">-</button>
<button id="23">-</button>
</div>
<div>
<button id="31">-</button>
<button id="32">-</button>
<button id="33">-</button>
</div>
</body>
</html>
If you open up your index.html file now, you’ll see our 3x3 board with some very small buttons. Later on, we will be using the id’s on these buttons to sync up our two players via WebSockets!
Let’s add some CSS to give our game some style. First import the stylesheet into the index.html file. In the head of the html file add:
<link rel="stylesheet" href="./styles.css"></link>
Then we can add some styling to each of our buttons:
/* styles.css */
button {
width: 100px;
height: 100px;
font-size: 25px;
}
Reload the index.html file and the buttons should be appearing larger and look more like a proper game of tic-tac-toe! Have fun with this part and make the game your own!
If you try to click a button, nothing happens! Let's change that now. Start by importing the index.js file into index.html after the body closing tag. The final index.html file should look like this:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="./styles.css"></link>
<title>Tic-Tac-Toe</title>
</head>
<body>
<div>
<button id="11">-</button>
<button id="12">-</button>
<button id="13">-</button>
</div>
<div>
<button id="21">-</button>
<button id="22">-</button>
<button id="23">-</button>
</div>
<div>
<button id="31">-</button>
<button id="32">-</button>
<button id="33">-</button>
</div>
</body>
<script src="./index.js"></script>
</html>
Now we can add some Javascript code to our game. Update the index.js file to set an event listener on every button that will change its text to “X” or “O”:
// index.js
// wait for the window to load
window.onload = () => {
// grab all of the button elements off of the DOM
const collectionOfButtons = document.querySelectorAll('button');
// boolean that will track if it's the 'X' player's turn
let xTurn = true;
// add an on-click event listener for each button to update its text to X or O
collectionOfButtons.forEach((buttonElement) => {
buttonElement.addEventListener('click', (event) => {
// when clicked, update the button's text to an 'X' or an 'O' & disable the button
event.target.innerHTML = xTurn ? 'X' : 'O';
event.target.disabled = true;
// change the turn tracker boolean
xTurn = !xTurn;
});
});
};
We’re grabbing an HTMLCollection of all of the buttons on our webpage, and applying an on-click event listener to each of them. When clicked, the button will change its text to either “X” or “O” and disable itself so it can’t be clicked more than once.
Let’s refresh the index.html page and play our game locally! You should be able to alternate between placing X’s and O’s on the board due to the xTurn boolean variable. Now we have some of the base functionality on our front-end, let’s start building out our backend.
Before we add WebSockets, we need to create a server. This is where the express library as well as the native Node.js
path
and http
modules will come in.// server.js
const express = require('express');
const path = require('path');
const http = require('http');
const app = express();
const server = http.createServer(app);
// serve up static files (index.html, styles.css, & index.js)
app.use(express.static(path.resolve(__dirname, '../client')));
server.listen(3000, () => { console.log('listening on port 3000'); });
The start of our express server is just to “serve up” the front-end that we just built. The express.static method makes all of the files in our client folder available to the browser upon request.
Startup the server by running node server/server.js in the terminal, and then go to localhost:3000/ in your browser. You should see the exact same app as before when we opened the index.html file, but now a server is providing the HTML, CSS and JS files.
Now that we have a server and front-end, we need to add WebSocket functionality to both of them. In the server.js file:
The server.js file should now look like this:
// server.js
const express = require('express');
const path = require('path');
const http = require('http');
const WebSocket = require('ws');
const app = express();
const server = http.createServer(app);
const wsServer = new WebSocket.Server({ server });
// a set to hold all of our connected clients
const setClients = new Set();
wsServer.on('connection', (socketConnection) => {
// When a connection opens, add it to the clients set and log the number of connections
setClients.add(socketConnection);
console.log('New client connected, total connections is: ', setClients.size);
// When the client sends a message to the server, relay that message to all clients
socketConnection.on('message', (message) => {
setClients.forEach((oneClient) => {
oneClient.send(message);
});
});
// When a connection closes, remove it from the clients set and log the number of connections
socketConnection.on('close', () => {
setClients.delete(socketConnection);
console.log('Client disconnected, total connections is: ', setClients.size);
});
});
// serve up static files
app.use(express.static(path.resolve(__dirname, '../client')));
server.listen(3000, () => { console.log('listening on port 3000'); });
Our server is now configured to create WebSocket connections and relay messages between all the connected clients.
Last step! We have to make changes to the front end to connect to the WebSockets server, send messages (containing the id of a button that was clicked), and receive messages. Make the following changes to the index.js file:
The index.js file should now look like this:
// index.js
window.onload = () => {
// connect to socket server through WebSocket API
const socketConnection = new WebSocket('ws://www.localhost:3000/');
// when connection opens, log it to the console
socketConnection.onopen = (connectionEvent) => {
console.log('websocket connection is open', connectionEvent);
};
// when a message is received from the socket connection,
// the message will contain the id of a button that the other player clicked
socketConnection.onmessage = (messageObject) => {
// if the button is unclicked, changes its text to "O", and disable the button
const buttonClicked = document.getElementById(messageObject.data);
if (buttonClicked.innerHTML === '-') {
buttonClicked.innerHTML = 'O';
buttonClicked.disabled = true;
}
};
// an event listener to log any errors with the socket connection.
socketConnection.onerror = (error) => {
console.log('socket error: ', error);
};
// put an event listener on every button that changes the text to "X",
// disables the button, and sends a message through the socket connection
// with the id of the clicked button
const collectionOfButtons = document.querySelectorAll('button');
collectionOfButtons.forEach((buttonElement) => {
buttonElement.addEventListener('click', (event) => {
// set the target's value to "X" and disable the button
event.target.innerHTML = 'X';
event.target.disabled = true;
// send message (of the clicked button's id) through the socket connection
socketConnection.send(event.target.id);
});
});
};
Restart the server by pressing Ctrl + C in the terminal, and then restart the server by running node
server/server.js
again. Open up two browser windows to localhost:3000/ and watch how the WebSockets server relays messages between the two connected clients!You may have noticed a couple things that are “off” with our tic-tac-toe game. Both players/browsers are playing as if they are player “X.” And an even bigger bug, they don’t have to wait for each other to make another move! I will leave the challenge of implementing some additional game logic to you!
Congratulations on using WebSockets to create a multiplayer tic-tac-toe game! I hope this opens your eyes to the power of WebSockets and how they can be used to create responsive, multi-user applications. Without WebSockets, our application would have to rely on client requests and long-polling to maintain updated data, leading to unnecessary strain on the browser and server. By implementing WebSockets, the server can push new information to each client exactly when it wants to, and without the overhead associated with HTTP requests!
Aside from
ws
, there are many other libraries for implementing WebSockets on both the client and server sides, including:For further reading on the WebSockets protocol, and the browser’s native WebSockets API:
Previously published at https://www.codesmith.io/blog/introduction-to-websockets