Exploring Networking Basics: Building a Chat App with TCP

Abdellah Ibn El Azraq
4 min readJun 12, 2024

--

In the previous article, we explored network protocols such as HTTP and developed our own protocol over TCP. In this article, we will use the protocol we built to create a chat application. Given that our protocol supports bi-directional communication, it is well-suited for applications like chat, where both the server and client need to send data to each other.

Our chat application is designed to be a simple yet effective demonstration of basic networking concepts and the capabilities of a custom network protocol.

Build The Server:

We will start by implementing the server first. The purpose of the server is to accept data from different connections and resend the data to all connected clients. Fortunately, our protocol already handles most of this functionality. We only need to add a small modification to distinguish between new connections and messages. When a client connects to the server and sends their username, we want to notify other clients about the new user.

Here’s how our server implementation will look:

const { server } = require("../protocol88/protocol88");

// Start the server on the specified IP address and port
server.startServer("127.0.0.1", 3800, (address) => {
console.log("Server is listening on", address);
});

// Handle incoming requests
server.on("request", (data) => {
const { message, username } = data.body;

if (!message) {
// Notify all clients that a new user has joined
server.send(`User ${username} joined the chat!`);
} else {
// Send the message from the user to all clients
server.send(`${username}: ${message}`);
}
});

// Handle any errors that occur
server.on("error", (error) => {
console.log("Server error:", error);
});

As you can see, the server implementation is straightforward and self-explanatory. The server starts listening on the specified IP address and port, handles incoming messages by either notifying all clients of a new user or broadcasting a user’s message, and logs any errors that occur.

Build the Client:

We can build the client using any programming language that supports TCP and server socket connections. Here, we’ll implement the client in Node.js. Implementing the client will require a bit more work since we want to take input from the user and send it to the server. To read input from the user in Node.js, we can use the readlinemodule, which makes working with user input and output easier. Let's start by implementing the client.

Initial Implementation

const { connectToServer } = require("../protocol88/protocol88");
const readline = require("readline/promises");

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const client = connectToServer("127.0.0.1", 3800);

let username;

client.on("response", (response) => {
console.log(response.body);
askAndSend("Write a message: ");
});

async function askAndSend(prompt) {
const answer = await rl.question(prompt);

if (username) {
client.send({ username, message: answer });
} else {
username = answer;
client.send({ username });
}
}

(async () => {
await askAndSend("Enter your username: ");
})();

If we run the client after starting the server, we will see something like this:

Enter your username: bob
User bob joined the chat!
Write a message: Hi!
bob: Hi!
Write a message:

Improved Interface

To improve the interface and clear the prompt message when we receive a message from the server, we can add a clear function. This function will remove the previous line, which is the prompt line.

const { connectToServer } = require("../protocol88/protocol88");
const readline = require("readline/promises");

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const client = connectToServer("127.0.0.1", 3800);

let username;

function clear() {
console.log();
process.stdout.moveCursor(0, -1);
process.stdout.clearLine(0);
}

client.on("response", async (response) => {
clear();
console.log(response.body);
askAndSend("Write a message: ");
});

async function askAndSend(prompt) {
const answer = await rl.question(prompt);

process.stdout.moveCursor(0, -1);
process.stdout.clearLine(0);

if (username) {
client.send({ username, message: answer });
} else {
username = answer;
client.send({ username });
}
}

(async () => {
await askAndSend("Enter your username: ");
})();

With this improvement, the interface will be cleaner and more user-friendly:

Source Code

Abstraction and Implementation:

Abstraction is a fundamental concept in computer science, and throughout this example, we can clearly see its benefits. We used the protocol we built without worrying about its internal workings, focusing only on the protocol’s API and how to use it. This level of abstraction allows us to simplify complex systems and work more efficiently.

Almost everything in computing is based on layers of abstraction. We build levels of abstraction on top of each other without concerning ourselves with the underlying implementation details. For example, HTTP is an abstraction on top of TCP, which in turn is built on other abstractions. Typically, we focus on how to use HTTP APIs rather than understanding the intricacies of how HTTP works internally.

In future articles, we will delve deeper into networking topics such as compression and security. If you enjoyed reading this article, you can follow me on my social accounts LinkedIn or X.

--

--

Abdellah Ibn El Azraq
Abdellah Ibn El Azraq

Written by Abdellah Ibn El Azraq

I'm a software developer with a passion for coding, learning new technologies and share what I know with the world

No responses yet