Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Chat Application TCP/UDP Project

Advanced•1–2 weeks•sockets, select/poll, protocols

A multi-user chat server with rooms — built on TCP, compared with UDP.

Share on X

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

Feature checklist

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, announce

Full 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

Compile and run

g++ -std=c++17 -o chat_server chat_server.cpp -pthread

POSIX 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 sockets

Try it in your browser

No browser demo is included for this project. Build the C++ version below — that is the real program.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS