A from-scratch IRC server in C++98, built around a single-threaded
epollevent loop, a Singleton server, and a command-dispatcher architecture.
orcNet is an IRC server written in strict C++98. It accepts multiple simultaneous clients on a single thread, multiplexing I/O with epoll. The project demonstrates how to model network resources, protocol commands, and shared server state without modern smart pointers or multi-threading.
The server is built around three collaborating objects:
┌────────────────────────────────────────────────────────────┐
│ Server │
│ Singleton. Owns the listening socket, the epoll instance, │
│ and the registries of all Clients and Channels. │
└──────────────┬──────────────────────────────┬──────────────┘
│ │
┌────────▼──────────┐ ┌──────────▼──────────┐
│ Client │ │ Channel │
│ One per TCP conn │ │ One per #channel │
│ Holds read buffer │ │ Holds members & ops │
│ Has messageStack │ │ Has mode flags │
│ State: READ/WRITE │ │ │
└────────┬──────────┘ └─────────────────────┘
│
┌──────────▼───────────┐
│ MessageValidator │
│ Parses, validates & │
│ dispatches IRC cmds │
└──────────────────────┘
Serveris a Singleton — Many validators and command handlers need to ask "is this nick in use?", "does this channel exist?", etc. A single globalServer::getServer()keeps every component consistent without passing pointers through every function call.Clientowns connection state — Each client knows its file descriptor, registration status, pending outgoing messages (messageStack), and currentConnectionState. That state drives whatepollshould watch for next.Channelis the unit of broadcast — It keeps its own member list and operator list. When someone speaks,Server::sendChannelMessage()pushes the message to every other member's outgoing queue and flips them toWRITE.MessageValidatoris static — Validation rules don't need state; they inspect the input, consult the singleton server, and return a(bool, payload)pair that the command handler consumes.
| Pattern | Where you see it | What it solves |
|---|---|---|
| Singleton | Server::getServer() |
One authoritative server instance; accessible from validators and command handlers |
| RAII | Client, Channel, exception classes |
Sockets, epoll state, and members cleaned up in destructors |
| Command Dispatcher | Client::insert() dispatches after MessageValidator::validate() |
Separates parsing, validation, and execution so commands are easy to add |
| State Machine | ConnectionState: READ, WRITE, CLOSE |
Tells epoll exactly when to poll for input or output, and when to tear down |
| Factory of replies | FormattedMessages |
All IRC numeric replies and prefixed commands generated in one place |
| Custom exception hierarchy | AException → SysCallException, ParsingException |
Distinguishes syscall failures (bind, listen, accept) from setup/logic errors |
| Pub-Sub broadcast | Server::sendChannelMessage() |
Sends a message to every channel member except the emitter |
epoll_wait()
│
├── serverFD ready → acceptConnection() → new Client → EPOLLIN
│
├── EPOLLIN on client → recv() → buffer lines → parseMessage() → queue replies → EPOLLOUT
│
└── EPOLLOUT on client → send messageStack → return to EPOLLIN (or CLOSE)
All sockets are non-blocking and registered with edge-triggered (EPOLLET) epoll. A client buffers partial reads in a std::stringstream; only complete \r\n or \n-terminated lines are parsed.
When a handler produces a reply, it pushes it onto the client's messageStack and calls setState(WRITE). The next loop iteration sees EPOLLOUT, drains the queue, then switches back to READ. On QUIT, disconnect, or fatal error, the state becomes CLOSE and the server removes the FD from epoll and frees the Client.
| Command | What it does |
|---|---|
PASS |
Server password authentication |
NICK |
Set / change nickname |
USER |
Set username / realname |
JOIN |
Join a channel; comma-separated channels supported |
PART |
Leave a channel with optional reason |
PRIVMSG |
Message a channel or a user |
WHO |
List members of a channel |
KICK |
Operator removes a member |
INVITE |
Operator invites a user |
MODE |
View / change modes: o, i, t, k, l |
TOPIC |
View / set a channel topic |
QUIT |
Disconnect |
CAP LS 302 |
Capability negotiation — accepted as a no-op |
make
./ircserv <port> <password>Example:
./ircserv 6667 secret123Then connect:
nc localhost 6667or point any IRC client at localhost:6667.
PASS secret123
NICK alice
USER alice 0 * :Alice Smith
- Single-threaded concurrency: No threads, no locks — all coordination happens through
epolland explicit state onClientobjects. - Two client dialects: The parser detects whether a connection uses
\r\n(HexChat) or plain\n(netcat) by counting\r\nsequences. - Buffered output: Outgoing messages are queued so the server never blocks on
send(). - Colored logging:
Logger+Colornamespaces give timestamped, color-coded debug/info/error output. - Manual memory management: Every
ClientandChannelis freed inServer::deallocate()or inremoveFdFromManager()/ channel destructors.
- C++98 only — raw pointers are used intentionally.
- No TLS/SSL.
- Server-wide operator (
OPER) is not fully implemented beyond the numeric reply. - Registration expects
PASS → NICK → USERorder.
Built as part of the 42 curriculum — a focused exercise in sockets, protocol parsing, and event-driven design.