A multi-threaded TCP chat server built from scratch with Python's standard library. Supports concurrent clients, named message broadcasting, and server-side admin controls.
I built this to understand TCP networking and concurrency below the abstraction layer of web frameworks: the sockets API (bind, listen, accept, send, recv, and the TCP state machine), thread-per-connection design with shared mutable state, and the GIL's real-world impact on I/O-bound workloads.
The server uses a thread-per-client model. One acceptor thread blocks on socket.accept() and spawns a ClientHandler thread per connection. Each handler runs an independent receive loop; messages fan out through a shared clients dict (username → socket).
┌─────────────────────────────────────────────────┐
│ SERVER (:9999) │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ ChatServer Class │ │
│ │ ┌─────────┐ ┌──────────┐ ┌───────┐ │ │
│ │ │ Accept │ │Broadcast │ │Client │ │ │
│ │ │ Thread │ │ Engine │ │ Map │ │ │
│ │ └────┬────┘ └────┬─────┘ └───┬───┘ │ │
│ └───────┼─────────────┼────────────┼──────┘ │
└───────────┼─────────────┼────────────┼──────────┘
│ │ │
┌───────────────────────┼─────────────┼────────────┼───────────────────┐
│ TCP Connection Pool │ │ │
│ │ │ │ │
│ ┌────────────────────┴──────┐ ┌───┴────────────┴──────┐ │
│ │ Client Handler Thread 1 │ │ Client Handler Thd 2 │ ... │
│ │ ┌──────────────────────┐│ │ ┌──────────────────┐ │ │
│ │ │ get_username() ││ │ │ get_username() │ │ │
│ │ │ message loop ││ │ │ message loop │ │ │
│ │ └──────────────────────┘│ │ └──────────────────┘ │ │
│ └──────────────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Client A │ │ Client B │
│ (ephemeral) │ │ (ephemeral) │
└─────────────────┘ └─────────────────┘
Client A Server Client B
│ │ │
│──── SYN ───────────────► │ │
│◄─── SYN-ACK ──────────── │ │
│──── ACK ────────────────►│ │
│ │ │
│──── "Enter your ───────► │ │
│ username:" │ │
│◄─── "Alice" ──────────── │ │
│ │── "Alice has joined." ──────► │
│ │ │
│──── "hello everyone" ──► │ │
│ │── "Alice: hello everyone" ──► │
│◄── "Bob: hey Alice" ──── │◄─── "Bob: hey Alice" ─────── │
│ │ │
│──── FIN ────────────────►│ │
│ │── "Alice has disconnected." ► │
Daemon threads — Every handler thread is created with daemon=True. When the main thread terminates (via os._exit on shutdown), daemon threads are killed automatically. This avoids explicit join tracking, though in-flight send() calls may be interrupted without recovery.
Safe dict iteration with list() copy — The broadcast loop iterates over list(self.clients.items()) rather than the dict directly. Without this intermediate copy, a mid-broadcast disconnect that removes an entry from clients raises RuntimeError: dictionary changed size during iteration.
os._exit() for shutdown — sys.exit() raises SystemExit, which a bare except: anywhere in the thread pool would catch and suppress. os._exit() terminates the process immediately, file descriptors and all. A cooperative threading.Event-based approach would be cleaner but was deferred.
Binding to 0.0.0.0 — The server listens on all interfaces so LAN clients connect without configuration. A production deployment would bind to a specific interface; for a learning project, 0.0.0.0 is the pragmatic default.
Cleanup on disconnect — When recv() returns empty bytes (FIN from peer), the handler removes its entry from clients and broadcasts a disconnect notification. The socket is closed via socket.close() to release the fd.
Broken pipe during broadcast — Writing to a socket whose peer has already sent FIN raises BrokenPipeError (SIGPIPE on UNIX). The broadcast loop now catches OSError (parent class of both BrokenPipeError and ConnectionResetError) per-client, removes the dead entry, and continues. Without this, one abrupt disconnect crashed the entire server.
Stale client entries after handler crash — If a handler thread exited without cleanup (e.g., unhandled exception), its entry remained in clients. Subsequent broadcasts would silently fail on that socket. Fixed by wrapping the message loop in try/finally that guarantees removal from the client map.
Blocking accept() on shutdown — After the shutdown command, the acceptor thread blocks on accept() forever. The fix: close the server socket so accept() raises OSError, which the loop interprets as the signal to exit.
- Test suite — Zero tests. A
pytestsuite with loopback socket tests would make refactoring safe. - Structured logging — Replace
printwithlogging(INFO for joins, DEBUG for raw bytes, ERROR for failures). - Package with
pyproject.toml— Define entry points sopip install -e .works. - Cooperative shutdown — Use
threading.Eventto drain work before exit instead of daemon-thread semantics. - Rate limiting — No back-pressure mechanism; a malicious client could flood the broadcast loop.
chat-server/
├── client/
│ └── client.py # TCP chat client
├── server/
│ ├── __init__.py
│ ├── main.py # Server entry point
│ ├── chat_server.py # ChatServer class
│ └── client_handler.py # Per-client handling logic
├── .gitignore
├── LICENSE
└── README.md
git clone https://github.com/Utkarsh464/chat-server.git && cd chat-serverNo external dependencies — pure Python standard library (3.7+).
cd server && python main.py| Command | Effect |
|---|---|
text message |
Broadcasts [Server]: <message> to all clients |
shutdown |
Notifies all clients and stops the server |
exit |
Exits the admin input loop (server keeps running) |
cd client && python client.py| Command | Effect |
|---|---|
any text |
Sends message to all connected users |
exit |
Disconnects from server |
Enter the port number for the chat server: 9999
Server IP: <server-ip>
Server is listening on port 9999...
A new client has connected.
Alice has joined.
A new client has connected.
Bob has joined.
Bob: hi ..anyone here?
Alice: hey Bob! I was waiting for you
[Server]: focus on the fight guys
shutdown
Server shut down.
Enter the server IP address: <server-ip>
Enter the server port: 9999
Enter your username: Alice
Bob: hi ..anyone here?
hey Bob! I was waiting for you
[Server]: focus on the fight guys
[Server]: Server is shutting down.
- End-to-end encryption for private messaging
- SQLite-backed persistent chat history
- File sharing and image transfer
- Private messaging (
/msg <user> <message>) - Nickname changes and color-coded usernames
- Web-based client using WebSockets
- Rate limiting and anti-spam measures
- Docker containerization
- CI/CD pipeline with GitHub Actions
Distributed under the MIT License. See LICENSE for more information.