C++ Chat Application TCP/UDP Project
A multi-user chat server with rooms — built on TCP, compared with UDP.
What you will build
Grow the two-person chat into a real server: many clients, nicknames, rooms, private messages, all multiplexed with select() or poll(). Then rebuild message delivery over UDP and confront what 'unreliable' actually means. A networking masterclass in one project.
What you will learn
- Multiplexing many sockets with poll/select
- Broadcast and room routing
- TCP vs UDP trade-offs in practice
- Designing a message protocol with commands
- Handling partial reads and message framing
Feature checklist
- Server handles 10+ simultaneous clients
- Nicknames and join/leave announcements
- Rooms: /join #room to switch
- Private messages with /msg
- Online user list
- UDP mode with sequence numbers and acks (stretch goal)
Starter code
// poll()-based multi-client server sketch (POSIX)
#include <poll.h>
#include <vector>
#include <string>
#include <cstring>
#include <unistd.h>
struct Client { int fd; std::string nick, room; };
void broadcast(const std::vector<Client>& clients,
const Client& from, const std::string& msg) {
std::string line = from.nick + ": " + msg + "\n";
for (auto& c : clients)
if (c.fd != from.fd && c.room == from.room)
send(c.fd, line.c_str(), line.size(), 0);
}
// Main loop:
// poll() over {listen fd} + all client fds
// new connection -> accept, add Client, ask for nick
// client readable -> recv a line; if "/join x" move room,
// if "/msg u text" route privately,
// else broadcast() to the room
// recv returns 0 -> client left: close, erase, announceFull walkthrough
Step 1: Grow from two users to many: the poll() event loop
The intermediate chat used one thread for one peer. Ten clients cannot each get a blocking thread cheaply here, so the server uses poll() from <poll.h>: one array of pollfd (one per socket), and poll() sleeps until SOME socket is readable. One thread, many clients, zero busy-waiting — the event-driven pattern behind real servers. The starter's struct Client { int fd; std::string name; std::string room; } is the per-connection state poll cannot give you.
Step 2: Accept and register clients
The listening socket sits in the poll array too; when IT is readable, accept() a new client, add their fd to the array and a Client to the vector, and prompt for a nickname. A nickname collision gets a polite refusal or a suffix — decide, because broadcast() will identify people by this name.
Step 3: broadcast(): fan-out with an exclusion
broadcast(msg, room, excludeFd) loops the clients in that room and send()s to everyone except the sender (clients echo their own messages locally). Centralising fan-out in one function means join/leave announcements, chat lines, and system notices all travel the same tested path.
void broadcast(const std::string& msg, const std::string& room, int excludeFd) {
for (auto& c : clients)
if (c.room == room && c.fd != excludeFd)
send(c.fd, msg.c_str(), msg.size(), 0);
}Step 4: Rooms and commands: /join, /msg, /list, /quit
Lines starting with '/' are commands, everything else is chat. /join #room moves the Client's room field and triggers leave/join announcements in both rooms. /msg name text finds ONE client by nickname and sends privately. /list prints the room's nicknames. Commands are parsed on the first token — the mini shell's splitArgs instinct, reused on a socket.
Step 5: Handle disconnects without poisoning the loop
recv() returning 0 (or a send failing with EPIPE) means that client is gone: close the fd, REMOVE the Client from the vector and its pollfd from the array, and announce the departure. A dead fd left in the poll set makes poll() return instantly forever — the server spins at 100% CPU mourning a ghost. Removal mid-iteration is the fiddly part; mark-and-sweep after the event pass is the safe shape.
Step 6: UDP mode: the stretch goal with honest trade-offs
The UDP variant replaces connections with datagrams: no accept, no per-client socket — one socket, recvfrom gives you the sender's address each time, and 'clients' are addresses you have seen. UDP can drop, duplicate, or reorder messages, so the starter adds sequence numbers per sender and acks for messages that matter. Run both modes side by side and FEEL the difference: TCP hides the network's chaos; UDP hands it to you in a bucket.
Step 7: Test like a crowd
Open four terminals: three clients (two joining #general, one in #random) and the server. Verify room isolation (the #random user must NOT see #general traffic), a /msg that reaches exactly one person, /list accuracy, and a mid-conversation quit that announces itself and leaves the server responsive. Every one of those is a feature in the checklist, tested by acting, not by reading code.
Common mistakes
- Leaving a disconnected fd in the poll array, so poll() never blocks again and the server burns a CPU core forever.
- Broadcasting a private /msg to the whole room because the command parser fell through to the chat path.
- Assuming recv() returns whole messages on TCP — with several clients typing at once, lines arrive split and merged; buffer per client and split on newlines.
Compile and run
g++ -std=c++17 -o chat_server chat_server.cpp -pthreadPOSIX sockets + poll(): Linux/macOS as-is; Windows needs the Winsock port. -pthread if you keep a thread per client instead of the poll loop. Test everything on 127.0.0.1 first, exactly like the simpler chat project.
Sample run
$ ./chat_server
Chat server on port 5555 — rooms enabled, 0 clients
[ada joined #general]
[bola joined #general]
ada (#general): evening all — the poll loop is holding 3 clients easily
bola (#general): and nobody's messages are leaking into #random, I checked
[chidi joined #random]
[ada -> bola, private] psst: /msg works
[chidi left #random]
[bola left #general]
Server status: 1 client online · 14 messages relayed · 0 sent to dead socketsTry it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.