Demystifying complex messaging applications using WebSocket

Demystifying complex messaging applications using WebSocket!

Have you ever tried to create chat applications? Whenever you want bidirectional communication(server to client and client to server), WebSocket is the new-age solution. But setting up WebSocket in an efficient way is still a key. You may find various articles over this which makes set-up easy, but I think it is inefficient and also not an optimized solution. So we should rethink this, play around with it, and then decide. I have proposed a solution, your opinion may differ please let me know your dimensions as well. I hope you already have a basic idea about WebSocket, so let’s hit the road.

Introduction:

HTTP is unidirectional, i.e the client sends a request and the server will reply with a response. But what if the client and the server want to send a message or some information to each other without request-response? That’s where WebSocket comes in. It allows us to bidirectional communication, with the underline protocol of STOMP similar to HTTP over TCP. It connects the client with HTTP and then upgrades the protocol to WebSocket. Some of the basic use cases of WebSockets are messaging apps, chat rooms, and sending notifications.

Choosing npm library:

As we all know npm is growing pretty fast, and we do have a bunch of stuff over the internet as well. It’s easy to find the content but hard to choose and proceed. In the process, we might place the wrong foot, create an unnecessary mess. Working with WebSockets there are two very great libraries are out there, 1. socket.io, 2. ws.

Although socket.io is rich and has some extra features, I would prefer to use ws instead. The major reasons behind this are:
– The size of socket.io is over 1MB, comparatively, ws has around 125kb which is much lightweight.
– We can achieve the same features with few lines of code and scale system as per the requirements.

Securing sockets:

Securing websites and applications is a necessity, to accomplish that we can use some in-built features of WS.

1. We can specify the origin and verify the client on every client connection.

const wss = new WebSocket.Server({
     server: httpServer, // express http server
     origin: "wss://abcd.com",
     verifyClient: (info, callback) => {
         return (info.origin === "wss://abcd.com" &&  info.secure === true)
             ? callback(true)
             : callback(false);
     }
});

2. We can specify the appended URL parameter over the origin and then manually connect and segregate the clients based on functional requirements.

wss.on('connection', async function connection(wsInstance, req) {
     if (req.url === '/message') {
         // store socket for future use
     } else {
         // destroy socket
         req.socket.destroy();
     };
});

You can destroy the connection if the above requirements are not met when a new client is connected and if all is good then can store the socket for future use.

Storing sockets:

To send a message to the client multiple times, we need to store the socket and access the class method send(). As it’s a class object, we cannot store it in any persistent data structure. And because of STOMP heartbeat is of 25 seconds, we need to handle the frequent disconnections.

Although in many articles Array is used to store the sockets. To broadcasting the message, the forEach() method is used. But I do prefer to store the socket class object in the Map(a key-value pair) and sending a message to the single client by its key and broadcasting it via the in-built wss.clients.forEach() array.

let webSocketClients: new Map();

// store socket for future use
webSocketClients.set(customID, wsInstance);

It has some major advantages:
– As we have stored socket class object in Map, it’s partially persisted, and it’s much easier to handle than the Array and finding the Array Indexes.
– We can reuse the socket class object and update the connection according to the key.
– With a particular key we can easily identify the sender client and work around logic accordingly.

Communication:

Now we have stored sockets, we can send messages to particular clients via Map as well as broadcast them using in-built clients Array.

Send a message to a particular client:

// get particular client websocket
const clientSocket = await webSocketClients.get(customID);

// send message to client
if (clientSocket && clientSocket.readyState === WebSocket.OPEN) {  
    clientSocket.send(JSON.stringify(message));
};

Broadcast message to all the clients:

wss.clients.forEach(clientSocket => {
    // send message to all clients
    if (clientSocket && clientSocket.readyState === WebSocket.OPEN) {
         clientSocket.send(JSON.stringify(message));
     };
});

Receiving messages is pretty much simpler, on the triggered events we can handle the incoming message, and apply the functional logic.

wsInstance.on('message', async function incoming(message) {
     // parse to original data
     const receivedData = JSON.parse(message);
     // handle message logic
});

Considering the architectural view and messaging frequency, it’s good to use the message brokers like RabbitMQ or Kafka, for both incoming as well as outgoing messages. You might think of Redis as well. To reduce down the response time you can decouple the server logic.

With selecting a library, securing and storing connections, and now sending and receiving messages, I think now we are ready to build a complex messaging application using WebSockets. Let’s get quickly to the conclusion.

Conclusions:

1. For bidirectional communications, WebSocket is a tried and tested solution.
2. Choosing a lightweight library, securing the client connections, and storing sockets for future use are the keys to the complex messaging applications using WebSocket.
3. Considering the performance use message brokers to decouple the system.

Code File:

const { v4: uuidv4 } = require('uuid'); // uuid v4 generator
const express = require("express");     // express server module
const app = express();                  // express app instance

// constants
let webSocketClients = new Map(); 
let wss;

//
// websocket server
//
const httpServer = require('http').createServer(app);

// server initiazaton function
const initServer = (httpServer) => {
     //
     // create server
     //
     wss = new WebSocket.Server({
         server: httpServer,
         origin: "wss://abcd.com",
         verifyClient: (info, callback) => {
             return (info.origin === "wss://abcd.com" && info.secure === true)
                 ? callback(true)
                 : callback(false);
         }
     });

     //
     // new connection
     //
     wss.on('connection', async function connection(wsInstance, req) {
         try {
             let socketID = uuidv4();
             if (req.url === '/messaging') {
                 // store socket
                 webSocketClients.set(socketID, wsInstance);
             } else {
                 // destroy socket
                 req.socket.destroy();
             };

             //
             // received message
             //
             wsInstance.on('message', async function incoming(message) {
                 // parse to original data
                 const receivedData = JSON.parse(message);
                 //
                 // handle message logic + store to database
                 //
             });
          } catch (err) {
             console.log(err);
          };
     });
 };

//
// message send
//
// message to particular client function
const sendMessageToSingleClient = async (socketID, message) => {
     // get particular client websocket
     const clientSocket = await webSocketClients.get(socketID);
     // send message to client
     if (clientSocket && clientSocket.readyState === WebSocket.OPEN) {
         clientSocket.send(JSON.stringify(message));
     };
 };

// brodcast message to all clients function
const broadcastMessageToAllClients = async (message) => {
     wss.clients.forEach(clientSocket => {
         if (clientSocket && clientSocket.readyState === WebSocket.OPEN) {
             clientSocket.send(JSON.stringify(message));
         };
     });
};

//
// server start
//
// websocket server
initServer(httpServer);

// http api server
httpServer.listen(PORT, () => console.log(`Messaging server started!`));

Note:

If you want to get an in-depth idea about the WebSockets, I will suggest a book by Andrew Lombardi – WebSocket: lightweight client-server communications.

You Might Also Like
Leave a Reply

%d bloggers like this: