ft_irc
is a project which you may do during your cursus, and if you’re reading this then I assume you are. It’s a cool project, and resolves around you developing an IRC (Internet Relay Chat) server. While it’s a manageable project, it will challenge and expand your understanding of bi-directional file-descriptor communication (aka sockets).
If you don’t know what IRC is, here’s a TL;DR: it’s a simple and robust protocol built to host communities. Think of it as an early predecessor to modern platforms like Discord or Slack, but with a key difference: IRC is open, meaning anyone can create a server or build a client, giving users complete control over their chat environment, without relying on third-party services.
Understanding the project scope
I like to separate this project under four fundamental pillars: the server, the command parser, the channels, and the client (not the actual IRC client you will use to connect to the server, but a class which represents a “connection”, which I refer to a client). Each of these components plays a crucial role in creating a functional IRC server that allows clients to connect, communicate, and interact in real-time. Let’s break down each of these components to understand their roles and how they work together to create an awesome space to chat!
Here’s a little diagram of how I personally developed this, mainly focusing on the OOP structure of the project:
The server
The server is integral to managing socket connections, which forms the backbone of the server-client communication, as well as safeguarding top-level variables such as the server password. The server uses sockets to handle incoming and outgoing data, enabling real-time interactions between clients and the IRC network.
Socket management begins with creating and binding a listening socket. This socket listens for incoming connection requests from clients. Once a connection is established, the server uses another socket to communicate with the client. Managing these sockets involves several steps: creating the socket, binding it to a port, listening for incoming connections, and accepting those connections. Each accepted connection is then handled by a new socket, allowing multiple clients to interact with the server concurrently.
Here’s a basic example demonstrating how to accept connections and read data from a client using a socket:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[1024];
int port = 6667; // Default IRC port
// Create the server socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return 1;
}
// Set up the server address struct
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // Set the address family to IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // Bind to all available interfaces
server_addr.sin_port = htons(port); // Convert the port to network byte order
// Bind the socket to the address and port
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(server_fd);
return 1;
}
// Listen for incoming connections
if (listen(server_fd, 5) < 0) {
perror("listen");
close(server_fd);
return 1;
}
std::cout << "Server listening on port " << port << std::endl;
// Accept a connection
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd < 0) {
perror("accept");
close(server_fd);
return 1;
}
std::cout << "Client connected" << std::endl;
// Read data from the client
ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
if (bytes_read < 0) {
perror("read");
close(client_fd);
close(server_fd);
return 1;
}
buffer[bytes_read] = '\0'; // Null-terminate the buffer
std::cout << "Received message: " << buffer << std::endl;
// Close the client and server sockets
close(client_fd);
close(server_fd);
return 0;
}
This is a simple program you can copy and run locally (after reading the code and making it’s not malware of course!). You can test it out by running it in the background and running nc 127.0.0.1 6697
in a separate terminal window. You can input anything you want into STDIN
and you’ll notice back in your server logs your input is printed. Here is a quick explanation of the code:
- Socket Creation: The function
socket(2)
is used to create a TCP socket for the server. It establishes a communication endpoint for the server to listen for client connections. - Binding: The function
bind(2)
binds the created socket to a specific port and IP address, as defined in the structsockaddr_in
. This associates the socket with a network address. - Listening: The
listen(2)
function prepares the socket to accept incoming connections. It specifies the maximum number of connections that can be queued for acceptance. - Accepting Connections: The
accept(2)
function waits for a client to connect. When a connection is established, it creates a new socket (referred to as client_fd) to handle the communication with the connected client. - Reading Data: The
read(2)
function is used to read data from the client socket. It retrieves data sent by the client, which can then be processed by the server.
This basic setup demonstrates how to handle client connections and read data in a straightforward manner. You’ll need to adapt this code just a bit to suite the complexity of your server—it would be too easy if I just gave it to you, right?
Parsing the input
After you create a socket and manage active connections, you need a way to parse incoming commands and send them to the correct service for handling. This of course doesn’t require much work, mostly if-else is OK.
However, this is the part where you might want to handle a global authentication wall, as most commands are not accessible to non-registered users. A basic setup required for the subject is a 3-tier auth state, from UNAUTHENTICATED
, to AUTHENTICATED
, and finally to REGISTERED
.
Channels
Channels are the heart of group communication on your IRC server—they’re the rooms where clients gather, chat, and interact in real-time. Think of channels as virtual spaces, each with its own set of rules, members, and vibe.
Each channel is identified by a name (prefixed with a #
, like #general
) and can have various attributes such as a topic, a list of operators, a user limit, and even a password for restricted access. So when a user joins a channel, they’re subject to the channel’s specific rules and permissions.
The channel is responsible for maintaining a list of members, tracking which users are in the room at any given time, and ensuring the rules are enforced. This includes managing operator privileges, which allow certain users to kick or ban others, set topics, and tweak channel settings.
Channels also manage the flow of messages. When a user sends a message to a channel, the channel must ensure that the message is broadcasted to all members of the channel. This message synchronisation ensures everyone is on the same page, maintaining the natural rhythm of the conversation.
Another key aspect of channels is their ability to control access. Channels can be open to all, invite-only, or password-protected. These settings are applies differently too, some upon initialisation, and others with the MODE
command. I’ll let you figure that one out.
Clients
In the context of the server, a “Client” isn’t just a person sitting behind an IRC client like HexChat or irssi—it’s actually a representation of a connection to the server, abstracted into a class that manages the state of each user interacting with the IRC network.
The client class handles all the crucial data tied to each user: their nickname, username, hostname, real name, FD, and importantly, their current authentication state. As users progress through the connection lifecycle—from initial connection, to setting their nickname and username, and finally registering with the server—the client class helps track and enforce these states to ensure smooth server operations.
This class is also responsible for managing which channels the user is currently in, handling incoming and outgoing messages, and maintaining a correct status of the user’s activity on the server. For example, when a user sends a message, the client must ensures that the message is formatted correctly and sent to the intended recipient(s), whether that’s another user or an entire channel.
The client is more than just a data holder—it’s an active participant in the server’s functionality, ensuring that all communication happens as expected and that the user experience remains seamless and enjoyable.
Responding to messages
So now that you understand the basic structure of the project, I want to give you a hand with responding to certain messages, as it’s not always clear…
I recommend reading up on the IRCv3 specification, instead of the RFC1459, RFC2812 of RFC7194 specs, which are relatively outdated. Not that the old RFC won’t work, but simply because you may waste time decoding messages from clients that follow the new protocol.
The next thing I recommend, is to connect to Libera Chat—the most popular public IRC server—with netcat
. As you do this, you can replicate their behaviour instead of constantly relying on the RFC to explain the commands (as again: it’s not always clear).
Which client should I use?
There are many clients to choose from, but by far the best one we found was Halloy. It’s cross-platform, reliable, simple, and easy to configure. The only downside is that you need Cargo to run it at 42, which may not be available. Another option is irssi, but again it requires some dependencies to get up and running.
Common Mistakes
- Be careful to properly broadcast each event that other clients need to see (when someone joins/leaves a channel, if someone if kicked, or if the mode changes)
JOIN #chan1,#chan2,#chan3 pass1,pass2
- What if
username
,nickname
andrealname
were all different? - Make sure to test the
+k
mode properly! - Why do I have a
@
prefixed to my nickname?