Welcome!
In this tutorial, we will make our own simple chat application with command line interface (CLI) using Node JS, with the three main features:
- Register User Info: users can register themselves in the server.
- Obtain User Info: users can download other users information.
- Send Chat Message: after registration, users can send chat messages to each other via broadcasting approach.
TLDR: The full code can be seen in Github
Setup
To begin this tutorial, you will need to install Node JS version 18.x.x. Make a folder called chatty
. Then, open the folder in VSCode and create a file called package.json
with the following value:
{
"dependencies": {
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2"
}
}
This package.json
file contains all of our dependencies for this chat application.
- socket.io is used to make our Web-Socket server
- socket.io-client is used for our Web-Socket client
Run
We can now install our dependencies. Open terminal in VSCode and run npm install
from project root directory.
After install, a folder called node_modules
and a file called package-lock.json
will be created.
1. Connection Between Server And Client
First, we create a file called server.js
and add the following codes
const { Server } = require("socket.io");
const io = new Server();
io.on("connection", (socket) => {
console.log(`user ${socket.id} is connected`);
socket.on("disconnect", () => {
console.log(`user ${socket.id} is disconnected`);
});
});
io.listen(8080);
The io
is socket.io server object that we use to process Web Socket requests. This socket is run in port 8080
, with two events:
- The
connection
is triggered every time the server receives a new connection from client - The
disconnect
is executed whenever we lose connection with existing clients
Second, we create a file called client.js
and add the following codes
const { io } = require("socket.io-client");
const domain = "localhost";
const port = 8080;
const url = "http://" + domain + ":" + port.toString();
const socket = io(url);
socket.on("connect", () => {
console.log("Connected with id:", socket.id);
});
socket.on("disconnect", () => {
console.log("Disconnected!");
process.exit(0);
});
Recall that the server is running on port 8080
. Since we run both server and client in our own machine, we set the domain as localhost
. The client can connect to the server by mentioning the root url
of the server. We create socket
client object using the io(url)
method.
The client also listens to connect
and disconnect
events, which are triggered whenever the client connects/disconnects to the server.
Run
Lets try to run our server and client! From one terminal, run node server
. Then, open another terminal, run node client
.
Those images show that server and client have been connected with socket id of uPx1V2GnN9tbpsHIAAAB
. Each client connection will have unique socket id.
2. Assign Username
The client can choose what username they want to use in our chat application. In this case, we need to get user input from command line.
const readlinePromises = require('node:readline/promises');
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
We will use readline
module to get user input. We choose the promise version so that we can use the async/await function in our code. More information on this module can be found here. The rl
is the readline object that we use to process command line input.
username = await rl.question("What is your username? ");
console.log(`Hello ${username}, welcome to Chatty!`);
The rl.question()
will prompt a question "What is your username?" in the console and save the user answer input in the username
variable.
The whole client.js
looks as follows.
const { io } = require("socket.io-client");
const readlinePromises = require('node:readline/promises');
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
const domain = "localhost";
const port = 8080;
const url = "http://" + domain + ":" + port.toString();
const socket = io(url);
let username;
socket.on("connect", async () => {
console.log("Connected with id:", socket.id);
username = await rl.question("What is your username? ");
console.log(`Hello ${username}, welcome to Chatty!`);
});
socket.on("disconnect", () => {
console.log("Disconnected!");
process.exit(0);
});
Run
Try to run the client one more time (node client
) and type alice
as your username then ENTER, it will show you something like this.
3. Add User Registration
We use Javascript Map object to store list of user along with their information. This map works like a key-value store database, but store only in-memory.
const users = new Map(); // use this to store users info
We can now use users.set()
to save and users.get()
to retrieve data from our store.
First, we add a new channel in our web socket server.js
called register
.
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
socket.broadcast.emit("register", data);
});
We send three data
through this channel.
- The
username
for the client username. - The
socketId
for the unique socket.io id per client. - The
time
is the time client connect to our server.
This data
object will be stored in users
and the username
will be used as key while socketId
and time
act as values.
After that, we broadcast this data to other clients that currently connected to the server on register
channel using socket.broadcast.emit()
.
Second, we add the same channel in client.js
.
socket.on("connect", async () => {
console.log("Connected with id:", socket.id);
//...
const data = {
username: username,
socketId: socket.id,
time: Math.floor(Date.now() / 1000) // epoch time
};
socket.emit("register", data);
});
We construct data
, which includes username
, socketId
, and time
. The time is formatted in the Epoch style. Then, we transmit this data to our server on register
channel using socket.emit()
method. This process is executed right after the client connected to the server.
Our client also listens to register
events.
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
console.log(`New user added ${data.username} with socket id ${data.socketId}`);
});
Similar to our server, we save the data being sent through register
channel in users
. In this case, clients only store data from other clients.
The whole server.js
will look something like this
const { Server } = require("socket.io");
const io = new Server();
const users = new Map();
io.on("connection", (socket) => {
console.log(`user ${socket.id} is connected`);
socket.on("disconnect", () => {
console.log(`user ${socket.id} is disconnected`);
});
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
socket.broadcast.emit("register", data);
});
});
io.listen(8080);
While client.js
looks like this
const { io } = require("socket.io-client");
const readlinePromises = require('node:readline/promises');
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
const domain = "localhost";
const port = 8080;
const url = "http://" + domain + ":" + port.toString();
const socket = io(url);
const users = new Map();
let username;
socket.on("connect", async () => {
console.log("Connected with id:", socket.id);
username = await rl.question("What is your username? ");
console.log(`Hello ${username}, welcome to Chatty!`);
const data = {
username: username,
socketId: socket.id,
time: Math.floor(Date.now() / 1000) // epoch time
};
socket.emit("register", data);
});
socket.on("disconnect", () => {
console.log("Disconnected!");
process.exit(0);
});
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
console.log(`New user added ${data.username} with socket id ${data.socketId}`);
});
Run
Try to run node server
. Then open another terminal and run node client
, use alice
as username.
After that, open another terminal and run node client
, this time, use bob
as username.
Go back to alice
terminal, you will see bob data registration in the last line of console.
Okay, now we know that the user registration is working, and alice
can get bob
data. However, bob
does not know any information about alice
. He does not receive broadcast of register
channel because alice
register first before bob
when bob
still offline.
To solve this issue, we need to create another channel to retrieve user info.
4. Retrieve User Information
First, we create a channel called user-info
in server.js
.
socket.on("user-info", () => {
const data = {
keys: Array.from(users.keys()),
values: Array.from(users.values())
};
socket.emit("user-info", data);
});
The server gets all keys and values from users
key-value store. Then convert them into arrays using Array.from()
method. Those keys and values then will be sent to client through the user-info
channel.
Second, we also create user-info
in client.js
.
socket.on("connect", async () => {
console.log("Connected with id:", socket.id);
socket.emit("user-info");
//...
})
Right after the client connects to the server, the client sends message through user-info
channel to get updated list of users from the server.
socket.on("user-info", (data) => {
for (let i = 0; i < data.keys.length; i++) {
const k = data.keys[i];
const v = data.values[i];
const value = {
socketId: v.socketId,
time: v.time
};
users.set(k, value);
}
});
The client also listens to user-info
events. Upon receiving such events, the client loops through all the keys and values and then store each of them in their own users
data store.
In summary, the user-info
channel allows the client to copy the contents of users
in the server.
To check list of users stored in client's data store, we will create a command line action prompt. For example, when the client type !users
in their console, the system will display list of current users.
To do so, we will use readline
module.
// triggered on end-of-line input (\n, \r, or \r\n)
rl.on("line", (input) => {
if (input == "!users") {
console.log(users);
}
});
// triggered on CTRL+C like command
rl.on('SIGINT', () => {
process.exit(0);
});
The line
action will be triggered every time the user press ENTER. In this case, when user type !users
and press ENTER, the system will dump users
to the console.
The SIGINT
is triggered when the client press CTRL+C from the console. In this case, this action will close the program.
The whole server.js
will looks like this.
const { Server } = require("socket.io");
const io = new Server();
const users = new Map();
io.on("connection", (socket) => {
console.log(`user ${socket.id} is connected`);
socket.on("disconnect", () => {
console.log(`user ${socket.id} is disconnected`);
});
socket.on("user-info", () => {
const data = {
keys: Array.from(users.keys()),
values: Array.from(users.values())
};
socket.emit("user-info", data);
});
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
socket.broadcast.emit("register", data);
});
});
io.listen(8080);
The updated client.js
is as follows.
const { io } = require("socket.io-client");
const readlinePromises = require('node:readline/promises');
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
const domain = "localhost";
const port = 8080;
const url = "http://" + domain + ":" + port.toString();
const socket = io(url);
const users = new Map();
let username;
socket.on("connect", async () => {
console.log("Connected with id:", socket.id);
socket.emit("user-info");
username = await rl.question("What is your username? ");
console.log(`Hello ${username}, welcome to Chatty!`);
const data = {
username: username,
socketId: socket.id,
time: Math.floor(Date.now() / 1000) // epoch time
};
socket.emit("register", data);
});
socket.on("disconnect", () => {
console.log("Disconnected!");
process.exit(0);
});
socket.on("user-info", (data) => {
for (let i = 0; i < data.keys.length; i++) {
const k = data.keys[i];
const v = data.values[i];
const value = {
socketId: v.socketId,
time: v.time
};
users.set(k, value);
}
});
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
});
// triggered on end-of-line input (\n, \r, or \r\n)
rl.on("line", (input) => {
if (input == "!users") {
console.log(users);
}
});
// triggered on CTRL+C like command
rl.on('SIGINT', () => {
process.exit(0);
});
Run
Run the following command sequentially:
- Run the
node server
- Open new terminal and run
node client
. Typealice
as username. - Open another terminal and run
node client
. Typebob
as username. - Then, in both
alice
andbob
terminal, you can type!users
You can see that now, alice
has bob
info, and bob
has alice
info.
5. Send Chat Message
In our final step, we will create a channel called chat
to send and broadcast our chat messages.
Add the following code in server.js
.
socket.on("chat", (data) => {
socket.broadcast.emit("chat", data);
});
Then, add this to the client.js
.
socket.on("chat", (data) => {
console.log(`${data.sender}: ${data.msg}`);
});
// triggered on end-of-line input (\n, \r, or \r\n)
rl.on("line", (input) => {
if (input == "!users") {
console.log(users);
} else {
const data = {
"sender": username,
"msg": input
};
socket.emit("chat", data);
}
});
The client listens to chat
events, and then prints the sender
and msg
to the console.
The client can send message by typing any value and then press ENTER, this will trigger the line
and then send the input
to the server via chat
channel.
The final server.js
code is like this
const { Server } = require("socket.io");
const io = new Server();
const users = new Map();
io.on("connection", (socket) => {
console.log(`user ${socket.id} is connected`);
socket.on("disconnect", () => {
console.log(`user ${socket.id} is disconnected`);
});
socket.on("user-info", () => {
const data = {
keys: Array.from(users.keys()),
values: Array.from(users.values())
};
socket.emit("user-info", data);
});
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
socket.broadcast.emit("register", data);
});
socket.on("chat", (data) => {
socket.broadcast.emit("chat", data);
});
});
io.listen(8080);
While, the final client.js
code is like this.
const { io } = require("socket.io-client");
const readlinePromises = require('node:readline/promises');
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
const domain = "localhost";
const port = 8080;
const url = "http://" + domain + ":" + port.toString();
const socket = io(url);
const users = new Map();
let username;
socket.on("connect", async () => {
console.log("Connected with id:", socket.id);
socket.emit("user-info");
username = await rl.question("What is your username? ");
console.log(`Hello ${username}, welcome to Chatty!`);
const data = {
username: username,
socketId: socket.id,
time: Math.floor(Date.now() / 1000) // epoch time
};
socket.emit("register", data);
});
socket.on("disconnect", () => {
console.log("Disconnected!");
process.exit(0);
});
socket.on("user-info", (data) => {
for (let i = 0; i < data.keys.length; i++) {
const k = data.keys[i];
const v = data.values[i];
const value = {
socketId: v.socketId,
time: v.time
};
users.set(k, value);
}
});
socket.on("register", (data) => {
const value = {
socketId: data.socketId,
time: data.time
};
users.set(data.username, value);
});
socket.on("chat", (data) => {
console.log(`${data.sender}: ${data.msg}`);
});
// triggered on end-of-line input (\n, \r, or \r\n)
rl.on("line", (input) => {
if (input == "!users") {
console.log(users);
} else {
const data = {
"sender": username,
"msg": input
};
socket.emit("chat", data);
}
});
// triggered on CTRL+C like command
rl.on('SIGINT', () => {
process.exit(0);
});
Run
Run the following commands:
- Run the
node server
. - Open new terminal, run
node client
, putalice
as username. - Open another terminal, run
node client
, putbob
as username. - Open yet another terminal, run
node client
, puttrudy
as username. - In
alice
,bob
, andtrudy
terminal, type any messages.
You can see that alice
, bob
, and trudy
can send messages via broadcasting, like in a group chat.
That's all for this tutorial.
Best of luck!
Top comments (0)