Node Multi Client Chat
Create a multi client command line chat using Node. Specifically, we will be using the Node net module.
Table of Contents 📖
- Node Net Module
- Transmission Control Protocol (TCP)
- Building the Server
- Building the Server - What is a Socket?
- Building the Server - Creating a TCP Server
- Building the Server - EventEmitter
- createServer() Listens for a "connection" Event
- Building the Server - Adding New Users to Group Chat
- Building the Server - Listening for Messages
- Building the Server - Broadcasting the Message to the Group Chat
- Building the Server - Listening for Errors
- Building the Server - Listening for a Close Event
- Building the Server - Binding to a Port
- What is a Port Number?
- Building the Server - Creating the Broadcast Function
- Building the Server - Removing a User from Group Chat
- What is indexOf()?
- What is splice()?
- Building the Server - Broadcasting a Message
- What is forEach()?
- What is write()?
- Building the Server - Don't Send Message to Sender
- Building the Client
- Building the Client - Readline Module
- Building the Client - Enabling Reading from Command Line
- Building the Client - Enabling Writing to Command Line
- Building the Client - Giving a User a Username
- What is a Promise Object?
- Prompting User for Input
- Working with a Resolved Promise
- Building the Client - Connecting to the Server
- Building the Client - What Port to Connect to?
- Building the Client - Telling other Users We Joined the Chat
- Building the Client - Sending Messages to other Users
- Building the Client - Does the User Want to Leave?
- Why use a Timeout?
- Building the Client - Sending Messages
- Building the Client - Receiving Messages
- Building the Client - Leaving the Chat
- Building the Client - Ending the Connection
- socket.end() Emitted Event
- Building the Client - Handling Errors
- Summary
Node Net Module
The backbone of this application will be the Node net module. The Node net module is an asynchronous network API for creating TCP servers and clients.
Transmission Control Protocol (TCP)
TCP stands for transmission control protocol and is a protocol designed to send packets/messages between devices on a network. TCP is the protocol most often used in message sending applications because it ensures that packets/messages are delivered successfully and in order, unlike UDP.
Building the Server
This application will include two files, server.js and client.js. The server.js file will be in charge of broadcasting messages to the clients. Let's start with our server.js file. To get started, lets import the net module.
const net = require('net');
Next, we will create an array called sockets to hold our sockets.
let sockets = [];
Building the Server - What is a Socket?
A socket is an endpoint in a connection between two programs. Essentially, each socket represents a client's connection with the server. Node has a class, net.Socket, that is an abstraction of a TCP socket. An object of type net.Socket is created by a client to interact with a TCP server.
Building the Server - Creating a TCP Server
Before we have our clients connect with our server, we need to create our server.
const server = net.createServer();
The method net.createServer() returns an object of type net.Server, which is a class used to create a TCP server.
Building the Server - EventEmitter
An object of the class net.Server is also an EventEmitter. EventEmitter is a module that facilitates communication between Node objects. It is these events that will drive this application.
createServer() Listens for a "connection" Event
The method createServer() automatically listens for a connection event. In other words, it listens for incoming connections, and when it receives one, it creates a net.Socket object to communicate with whoever connected.
const server = net.createServer(socket => {});
The net.Socket object passed to this method is the one that will be used to communicate with whoever connected.
Building the Server - Adding New Users to Group Chat
As this is a group chat, we need to push this net.Socket object to our array of sockets.
sockets.push(socket);
console.log('Client connected.');
This is because when a client sends a message, we will loop through this socket array and send the message down each connection or socket. In other words, this sockets array will be responsible for sending a message to everyone in the group chat.
Building the Server - Listening for Messages
Now lets listen for incoming messages from other clients. To do this, we create a listener for an event of the type data.
socket.on('data', data => {});
Remember, an object of the class net.Socket is also an EventEmitter. This means we can listen for events, which is done with the on() method. Specifically, the on() method is used to register listeners, and here, we are listening for data from the client. More specifically, we are listening for messages sent from the client.
Building the Server - Broadcasting the Message to the Group Chat
After we have done that, we need to broadcast the message to everyone in the group chat, in other words, all the sockets in our sockets array.
socket.on('data', data => {
broadcast(data, socket);
});
The method broadcast is one that we will make ourselves. It accepts the message to send to everyone in the group chat, and also the socket connection that the message came from. This is so clients won't receives messages they sent themselves.
Building the Server - Listening for Errors
Now, lets listen for an error event. This event will be emitted from the client if the user presses ctrl+c in their terminal to end the program. To stop a large error message from being logged to the console of the server, we just log that a client has disconnected.
socket.on('error', err => {
console.log('A client has disconnected.');
});
Building the Server - Listening for a Close Event
We also need to listen for a close event. A close event will be emitted when a client types "quit" into the console, removing themselves from the chat.
socket.on('close', () => {
console.log('A client has left the chat.');
});
Building the Server - Binding to a Port
We have now set up our server and registered events we want it to listen for. However, to actually establish connections with clients, we need to listen on a specified port number.
server.listen(1234);
The method listen() causes the TCP server to listen for connections on the provided port number.
What is a Port Number?
A port number is a 16 bit unsigned number used to identify a process. Here, the process is our messenger application. To boil this down more, most computers run many processes. IP addresses are used to say which computer to talk to, while a port is used to say which process on the computer the data should go towards. So our clients that we create will be establishing connections with the server on port 1234.
Building the Server - Creating the Broadcast Function
Now, lets create our broadcast function that we mentioned briefly.
function broadcast(message, socketSent) {
}
This method takes the message to be sent to everyone in the group chat and also the socket the message came from so that they don't receive the message they sent.
Building the Server - Removing a User from Group Chat
The first thing we will do is check if the message sent is equal to "quit"
function broadcast(message, socketSent) {
if (message === 'quit') {
}
}
If the message is equal to "quit", then we want to remove the user from the group chat. In other words, we remove them from the socket array. To do this, we will first find the index of this socket in our sockets array using the indexOf() method, and then use splice() to remove it from the array.
function broadcast(message, socketSent) {
if (message === 'quit') {
const index = sockets.indexOf(socketSent);
sockets.splice(index, 1);
}
}
What is indexOf()?
The method indexOf() is part of the JavaScript Array prototype. It returns the first index of the provided argument. Here, the argument is the socket of the client who sent "quit". In other words, the client who wants to leave the chat.
What is splice()?
The method splice() is also part of the JavaScript Array prototype and it changes the contents of an array by removing or replacing existing elements and/or adding new elements in their place.
Splice() has a few function definitions, but one is splice(start, deleteCount) which is what we will be using. Start is where in the array we want to start removing elements, while deleteCount is how many elements to delete. So we want to start deleting at the socket index of the client who left the chat and only remove them, a delete count of 1.
Building the Server - Broadcasting a Message
If the message was not "quit", then we want to broadcast the message to every connected client except the one who sent the message. So if the message is not quit, we will loop through our sockets array, and for each socket we will write the message down the connection.
function broadcast(message, socketSent) {
if (message === 'quit') {
const index = sockets.indexOf(socketSent);
sockets.splice(index, 1);
} else {
sockets.forEach(socket => {
socket.write(message);
});
}
}
What is forEach()?
The forEach() method is part of the JavaScript Array prototype and it executes a provided function one time on each array element. So our array element here is our socket and what we want to do each time is send the message down the socket connection.
What is write()?
The method write() is a net.Socket object method and it sends data on the socket. We can pass a second parameter specifying the encoding but if we don't it defaults to UTF8 encoding which is fine for our program.
Building the Server - Don't Send Message to Sender
However, we don't want to send this message to the client who sent the message, so we need to check if the socketSent provided to our broadcast method is equal to the current socket being iterated on in our array. If it is not the client who sent the message, then we send the message down the socket connection.
function broadcast(message, socketSent) {
if (message === 'quit') {
const index = sockets.indexOf(socketSent);
sockets.splice(index, 1);
} else {
sockets.forEach(socket => {
if (socket !== socketSent) socket.write(message);
});
}
}
Building the Client
Lets now work on our client.js file. So create a file called client.js and once again require the net module.
const net = require('net');
Building the Client - Readline Module
This application is a command line messenger, meaning the messages that we want to send are typed into the command line. So we need a way to read and write user input in the command line. In Node, this is done with the module readline.
const readLine = require('readline');
Building the Client - Enabling Reading from Command Line
We can read data from the command line by providing the method createInterface() an object with the key input and the value process.stdin.
const readLine = require('readline').createInterface({
input: process.stdin
});
The value process.stdin returns a stream connected to standard input, which in our case is the command line.
Building the Client - Enabling Writing to Command Line
We also need to pass the key output and the value process.stdout to createInterface() because we also want to write our messages to the command line.
const readLine = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
The key output specifies a writable stream to write data to and the value process.stdout is used internally by console.log(). So all this code is saying we want to read data from the command line and output data to the command line.
Building the Client - Giving a User a Username
Each user in this application needs a username to enter the chat. So we need a way to make the program wait for the user to enter a username, and once they do we connect them to the server. In other words, a user is not allowed to connect to the server until they enter a username. We can do this with Promises.
const waitForUsername = new Promise(resolve => {
});
What is a Promise Object?
A Promise object represents an eventual success or failure of an asynchronous operation. In our case, the promise will be successful when the user provides a username.
Prompting User for Input
The method readline.question() allows us to create a prompt in the command line and wait for input. After the user types something in and presses enter, we will resolve the username they provided.
readLine.question('Enter a username to join the chat: ', answer => {
resolve(answer);
});
In other words, we will mark the promise as successful and then allow the program to continue. So after the user has provided a username, we will form a connection with the server.
Working with a Resolved Promise
When a promise is successful, or has been resolved, we call the method then() on it. The method then() will accept whaterver was passed to our resolve() function, which here is the username.
waitForUsername.then(username => {
});
Building the Client - Connecting to the Server
To form a connection with the server we use the method net.connect(). The method net.connect() creates a new net.Socket object and immediately executes the function socket.connect(), which will connect us to our server.
const socket = net.connect({
});
Building the Client - What Port to Connect to?
It is important to remember that the server is listening on the port 1234, so we want to pass the key port and the value 1234 to the net.connect() method.
const socket = net.connect({
port: 1234
});
Building the Client - Telling other Users We Joined the Chat
We also want to send over the username that the user entered to notify everyone in the group chat that a new user has joined. Remember, a net.Socket object is also an EventEmitter so we can listen for registered events with the on() method. One of these registered events is "connect".
socket.on('connect', () => {
});
The function we provide to on() will be executed when the client connects to the server. So here, when we connect to the server we want to write over the user's username and that they have joined the chat with. This will be broadcasted to everyone in the chat.
socket.on('connect', () => {
socket.write(username + ' has joined the chat.');
});
Building the Client - Sending Messages to other Users
Now, lets work on sending over messages the user sent, starting with if the client has typed the word "quit" into the console. Lucky for us, the interface we created with readline is also an EventEmitter, so we can listen for registered events on it aswell. One of these events is "line".
readLine.on('line');
This event is triggered whenever the user presses the enter key when typing into the command line. We then execute the callback function passing it data, which is what the user typed in.
readLine.on('line', data => {});
Building the Client - Does the User Want to Leave?
We first want to check if what the user typed in was equal to "quit". If it is, this means that our user wants to leave the chat room, so we first write to the server that we have left the chat, and then we set a timeout of a second.
readLine.on('line', data => {
if (data === 'quit') {
socket.write(`${username} has left the chat.`);
socket.setTimeout(1000);
}
});
This is because when the user leaves the chat we also want to send over the word quit to the server so they can remove us from the chat room.
Why use a Timeout?
If we don't set a timeout, then the data "username has left the chat" will get merged with the word "quit" and the server will never receive just the word "quit". This will make more sense soon but essentially if we call the write method in succession the data sent over could possibly be merged.
Building the Client - Sending Messages
If the user did not type in the word "quit", then we want to write over the username of the user who sent the message and also the message they want to send.
readLine.on('line', data => {
if (data === 'quit') {
socket.write(`${username} has left the chat.`);
socket.setTimeout(1000);
} else {
socket.write(username + ': ' + data);
}
});
This is done by creating a string that concatenates the username of the user with the data.
Building the Client - Receiving Messages
To work with messages received from other users, we listen for the registered "data" event and then console.log() out the received message.
socket.on('data', data => {
console.log('\x1b[33m%s\x1b[0m', data);
});
However, we not only console.log() out the data, but we also provide it a string that will change the color of it. This will help us distinguish between messages we sent from those that we receive.
Building the Client - Leaving the Chat
Now, lets respond to that timeout that we emit when the user wants to leave the chat. What we will do is write the word quit to the server.
socket.on('timeout', () => {
socket.write('quit');
});
Remember, we are calling this timeout because we want to prevent the merging of the message "quit" from the message "username has left the chat." When the server receives the word "quit", it will remove us from the group chat.
Building the Client - Ending the Connection
We then want to call the socket.end() method which tells the server that we want to close down our connection.
socket.on('timeout', () => {
socket.write('quit');
socket.end();
});
Specifically, socket.end() will send a FIN packet to the server. FIN packets are sent to close a connection. The server will then respond with a FIN packet to the client to accept the socket termination. We also want to respond to the event emitted when socket.end() is called.
TCP has two supported connection termination types: graceful connection termination and abrupt connection termination. Above outlines graceful connection termination.
socket.end() Emitted Event
The method socket.end() emits an "end" event. So when the socket connection has ended, we want to terminate this application. In Node, process is a core module that provides methods to programatically exit from a Node program. This is what process.exit() does.
socket.on('end', () => {
process.exit();
});
Building the Client - Handling Errors
Finally, we just want to respond to an error with the server. Specifically, if we press ctrl+c and closed down the server program, we want a friendly message to be printed out to the client.
socket.on('error', () => {
console.log('The server seems to have been shut down...');
});
This is done by responding to the registered "error" event.
Summary
But so this was how to create a command line multi-client chat using Node and the net module. If you enjoyed this article please consider donating so I can pay for the hosting fees and also checkout my Youtube channel WittCode!