Basic Chatroom App
Abstract
Section titled “Abstract”A chatroom is the “hello world” of network programming — the moment sockets stop being abstract and start feeling like magic. In this tutorial you build a TCP server that accepts many clients at once and broadcasts each message to everyone else, plus a client that sends and receives at the same time. You’ll meet the two ideas that power almost all networked apps: sockets (the pipe between machines) and threads (so one slow client doesn’t block the rest). Then you’ll harden the naive version with usernames, graceful disconnects, and proper message framing.
You will leave understanding:
- The TCP server lifecycle:
socket→bind→listen→accept. - Why each client needs its own thread, and what
broadcastdoes. - How
encode/decodemove strings across the wire as bytes. - Why bare
except:and unframedrecvare the two classic chat bugs.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Standard library only —
socketandthreadingare built in. - A basic mental model of client/server networking.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
chatroom. - Inside it, create
basic_chatroom_app.py.
Write the code
Section titled “Write the code”basic_chatroom_app.py
pch.viewSource"""
Basic Chatroom App
A Python application that allows multiple users to communicate in a chatroom.
Features include:
- Server-side implementation to manage multiple clients.
- Client-side implementation for sending and receiving messages.
"""
import socket
import threading
def ask(prompt="", default=""):
"""Read a line, or fall back to `default` when nobody is there to type.
Without this the script raises EOFError the moment it runs unattended — in
a test, a scheduled job, or the build that captures this output for the
docs. The fallback is printed rather than silent, so a reader can always
tell which answers were typed and which were assumed.
"""
try:
return input(prompt).strip() or default
except EOFError:
print(f"{default} (no input available, using the default)")
return default
# Server-side implementation
class ChatServer:
def __init__(self, host="localhost", port=12345):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)
print(f"Server started on {host}:{port}")
self.clients = []
def broadcast(self, message, client_socket):
"""Send a message to all connected clients except the sender."""
for client in self.clients:
if client != client_socket:
try:
client.send(message)
except:
self.clients.remove(client)
def handle_client(self, client_socket):
"""Handle communication with a connected client."""
while True:
try:
message = client_socket.recv(1024)
if message:
print(f"Received: {message.decode('utf-8')}")
self.broadcast(message, client_socket)
except:
self.clients.remove(client_socket)
client_socket.close()
break
def run(self):
"""Start the server and accept incoming connections."""
while True:
client_socket, client_address = self.server.accept()
print(f"New connection: {client_address}")
self.clients.append(client_socket)
threading.Thread(target=self.handle_client, args=(client_socket,)).start()
# Client-side implementation
class ChatClient:
def __init__(self, host="localhost", port=12345):
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.connect((host, port))
threading.Thread(target=self.receive_messages).start()
def send_message(self, message):
"""Send a message to the server."""
self.client.send(message.encode("utf-8"))
def receive_messages(self):
"""Receive messages from the server."""
while True:
try:
message = self.client.recv(1024).decode("utf-8")
print(message)
except:
print("Disconnected from server.")
self.client.close()
break
if __name__ == "__main__":
choice = ask("Do you want to start the server or client? (server/client): ", '1').strip().lower()
if choice == "server":
server = ChatServer()
server.run()
elif choice == "client":
client = ChatClient()
print("Type your messages below:")
while True:
msg = ask("", '1')
client.send_message(msg)
else:
print("Invalid choice. Exiting.") Run it
Section titled “Run it”# Terminal 1 — start the server
C:\Users\Your Name\chatroom> python basic_chatroom_app.py
Do you want to start the server or client? (server/client): server
# Terminal 2 and 3 — start clients
C:\Users\Your Name\chatroom> python basic_chatroom_app.py
Do you want to start the server or client? (server/client): clientType in one client; the message appears in the others.
How it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD RUN(["python basic_chatroom_app.py"]) ChatServer["ChatServer
class"] ChatClient["ChatClient
class"] RUN --> ChatServer RUN --> ChatClient
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. The server socket
Section titled “1. The server socket”self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)AF_INET = IPv4, SOCK_STREAM = TCP (reliable, ordered). bind claims the address/port, listen(5) opens the door with a backlog of 5 pending connections.
2. Accepting clients, one thread each
Section titled “2. Accepting clients, one thread each”while True:
client_socket, client_address = self.server.accept() # blocks until someone connects
self.clients.append(client_socket)
threading.Thread(target=self.handle_client, args=(client_socket,)).start()accept() blocks until a client connects, then returns a dedicated socket for that client. Spinning up a thread per client means the server can talk to everyone concurrently — without threads, the second client would wait for the first to finish.
3. Broadcasting
Section titled “3. Broadcasting”def broadcast(self, message, client_socket):
for client in self.clients:
if client != client_socket: # don't echo back to the sender
try:
client.send(message)
except:
self.clients.remove(client)The server is a relay: a message from one client goes out to every other client. That if client != client_socket is what stops you from seeing your own messages twice.
4. Bytes on the wire
Section titled “4. Bytes on the wire”self.client.send(message.encode("utf-8")) # str -> bytes to send
message = self.client.recv(1024).decode("utf-8") # bytes -> str on receiveSockets move bytes, not strings. encode/decode translate. recv(1024) reads up to 1024 bytes at a time.
Fix #1: Stop Swallowing Errors
Section titled “Fix #1: Stop Swallowing Errors”Bare except: hides bugs (it even catches Ctrl-C). Catch what you mean and modifying a list while iterating it is unsafe:
def broadcast(self, message, sender):
dead = []
for client in self.clients:
if client is sender:
continue
try:
client.send(message)
except OSError: # socket actually broken
dead.append(client)
for client in dead: # remove AFTER the loop
self.clients.remove(client)Fix #2: Add Usernames
Section titled “Fix #2: Add Usernames”Right now everyone’s anonymous. Have each client send a name on join, store it, and prefix messages:
# client, right after connect:
self.client.send(self.username.encode("utf-8"))
# server, first recv in handle_client:
name = client_socket.recv(1024).decode("utf-8")
self.names[client_socket] = name
self.broadcast(f"*** {name} joined ***".encode(), client_socket)Fix #3: Frame Your Messages
Section titled “Fix #3: Frame Your Messages”TCP is a stream, not a series of messages — two fast sends can arrive glued together, or one send can arrive split. For real apps, prefix each message with its length:
import struct
def send_msg(sock, text):
data = text.encode("utf-8")
sock.sendall(struct.pack(">I", len(data)) + data) # 4-byte length header
def recv_exact(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
return None
buf += chunk
return bufRead the 4-byte length, then read exactly that many bytes. This is how you avoid the “messages mash together” bug.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
Address already in use | Previous server didn’t release the port | setsockopt(SO_REUSEADDR) before bind |
| Messages glue together / split | TCP is a stream, not messages | Length-prefix framing |
| Server crashes when a client quits | Bare except + list mutation in loop | Catch OSError, remove after the loop |
| Client can’t type and receive | Receive loop on main thread | Run recv on its own thread (the code does) |
| Garbled non-ASCII text | Wrong/missing encoding | Always encode/decode("utf-8") |
| Everyone is “anonymous” | No handshake | Send a username on connect |
Variations to Try
Section titled “Variations to Try”- Private messages —
/msg alice hellorouted to one user. - Chat rooms — multiple named channels; clients join/leave.
- Tkinter GUI — replace
input()/print()with a window (see Chat Application with Sockets). - Message history — persist the last N messages and replay on join.
- Nicknames & colors — per-user display styling.
asynciorewrite — swap threads forasync/await.- Encryption — wrap the socket in TLS with
ssl.
Real-World Applications
Section titled “Real-World Applications”- Messaging apps — the conceptual core of Slack, Discord, IRC.
- Live support widgets — customer chat on websites.
- Multiplayer games — real-time state sync between players.
- IoT command channels — devices reporting to a hub.
Educational Value
Section titled “Educational Value”- Socket programming — the server lifecycle and TCP basics.
- Concurrency — thread-per-client and shared state.
- Serialization — encoding text to bytes and message framing.
- Robustness — handling the disconnect/error paths that demos ignore.
Next Steps
Section titled “Next Steps”- Replace bare
exceptwith targeted error handling. - Add usernames and join/leave notices.
- Implement length-prefixed framing.
- Build a Tkinter GUI client.
Conclusion
Section titled “Conclusion”You built a multi-client chatroom and met the two pillars of network programming — sockets and threads — then fixed the three bugs every first chat app has: swallowed errors, anonymous users, and unframed streams. From here, a GUI or asyncio rewrite is a natural next step, and the relay-server pattern you learned underpins every messaging system you’ve ever used. Full source on GitHub. Explore more networking projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading