Chatty
Tags: node, tutorial
Published: 2023-11-09 09:04:52

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](https://github.com/mrkazawa/chatty)

---

## Setup
To begin this tutorial, you will need to install [Node JS version 18.x.x](https://nodejs.org/en/about/previous-releases). Make a folder called `chatty`. Then, open the folder in **VSCode** and create a file called `package.json` with the following value:

```json
{
  "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](https://socket.io/docs/v4/server-installation/) is used to make our Web-Socket server
- [socket.io-client](https://socket.io/docs/v4/client-installation/) 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**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubgl1v9ou2so5p151hga.png)

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

```js
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

```js
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`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifkp4lmepdlpmnce154s.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ylcwte1n7536xsxm4bfj.png)

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.

```js
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](https://nodejs.org/docs/latest-v18.x/api/readline.html). The `rl` is the readline object that we use to process command line input.

```js
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.

```js
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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rcv6nrr8wi5g2fjsj8th.png)

---

## 3. Add User Registration
We use [Javascript Map object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) to store list of user along with their information. This map works like a key-value store database, but store only in-memory.

```javascript
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`.

```js
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`. 

```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](https://www.epochconverter.com/). 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.

```js
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

```js
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

```js
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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hs2swsckplzeovo6x95n.png)

After that, open another terminal and run `node client`, this time, use `bob` as username.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwtsxr1lkcl6zevqu4cv.png)

Go back to `alice` terminal, you will see bob data registration in the last line of console.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hngype0vgzzxe5egquw8.png)

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`.

```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`.

```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.

```js
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.

```js
// 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.

```js
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.

```js
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`. Type `alice` as username.
- Open another terminal and run `node client`. Type `bob` as username.
- Then, in both `alice` and `bob` terminal, you can type `!users`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lape5wdp0xlzdv65izzu.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w1s61ypo7grakga9s4tl.png)

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`.

```js
socket.on("chat", (data) => {
  socket.broadcast.emit("chat", data);
});
```

Then, add this to the `client.js`.

```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

```js
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.

```js
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`, put `alice` as username.
- Open another terminal, run `node client`, put `bob` as username.
- Open yet another terminal, run `node client`, put `trudy` as username.
- In `alice`, `bob`, and `trudy` terminal, type any messages.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1h24q5gt3teblpmdyhmc.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kzz6m622u6hz4z0uylcz.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4so5iojcp380xgzqznss.png)

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!