Basic Chatroom App
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:
socketsocket→bindbind→listenlisten→acceptaccept. - Why each client needs its own thread, and what
broadcastbroadcastdoes. - How
encodeencode/decodedecodemove strings across the wire as bytes. - Why bare
except:except:and unframedrecvrecvare the two classic chat bugs.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- Standard library only —
socketsocketandthreadingthreadingare built in. - A basic mental model of client/server networking.
Getting Started
Create the project
- Create a folder named
chatroomchatroom. - Inside it, create
basic_chatroom_app.pybasic_chatroom_app.py.
Write the code
basic_chatroom_app.py
Source"""
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
# 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 = input("Do you want to start the server or client? (server/client): ").strip().lower()
if choice == "server":
server = ChatServer()
server.run()
elif choice == "client":
client = ChatClient()
print("Type your messages below:")
while True:
msg = input()
client.send_message(msg)
else:
print("Invalid choice. Exiting.")
"""
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
# 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 = input("Do you want to start the server or client? (server/client): ").strip().lower()
if choice == "server":
server = ChatServer()
server.run()
elif choice == "client":
client = ChatClient()
print("Type your messages below:")
while True:
msg = input()
client.send_message(msg)
else:
print("Invalid choice. Exiting.")
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): client# 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.
Step-by-Step Explanation
1. The server socket
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)AF_INETAF_INET = IPv4, SOCK_STREAMSOCK_STREAM = TCP (reliable, ordered). bindbind claims the address/port, listen(5)listen(5) opens the door with a backlog of 5 pending connections.
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()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()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
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)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_socketif client != client_socket is what stops you from seeing your own messages twice.
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 receiveself.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. encodeencode/decodedecode translate. recv(1024)recv(1024) reads up to 1024 bytes at a time.
Fix #1: Stop Swallowing Errors
Bare except: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)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
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)# 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
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 bufimport 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
| Problem | Cause | Fix |
|---|---|---|
Address already in useAddress already in use | Previous server didn’t release the port | setsockopt(SO_REUSEADDR)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 exceptexcept + list mutation in loop | Catch OSErrorOSError, remove after the loop |
| Client can’t type and receive | Receive loop on main thread | Run recvrecv on its own thread (the code does) |
| Garbled non-ASCII text | Wrong/missing encoding | Always encode/decode("utf-8")encode/decode("utf-8") |
| Everyone is “anonymous” | No handshake | Send a username on connect |
Variations to Try
- Private messages —
/msg alice hello/msg alice hellorouted to one user. - Chat rooms — multiple named channels; clients join/leave.
- Tkinter GUI — replace
input()input()/print()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.
asyncioasynciorewrite — swap threads forasyncasync/awaitawait.- Encryption — wrap the socket in TLS with
sslssl.
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
- 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
- Replace bare
exceptexceptwith targeted error handling. - Add usernames and join/leave notices.
- Implement length-prefixed framing.
- Build a Tkinter GUI client.
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 asyncioasyncio 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.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
