Distributed Network Chat App
Goals
Build a chat application from scratch that:
- Supports many concurrent clients on a single server.
- Allows both unicast (client → specific client) and broadcast (client → everyone) messages.
- Delivers messages reliably even when clients are slow or temporarily backed up.
- Doesn't spawn a thread per client — uses I/O multiplexing instead.
No high-level networking libraries — only POSIX sockets and select().
Server architecture
The server runs a single select() loop. The fd_set tracks the listening socket plus every connected client. When select() returns, the server walks the ready descriptors:
- Listening socket ready → accept a new client, add it to the connection table and the fd_set.
- Client socket ready for read → drain available bytes into that client's input buffer, parse out any complete message frames, dispatch them.
- Client socket ready for write → flush as much of that client's output queue as possible.
Each connected client owns:
- An input buffer (partial message reassembly)
- An output queue (messages waiting to be delivered)
- A user-id / nickname
Messages are length-prefixed: a 4-byte big-endian length, then a JSON payload. This means the parser never has to guess where a message ends.
Reliability
TCP gives us in-order, reliable byte delivery — but it doesn't give us message framing, and it doesn't tell us when the receiver has actually processed the message. The buffered output queue handles the "slow consumer" case: if a client can't keep up, messages stack up in their queue rather than blocking the whole server. There's a configurable cap; if a client exceeds it, they get disconnected.
For dispatching:
- Unicast lookups go through a hash table keyed by user-id.
- Broadcast walks the connection table and pushes the message into every queue.
Either way, the actual write happens on the next select() write-ready notification, so the dispatch path is non-blocking.
Client
The client is simpler — it has stdin and the server socket, multiplexed with the same select() pattern. Input from stdin is parsed for commands (/msg <user> <text>, /all <text>, /quit), framed, and sent to the server.
What I learned
select()is enough. I started this expecting to needepollor threads. With careful per-client buffering, a singleselect()loop handled all the concurrency the project needed and the code stayed readable.- Framing is the silent killer in network code. I spent more time on "did I get a complete message yet" than on any actual feature. Length-prefixed framing is boring and correct; everything else (delimiters, fixed-size messages, etc.) has edge cases.
- Backpressure has to be in the design from day one. The slow-consumer case is where chat servers fall over in production, and bolting it on later is much harder than designing the queue with a cap from the start.
Stack
C++17, POSIX sockets, select(), single-threaded server. No external dependencies.