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

Teedus Chung
6 min readJun 10, 2021

--

In the previous part of this project, we’ve implemented some functions for building the basic chat app. We’re now going to add more functions. Let’s take a look at the below list of specification:

Namespaces

It’s time to start thinking about namespaces. A namespace is a communication channel that allows us to separate the logic of our application over a single shared connection (also called “multiplexing”). A common use case is to separate admin concerns from those of regular users.

Namespaces are created only at the server-side and they’re joined by clients by sending a request to the server.

For example, let’s create 2 namespaces:

/*** server-side ***/const usersNsp = io.of("/users");
usersNsp.on("connect", (socket) => {
//...
});
const ordersNsp = io.of("/orders");
ordersNsp.on("connect", (socket) => {
//...
});

As you see, we created a new namespace name /users by using the of method and add the connect listener to it.

On the client-side, we’ll tell the client to connect to our own namespaces as follows:

/*** client-side ***/const usersSocket = io("http://localhost:3000/users");
const ordersSocket = io("http://localhost:3000/orders");

Please notice that only one connection will be established in our example, and the packets will automatically be routed to the right namespace.

Default namespace

The root namespace “/” is the default namespace. By default, if a namespace is not specified by clients while connecting to the server, the socket connection will be attached to the default namespace “/”.

For example:

/*** client-side ***/const socket = io("http://localhost:3000"); // Default namespace

This is also the same on the server-side:

/*** server-side ***/io.on("connect", (socket) => {});
io.sockets;
io.emit("hello");
// are actually equivalent toio.of("/").on("connect", (socket) => {});
io.of("/").sockets;
io.of("/").emit("hello");

In our project, we only use the default namespace for the sake of simplicity.

Rooms

Within each namespace, we can also define arbitrary channels as many as our application needs that sockets can join and leave. These channels are called rooms. Rooms allow us to broadcast data to a subset of sockets.

One thing to keep in mind while using rooms is that they are a server-only concept. That means rooms can only be joined on the server-side.

As we can see clearly in the above structure that sockets connect to namespaces and more specifically to rooms where a client can have one or more sockets as it needs.

Discovery more…

Once we have a Socket.IO server that is running, we can analyse its information. All of the runtime data related to our server is organized in the io variable:

/*** server.js ***/const port = 3000;
const io = require("socket.io")(port);

Here is how we can implement:

On the client-side:

/*** client.js ***///...rl.on("line", (input) => {    //...    // For tracing: tr;
else if ("tr;" === input) {
socket.emit("trace");
}
});

On the server-side:

/*** server.js ***///...io.of("/").on("connect", (socket) => {    //...    // Listen to the 'trace' event emitted from the client
socket.on("trare", () => {
console.log("\n=============== Trace ===============");
console.log(io.of("/"));
});
});

Let’s run and we’ll see it’s a helpful function to implement other functions in our project!

Implement the ‘send’ function

Client

/*** client.js ***/var s_pattern = /^s;([A-Z0-9]+);(.+)/i;rl.on("line", (input) => {    //...    else if (true === s_pattern.test(input)) {
var info = input.match(s_pattern);
socket.emit("send", {"sender": nickname, "action": "send", "receiver": info[1], "msg": info[2]});
}
});//...socket.on("send", (data) => {
console.log("%s", data.msg);
});

Server

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

//...

socket.on("send", (data) => {
console.log("\n%s", data);

var socket_id = null;
for (const [key, value] of io.of("/").sockets) {
if (data.receiver.toLowerCase() === value.nickname) {
socket_id = key;
}
}
if (socket_id !== null) {
io.of("/").to(socket_id).emit("send", data);
}
});
});

Implement the ‘join_group’ function

Client

/*** client.js ***///...rl.on("line", (input) => {    //...    else if (true === input.startsWith("jg;")) {
var str = input.slice(3);
socket.emit("join_group", {"sender": nickname, "action": "join_group", "group": str});
}
});//...socket.on("join_group", (data) => {
console.log("[INFO]: %s has joined the group", data.sender);
});

Server

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

//...

socket.on("join_group", (data) => {
console.log("\n%s", data);
socket.join(data.group);
console.log("Group: ", data.group, ", Joined: ", data.sender);
io.of("/").to(data.group).emit("join_group", data);
});
});

Implement the ‘broadcast_group’ function

Client

/*** client.js ***/var bg_pattern = /^bg;([A-Z0-9]+);(.+)/i;rl.on("line", (input) => {    //...    else if (true === bg_pattern.test(input)) {
var info = input.match(bg_pattern);
socket.emit("broadcast_group", {"sender": nickname, "action": "broadcast_group", "group": info[1], "msg": info[2]});
}
});//...socket.on("broadcast_group", (data) => {
console.log("%s", data.msg);
});

Server

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

//...

socket.on("broadcast_group", (data) => {
console.log("\n%s", data);
socket.to(data.group).emit("broadcast_group", data); if (undefined === io.of("/").room_messages) {
io.of("/").room_messages = {};
}
if (undefined === io.of("/").room_messages[data.group]) {
io.of("/").room_messages[data.group] = [];
}
io.of("/").room_messages[data.group].push(data.msg);
});
});

Implement the ‘list_members_group’ function

Client

/*** client.js ***///...rl.on("line", (input) => {    //...    else if (true === input.startsWith("mbr;")) {
var str = input.slice(4);
socket.emit("list_members_group", {"sender": nickname, "action": "list_members_group", "group": str});
}
});//...socket.on("list_members_group", (data) => {
console.log("[INFO]: List of members:");
for (var i = data.members.length - 1; i >= 0; i--) {
console.log(data.members[i]);
}
});

Server

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

//...

socket.on("list_members_group", (data) => {
console.log("\n%s", data);
var socket_ids;
var members = [];
for (const [key, value] of io.of("/").adapter.rooms) {
if (key === data.group) {
socket_ids = value;
}
}
socket_ids.forEach((socket_id) => {
const socket_in_room = io.of("/").sockets.get(socket_id);
members.push(socket_in_room.nickname);
});
socket.emit("list_members_group", {"sender": data.sender, "action": "list_members_group", "group": data.group, "members": members});
});
});

Implement the ‘list_messages_group’ function

Client

/*** client.js ***///...rl.on("line", (input) => {    //...    else if (true === input.startsWith("msg;")) {
var str = input.slice(4);
socket.emit("list_messages_group", {"sender": nickname, "action": "list_messages_group", "group": str});
}
});//...socket.on("list_messages_group", (data) => {
console.log("[INFO]: History of messages:");
for (var i = data.msgs.length - 1; i >= 0; i--) {
console.log(data.msgs[i]);
}
});

Server

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

//...

socket.on("list_messages_group", (data) => {
console.log("\n%s", data);
var msgs = io.of("/").room_messages[data.group]; socket.emit("list_messages_group", {"sender": data.sender, "action": "list_messages_group", "group": data.group, "msgs": msgs});
});
});

Implement the ‘list_groups’ function

Client

/*** client.js ***///...rl.on("line", (input) => {    //...    else if ("grp;" === input) {
socket.emit("list_groups", {"sender": nickname, "action": "list_groups"});
}
});//...socket.on("list_groups", (data) => {
console.log("[INFO]: List of groups:");
for (var i = data.groups.length - 1; i >= 0; i--) {
console.log(data.groups[i]);
}
});

Server

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

//...

socket.on("list_groups", (data) => {
console.log("\n%s", data);
var groups = []; for (const [key, value] of io.of("/").adapter.rooms) {
if (false === value.has(key)) {
groups.push(key);
}
}
socket.emit("list_groups", {"sender": data.sender, "action": "list_groups", "groups": groups});
});
});

Implement the ‘leave_group’ function

Client

/*** client.js ***///...rl.on("line", (input) => {    //...    else if (true === input.startsWith("lg;")) {
var str = input.slice(3);
socket.emit("leave_group", {"sender": nickname, "action": "leave_group", "group": str});
}
});//...socket.on("leave_group", (data) => {
console.log("[INFO]: %s left the group", data.sender);
});

Server

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

//...

socket.on("leave_group", (data) => {
console.log("\n%s", data);

socket.leave(data.group);
console.log("Group: ", data.group, ", Left: ", data.sender);
io.of("/").to(data.group).emit("leave_group", data);
});
});

--

--

Responses (1)