Building a Command-Line Chat App with Node.js and Socket.IO — Part 1

Teedus Chung
9 min readJun 7, 2021

In this project, we’re going to use the Node.js platform to build a real-time chat application that allows users to exchange messages. To achieve this aim, we’re going to use the WebSocket protocol.

Node.js

Node.js is an open-source, cross-platform, JavaScript runtime environment. It executes JavaScript code outside of a browser.

The most important advantage of using Node.js is that we can use JavaScript as both front-end and back-end language.

WebSocket

HTTP is the underlying communication protocol of the World Wide Web in the client-server architecture where the client requests the resource and the server responds with the requested data. HTTP is a strictly unidirectional protocol - any data sent from the server to the client must be first requested by the client.

WebSocket, on the other hand, allow us for sending message-based data, similar to UDP, but with the reliability of TCP. Websocket uses HTTP as the initial transport mechanism but keeps the TCP connection alive after the HTTP response is received so that it can be used for sending messages between client and server.

Socket.IO

Socket.IO is a JavaScript library built on top of WebSocket. Although Socket.IO indeed uses WebSocket as transport when possible, it adds additional metadata to each packet. That is why a WebSocket client will not be able to successfully connect to a Socket.IO server, and vice versa.

The main advantages of Socket.IO over WebSocket are:

  • Unlike WebSocket, Socket.IO allows us to broadcast a message to all the connected clients.
  • Proxies and load balances make WebSocket hard to implement and scale. Socket.IO supports these technologies out of the box.
  • Socket.IO can fall back to other technologies other than WebSocket when the client doesn’t support it.
  • If a WebSocket connection drops, it will not automatically reconnect but Socket.IO handles that for us.
  • Socket.IO APIs are built to be easier to work with.

Set up the environment

At the time I’m writting, the latest LTS version of Node.js recommended is 14.17.0

  • Online Markdown Editor
  • If you’re using Windows but you want to enjoy the Linux shell, one simple way is that you install Git then use Git Bash

Initialize the project

  • Create your project directory named siochat where you should have at least 2 directories, one for the client-side and another for the server-side of the application as the following image:
  • Navigate to the siochat directory then create the package.json file in the terminal as follows:
% npm init --yes

We can learn more about npm and package.json at here

  • Install the client-side of the Socket.IO library as follows:
% npm install socket.io-client
  • Install the server-side of the Socket.IO library as follows:
% npm install socket.io

At the time I’m writting, the latest version of Socket.IO is 4.1.2

Run your chat server

In the server directory, we create a new file named server.js and write some code as follows:

/*** server.js ***/const port = 3000;
const io = require("socket.io")(port);
console.log("Server is listening on port: %d", port);

Then we’ll execute the server.js in the terminal as follows:

% node server.js

Hint: Press Ctrl + C to terminate the running process of your server.

Implement the ‘connect’ function

Server

Now we add the below code to the server.js file:

/*** server.js ***///...io.of("/").on("connect", (socket) => {
console.log("\nA client connected");

socket.on("disconnect", (reason) => {
console.log("\nA client disconnected, reason: %s", reason);
console.log("Number of clients: %d", io.of('/').server.engine.clientsCount);
});
});

Socket.IO is based on events, so we set up events with its handler, and when the event is emitted the corresponding handler is run. To set up events, the .on() method is used, it takes two parameters, the first is a string that holds the name of the event and the second parameter is a function callback.

The connect event is emitted when a connection is made. The socket argument will be used in further communication with the client.

The disconnect event is emitted after a client disconnected from the server.

Client

In the client directory, we create a new file named client.js and write some code as follows:

/*** client.js ***/const io = require("socket.io-client");
const socket = io("http://localhost:3000");
var nickname = null;console.log("Connecting to the server...");socket.on("connect", () => {
nickname = process.argv[2];
console.log("[INFO]: Welcome %s", nickname);
});
socket.on("disconnect", (reason) => {
console.log("[INFO]: Client disconnected, reason: %s", reason);
});

Remember that we created a server at port 3000 so that’s why we try to connect to port 3000. This gets us a channel held by the socket variable to communicate with the server.

We set up 2 event listeners as follows:

  • connect: This event is fired after the client established a successful initial connection to the server. Here we make the handler display [INFO]: Welcome <nickname>. All command-line arguments received by the shell are given to the process in an array called argv (short for ‘argument values’). Node.js exposes this array for every running process in the form of process.argv
  • disconnect: This event is fired after we disconnected from the server.

Run it together

We’ll execute the server.js in one terminal and the client.js in another terminal as follows:

Implement the ‘broadcast’ function

Let’s say we want to send a message to other connected clients. Take a look at the following specification:

  • Command: This is what we type in the client terminal such as b;hello to send the event.
  • Event: This is the name of the event sent to the server.
  • Json format: This field contains the payload that will be sent with the broadcast event. While data can be sent in a number of forms, JSON is the simplest. It is “self-describing” and easy to understand.

Client

The Readline module in Node.js allows us to read the input stream line by line. This module basically wraps up the process standard output and process standard input objects. We’ll use this module to make it easier to input and read the output given by the clients:

/*** client.js ***///...const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on("line", (input) => {

});

The line event is emitted whenever the input stream receives an end-of-line input (\n, \r, or \r\n). This usually occurs when the user presses the Enter or Return keys.

/*** client.js ***///...rl.on("line", (input) => {

if (true === input.startsWith("b;")) {
var str = input.slice(2);
socket.emit("broadcast", {"sender": nickname, "action": "broadcast", "msg": str});
}
});

The input argument holds the string containing a single line of the received input. We call socket.emit() method to send the broadcast event with its payload in JSON format to the server.

Now we register an event listener for the broadcast event. This listens to the event emitted from the server and displays messages of other clients in our client:

/*** client.js ***///...socket.on("broadcast", (data) => {
console.log("%s", data.msg);
});

Server

Let’s use the socket argument to register a listener for upcoming broadcast events:

/*** server.js ***///...io.of("/").on("connect", (socket) => {    //...    socket.on("broadcast", (data) => {
});
});

The data argument holds the data payload emitted alongside the broadcast event. After receiving, the server needs to send the data to all registered clients connected to it:

/*** server.js ***///...io.of("/").on("connect", (socket) => {    //...    socket.on("broadcast", (data) => {
console.log("\n%s", data);
socket.broadcast.emit("broadcast", data);
});
});

Note: Broadcasting means sending to all clients except the sender.

Running it

We’ll execute the server.js in one terminal and the client.js in two terminals as follows:

Let’s type b;Hello in the ted’s terminal and we’ll see Hello appears in the roni’s terminal, and vice versa as follows:

Implement the notification function

Clients must be automatically notified when a new client connected. Let’s see how we do it:

Note: The Command field is empty.

Client

If a client connects successfully to the server, the client will emit the join event:

/*** client.js ***///...socket.on("connect", () => {    //...

socket.emit("join", {"sender": nickname, "action": "join"});
});

We also register an event listener for the join event. This listens to the notification emitted from the server and displays it in our client:

/*** client.js ***///...socket.on("join", (data) => {
console.log("[INFO]: %s has joined the chat", data.sender);
});

Server

We add an event listener for the join event in which we save the nickname of the client to the socket object and notify other clients by emitting the join event with its data:

/*** server.js ***///...io.of('/').on("connect", (socket) => {

//...
socket.on("join", (data) => {
console.log("\n%s", data);
console.log("Nickname: ", data.sender, ", ID: ", socket.id);
console.log("Number of clients: %d", io.of('/').server.engine.clientsCount);
socket.nickname = data.sender;
socket.broadcast.emit("join", data);
});
});

Let’s run

We’ll execute the server.js in one terminal and the client.js in two terminals: ted then roni. We’ll see ted is notified of roni’s appearance:

Implement the ‘list’ function

In order to know all clients that connected to the server, we’ll do as follows:

Client

The client will ask by emitting the list event to the server:

/*** client.js ***///...rl.on("line", (input) => {    //...    else if ("ls;" === input) {
socket.emit("list", {"sender": nickname, "action": "list"});
}
});

Now we listen to the list event emitted from the server, then print out the list of the clients in our client:

/*** client.js ***///...socket.on("list", (data) => {
console.log("[INFO]: List of nicknames:");
for (var i = 0; i < data.users.length; i++) {
console.log(data.users[i]);
}
});

Server

After receiving the list event from a client, we loop through all sockets to find all connected client and send back the list event alongside the list of clients:

/*** server.js ***///...io.of("/").on("connect", (socket) => {

//...
socket.on("list", (data) => {
console.log("\n%s", data);
var users = []; for (const [key, value] of io.of("/").sockets) {
users.push(value.nickname);
}
socket.emit("list", {"sender": data.sender, "action": "list", "users": users});
});
});

Let’s run

Like the previous one with 1 server’s terminal and 2 client’s terminal, if we type ls; in the ted’s terminal, we’ll see something like the following image:

Implement the ‘quit’ function

Take a look at the below specification:

Client

Similar to the previous event, the client will quit the chat by emitting the quit event. On the other hand, the client needs to register an event listener for the quit event to receive notification from the server in case other clients quit the chat:

/*** client.js ***///...rl.on("line", (input) => {    //...    else if ("q;" === input) {
socket.emit("quit", {"sender": nickname, "action": "quit"});
}
});socket.on("quit", (data) => {
console.log("[INFO]: %s quit the chat", data.sender);
});

Server

/*** server.js ***///...io.of("/").on("connect", (socket) => {

//...
socket.on("quit", (data) => {
console.log("\n%s", data);
socket.broadcast.emit("quit", data);
socket.disconnect(true);
});
});

Conclusion

We’ll see it’s very simple with the help of the Socket.IO library, right? We now have implemented some basic functions of the chat app running on command-line. There are more functions that are waiting for us, so let’s take a break before moving on to part 2 of the project.

If you have any questions in regard to the project, please feel free to leave any comments.

Don’t forget to give me applause. Thanks for your reading.

--

--