This article was originally written on the DeadSimpleChat Blog: Build Node.JS Chat App with WebSocket, Node.js Cluster and Redis
Dead Simple Chat offers prebuilt Chat that can be added in seconds to any app or website.
In this blog post, we will build a highly scalable chat application using WebSocket Node.JS and Redis.
This chat application will be high-performance and can handle many concurrent users. Let's get started.
What is Node.JS Cluster?
Node.JS is high-performance javascript runtime, but it is primarily single-threaded.
That means if you have a computer with multiple CPU cores, which all modern computers have your Node.JS application will not be able to leverage all the available CPU cores.
The Node.JS will by default launch a single process that will run on a single CPU core. If you have a 10-core CPU, then the remaining 9 cores will be utilized by your Node.JS program.
The Node.JS cluster module is a built-in Node.JS module and it aims to solve exactly this problem. The cluster module allows you to launch multiple child processes or worker threads that would execute on other CPU cores to fully utilize the multiple cores offered in modern CPUs.
We will use the Node.JS Cluster module to launch multiple threads to handle concurrent connections to our chat server to extract maximum performance.
What are WebSockets?
Websockets allow us to have a real-time connection between the client which is the browser and our backend chat server.
This the client to receive the messages in real time without polling the server, and it allows the server to push the messages to the client.
It creates a bi-directional pipe between the client and the server which will allow us to send and receive the messages.
To handle WebSockets we will use the "Socket.IO" library which offers a nice wrapper around the vanilla WebSockets and provides additional functionality like stick connections which will be much needed in our clustered application.
Why use Redis to store messages?
Redis is a high-performance in-memory data store, and it is super fast. It can handle thousands of operations per second on modest hardware.
Thus using Redis to store messages would add minimal lag to our Chat Message Server.
✴️ You can use Dead Simple Chat as your in-app chat solution, which allows you to add chat highly scaleable chat in minutes.
Step 1: Scaffolding the Application
Let's scaffold the application, we will create a directory named chat-application
mkdir chat-application
Then we will cd
into the directory.
cd chat-application
And we will run the command npm init
npm init -y
The npm init
command will create a package.json
file.
Also, create a folder called as public
it will contain our chat front-end.
mkdir public
Step 2: Installing Dependencies
For our chat application, we will install the following packages:
express - Express is a minimal web application framework that will allow us to easily create a web app in Node.JS
ioredis - ioredis is a high-performance redis driver for Node.JS. This will allow us to connect to the redis database from our Node.JS application.
socket.io - socket.io is a WebSocket library that we will be using as it offers built-in sticky connection support for clustering.
@socket.io/sticky and @socket.io/cluster-adapter - These two packages will be used to setup stick connections in the worker threads.
Run the following command to install the above dependencies
npm install express ioredis socket.io @socket.io/cluster-adapter @socket.io/sticky --save
Step 3: Clustering Setup
Create a file called as server.js
this will contain all the backend code for our chat server and open the server.js
file in your favourite code editor.
Add the following code to the server.js
file:
const cluster = require('node:cluster');
const http = require('node:http');
const numCPUs = require('node:os').availableParallelism();
const process = require('node:process');
const { setupMaster, setupWorker } = require("@socket.io/sticky");
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
const { Server } = require("socket.io");
const { info } = require('node:console');
const express = require("express");
/**
* Checking if the thread is a worker thread
* or primary thread.
*/
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
/**
* Creating http-server for the master.
* All the child workers will share the same port (3000)
*/
const httpServer = http.createServer();
httpServer.listen(3000);
// Setting up stick session
setupMaster(httpServer, {
loadBalancingMethod: "least-connection"
});
// Setting up communication between workers and primary
setupPrimary();
cluster.setupPrimary({
serialization: "advanced"
});
// Launching workers based on the number of CPU threads.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
/**
* Setting up the worker threads
*/
console.log(`Worker ${process.pid} started`);
/**
* Creating Express App and Socket.io Server
* and binding them to HTTP Server.
*/
const app = express();
const httpServer = http.createServer(app);
const io = new Server(httpServer);
// Using the cluster socket.io adapter.
io.adapter(createAdapter());
// Setting up worker connection with the primary thread.
setupWorker(io);
io.on("connection", (socket) => {
// Handling socket connections.
socket.on("message", (data) => {
console.log(`Message arrived at ${process.pid}`);
});
});
// Handle HTTP Requests
app.get("/", (req, res) => {
res.send("Hello world");
});
In the above code snippet we have created a socket.io and express server that runs on multiple cores.
It checks the number of CPUs available and launches the worker threads equal to the number of CPU threads available.
Step 4: Handling Messages
Each time a new client connects to the chat server the io.on("connection", (socket) => { });
code block executes.
io.on("connection", (socket) => {
});
The socket
object allows us to communicate bi-directionally with the client, we can listen to the event emitted by the client and we can also emit events to the client.
io.on("connection", (socket) => {
// Handling socket connections.
socket.on("message", (data) => {
console.log(`Message arrived at ${process.pid}:`, data);
socket.emit("message", data);
});
});
In the above code block, we are listening to the message
event, the name of the event can be anything.
And then we are emitting a message event to the client with some data. Data can be anything, it can be a string or JSON object.
Sending Messages to all the client
The socket.emit
event just sends the message to on client, the client the socket object is associated with.
If you want to send messages to all the connected client, then you can use the method io.broadcast.emit("event", message)
The io.broadcast.emit
method will send the message to all the connected clients.
If you want to send the message to all clients except the current socket instance, then you can use the method socket.broadcast.emit("event", message);
Build a basic chat message server
A very basic chat message server would be a chat message server that sends a message to all connected clients when it receives a new message.
The code for that will look this:
io.on("connection", (socket) => {
// Handling socket connections.
socket.on("message", (data) => {
console.log(`Message arrived at ${process.pid}:`, data);
io.broadcast.emit("message", data);
});
});
We will certainly improve this, by adding validation to the messages, sanitizing the messages and storing them in our Redis database, but apart from that the design of the chat message server is almost complete.
With just a few lines of code, we have built a highly scaleable chat server. Now let's build the front-end and then after that, we will come back to adding persistent storage for the messages.
Step 5: Building the Front-end
Remember we had created a public
folder. We will tell express to serve all the files in the public folder as static assets. To do that we will add the following line of code.
app.use(express.static('public'))
Then in the public folder create a file called index.html
In the public/index.html
file add the following code:
<h1>Hello world</h1>
Now start the server and go to http://localhost:3000
and you will a page with "Hello world" text.
We will build our front-end inside the public folder. In the public folder create two more folders one is js
and css
.
The js
folder will contain the JavaScript code for the front-end and css
folder will contain the CSS.
Scaffolding the Front-End
We will delete everything from the index.html
file and create a basic HTML structure and import some dependencies like the socket.io client to connect to our websocket server and TailwindCSS for styling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@2.50.1/dist/full.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<script src="/js/main.js"></script>
</body>
</html>
Step 6: Building the Chat UI
Now once the front end is scaffolded let's build the front-end chat UI, we will design basic chat UI using TailwindCSS and our final code looks something like this:
<!DOCTYPE html>
<html lang="en" data-theme="business">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div class="grid h-screen">
<div class="flex max-h-screen flex-col">
<div class="header text-md border-b border-gray-200 py-4 pl-4 font-bold">Chat</div>
<div class="chat-messages flex-1 overflow-y-scroll">
<ul class="space-y-4">
<li>
<div class="flex space-x-2 pl-2 pt-2">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-indigo-400 flex items-center justify-center font-bold text-white">L</div>
</div>
<div class="flex flex-col">
<div class="flex items-baseline space-x-2">
<div class="font-bold">Lorenzon</div>
<div class="text-sm text-gray-400">5:20 pm</div>
</div>
<div class="text-sm text-gray-500">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet est ipsa cupiditate dolorem! Libero aliquam aut soluta incidunt aspernatur error voluptatibus provident tenetur eaque, voluptatum, quam dolor nemo suscipit qui!</div>
</div>
</div>
</li>
</ul>
</div>
<div class="input-container p-4">
<div class="input-area flex h-28 w-full flex-col space-y-2 rounded-2xl border-2 border-gray-200 p-3">
<textarea class="w-full flex-1 outline-none"></textarea>
<div class="buttons flex flex-row items-center space-x-2 align-baseline">
<div class="flex-1">
</div>
<div>
<button class="float-right rounded-xl bg-indigo-500 p-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 text-indigo-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/main.js"></script>
</body>
</html><!DOCTYPE html>
<html lang="en" data-theme="business">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div class="grid h-screen">
<div class="flex max-h-screen flex-col">
<div class="header text-md border-b border-gray-200 py-4 pl-4 font-bold">Chat</div>
<div class="chat-messages flex-1 overflow-y-scroll">
<ul class="space-y-4">
<li>
<div class="flex space-x-2 pl-2 pt-2">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-indigo-400 flex items-center justify-center font-bold text-white">L</div>
</div>
<div class="flex flex-col">
<div class="flex items-baseline space-x-2">
<div class="font-bold">Lorenzon</div>
<div class="text-sm text-gray-400">5:20 pm</div>
</div>
<div class="text-sm text-gray-500">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet est ipsa cupiditate dolorem! Libero aliquam aut soluta incidunt aspernatur error voluptatibus provident tenetur eaque, voluptatum, quam dolor nemo suscipit qui!</div>
</div>
</div>
</li>
</ul>
</div>
<div class="input-container p-4">
<div class="input-area flex h-28 w-full flex-col space-y-2 rounded-2xl border-2 border-gray-200 p-3">
<textarea class="w-full flex-1 outline-none"></textarea>
<div class="buttons flex flex-row items-center space-x-2 align-baseline">
<div class="flex-1">
</div>
<div>
<button class="float-right rounded-xl bg-indigo-500 p-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 text-indigo-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/main.js"></script>
</body>
</html>
And the Chat UI will look like this:
Wiring up the UI
Adding IDs to connect to the UI elements
We will update the HTML and id
chat-messages
to theul
tag that store the messages.Add id
message
to thetextarea
element that will contain the typed messageAdd id
send-message
to the send buttonAnd we will remove the placeholder message from the html, and our
index.html
file will look like this:```xml <!DOCTYPE html>
ChatChat
Join Chat
Join Chat
Join Chat
Join Chat
Join Chat
Join Chat
```