Node.js: real-time client-server communication: the use case of shell commands

Node.js: real-time client-server communication: the use case of shell commands

In this tutorial we will see how to implement real-time communication between server and client in Node.js.

In this tutorial we will see how to implement real-time communication between server and client in Node.js.

On the server, we will send the output of the ping command to the client via WebSocket as the results are returned. To do this we will have to use the spawn() method of the core module child_process because this method has events that we can handle with callback functions.

Let's create a class to handle the ping command.

'use strict';

const { spawn } = require('child_process');
const validator = require('validator');

class Ping {
    constructor(host, times = 1) {
        this.host = host;
        this.times = times;
    }

    validate() {
        if(!validator.isFQDN(this.host) && !validator.isIP(this.host)) {
            return false;
        }
        const times = this.times.toString();

        if(!validator.isInt(times)) {
            return false;
        }

        return true;
    }

    send({ ondata = function () {}, onerror = function () {}, onclose = function () {} }) {
        if(!this.validate()) {
            throw new Error('Invalid parameters.');
        }
        const cmd = spawn('ping', ['-c', this.times, this.host]);

        cmd.stdout.on('data', data => {
            ondata(data.toString());
        });

        cmd.stderr.on('data', data => {
            onerror(data.toString());
        });

        cmd.on('close', code => {
            onclose(code);
        });
    }
}

module.exports = Ping;

The parameters passed to the class are the host (domain or IP address) and the number of requests to send. These parameters are validated and if they pass validation they are used to invoke the ping command from the shell.

To send the data in real time to the client we need the NPM ws module which implements the server-side WebSocket. This module can be used with any instance of a server object of type http or https, that is, instantiated from one of these core modules of Node.js.

const express = require('express');
const ws = require('ws');
const Ping = require('./lib/Ping');
const PORT = process.env.PORT || 3000;
const app = express();

const wsServer = new ws.Server({ noServer: true });

//...

const server = app.listen(PORT);

server.on('upgrade', (request, socket, head) => {
    wsServer.handleUpgrade(request, socket, head, socket => {
        wsServer.emit('connection', socket, request);
    });
});

In this case, as we will be using the server object created by ExpressJS, we can instantiate our WebSocket server with the noServer parameter set to true.

The client will pass a JSON string to the server with the name of the command to execute and the host name. As an example we will omit the parameter with which the client could specify the number of pings in order to focus solely on what we will send to the client via our server's send() method.


//...

wsServer.on('connection', socket => {
    socket.on('message', message => {
        const messageData = JSON.parse(message.toString());

        const { cmd, param } = messageData;
        if(cmd === 'ping') {

            const pingRequest = new Ping(param, 3);
            const callbacks = {
                ondata(data) {
                    socket.send(data);
                },
                onerror(msg) {
                    socket.send(`Error: ${msg}`);
                },
                onclose(code) {
                    socket.send(`Exit code: ${code}`);
                }
            };

            try {
                pingRequest.send(callbacks);
            } catch(err) {
                socket.send(`Error: ${err.message}`);
            }
        }
    });
});

When the server receives the JSON string from the client, it transforms it into a JSON object and uses the host property to instantiate the Ping class which will launch the command from the shell. Now we can define and use the three callbacks that will handle the events and send the shell output to the client as it comes. Such output is generated using the WebSocket server's send() method handled by the functions of the callbacks object.

On the client side we have to validate the user input and if successful, send the data using the send() method of the WebSocket object.

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

Then we listen to the messages sent by the server, we transform them into strings and we use them to show the lines of text that are produced as we get the messages.

const response = document.getElementById('ping-response');
        const socket = new WebSocket(`ws://${location.host}`);

        socket.addEventListener('message', event => {
            let line = document.createElement('div');
            line.innerText = event.data.toString();
            response.appendChild(line);
        });

We always validate the data also on the client side and if the validation is successful we send this data to the server.

const value = host.value;

            if(!validator.isFQDN(value) && !validator.isIP(value)) {
                host.classList.add('is-invalid');
                return false;
            }

            const data = {
              cmd: 'ping',
              param: value
            };

            socket.send(JSON.stringify(data));

This communication technique is not only useful in the case of chats, but also when we need to know the progress of an operation on the server in real time.