Skip to content

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: socketsocketbindbindlistenlistenacceptaccept.
  • Why each client needs its own thread, and what broadcastbroadcast does.
  • How encodeencode/decodedecode move strings across the wire as bytes.
  • Why bare except:except: and unframed recvrecv are the two classic chat bugs.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • Standard library only — socketsocket and threadingthreading are built in.
  • A basic mental model of client/server networking.

Getting Started

Create the project

  1. Create a folder named chatroomchatroom.
  2. Inside it, create basic_chatroom_app.pybasic_chatroom_app.py.

Write the code

basic_chatroom_app.pySource
basic_chatroom_app.py
"""
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.py
"""
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

command
# 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
command
# 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

Type in one client; the message appears in the others.

Step-by-Step Explanation

1. The server socket

basic_chatroom_app.py
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)
basic_chatroom_app.py
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

basic_chatroom_app.py
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()
basic_chatroom_app.py
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

basic_chatroom_app.py
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)
basic_chatroom_app.py
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

basic_chatroom_app.py
self.client.send(message.encode("utf-8"))      # str -> bytes to send
message = self.client.recv(1024).decode("utf-8")  # bytes -> str on receive
basic_chatroom_app.py
self.client.send(message.encode("utf-8"))      # str -> bytes to send
message = self.client.recv(1024).decode("utf-8")  # bytes -> str on receive

Sockets 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:

safe_broadcast.py
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)
safe_broadcast.py
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:

usernames.py
# 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)
usernames.py
# 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:

framing.py
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 buf
framing.py
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 buf

Read the 4-byte length, then read exactly that many bytes. This is how you avoid the “messages mash together” bug.

Common Mistakes

ProblemCauseFix
Address already in useAddress already in usePrevious server didn’t release the portsetsockopt(SO_REUSEADDR)setsockopt(SO_REUSEADDR) before bind
Messages glue together / splitTCP is a stream, not messagesLength-prefix framing
Server crashes when a client quitsBare exceptexcept + list mutation in loopCatch OSErrorOSError, remove after the loop
Client can’t type and receiveReceive loop on main threadRun recvrecv on its own thread (the code does)
Garbled non-ASCII textWrong/missing encodingAlways encode/decode("utf-8")encode/decode("utf-8")
Everyone is “anonymous”No handshakeSend a username on connect

Variations to Try

  1. Private messages/msg alice hello/msg alice hello routed to one user.
  2. Chat rooms — multiple named channels; clients join/leave.
  3. Tkinter GUI — replace input()input()/print()print() with a window (see Chat Application with Sockets).
  4. Message history — persist the last N messages and replay on join.
  5. Nicknames & colors — per-user display styling.
  6. asyncioasyncio rewrite — swap threads for asyncasync/awaitawait.
  7. 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 exceptexcept with 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 coffee

Was this page helpful?

Let us know how we did