Python Socket Programming: Build a TCP Server and Client
Learn Python socket programming from scratch. Build TCP and UDP servers and clients, handle multiple connections, and implement a simple chat application.
What you'll learn
- ✓Create TCP servers and clients with the socket module
- ✓Handle multiple connections with threading and selectors
- ✓Build a working multi-client chat server
- ✓Understand UDP sockets and when to use them
Prerequisites
- •Python functions and classes
- •Basic networking concepts (IP, ports, TCP)
What Are Sockets?
A socket is an endpoint for sending or receiving data across a network. When you visit a website, your browser creates a socket to connect to the web server. When you send an API request, a socket handles the underlying communication.
Python’s socket module provides a low-level interface to the BSD socket API. While most developers use higher-level libraries like requests or aiohttp, understanding sockets teaches you what those libraries do under the hood and lets you build custom network protocols.
A Minimal TCP Server and Client
TCP (Transmission Control Protocol) provides reliable, ordered delivery of data. Most network applications use TCP.
The Server
import socket
def start_server(host='127.0.0.1', port=65432):
# Create a TCP socket
# AF_INET = IPv4, SOCK_STREAM = TCP
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
# Allow reusing the address immediately after the server stops
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind to the address and port
server.bind((host, port))
# Start listening for connections (backlog of 5)
server.listen(5)
print(f"Server listening on {host}:{port}")
while True:
# Accept a new connection (blocks until a client connects)
client_socket, client_address = server.accept()
with client_socket:
print(f"Connected by {client_address}")
# Receive data (up to 1024 bytes)
data = client_socket.recv(1024)
if data:
print(f"Received: {data.decode()}")
# Send a response
response = f"Echo: {data.decode()}"
client_socket.sendall(response.encode())
start_server()
The Client
import socket
def start_client(host='127.0.0.1', port=65432):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
# Connect to the server
client.connect((host, port))
# Send a message
message = "Hello, Server!"
client.sendall(message.encode())
# Receive the response
data = client.recv(1024)
print(f"Received from server: {data.decode()}")
start_client()
Run the server in one terminal and the client in another. The client sends “Hello, Server!” and receives “Echo: Hello, Server!” back.
Understanding the Socket Lifecycle
A TCP connection follows this flow:
# Server side:
# 1. socket() - Create the socket
# 2. bind() - Attach to an address and port
# 3. listen() - Start accepting connections
# 4. accept() - Wait for a client (returns a new socket)
# 5. recv/send - Exchange data
# 6. close() - Shut down
# Client side:
# 1. socket() - Create the socket
# 2. connect() - Connect to the server
# 3. send/recv - Exchange data
# 4. close() - Shut down
Important Details
import socket
# sendall vs send
# send() may not send all bytes in one call
# sendall() keeps sending until everything is delivered
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# sock.send(data) # Might send partial data
# sock.sendall(data) # Guarantees all data is sent
# recv returns empty bytes when the connection is closed
# Always check for this
data = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break # Connection closed
data += chunk
Handling Multiple Clients with Threading
The basic server above handles one client at a time. To serve multiple clients simultaneously, use threads:
import socket
import threading
def handle_client(client_socket, address):
"""Handle a single client connection."""
print(f"[+] New connection from {address}")
try:
while True:
data = client_socket.recv(1024)
if not data:
break
message = data.decode()
print(f"[{address}] {message}")
response = f"Server received: {message}"
client_socket.sendall(response.encode())
except ConnectionResetError:
print(f"[-] Connection reset by {address}")
finally:
client_socket.close()
print(f"[-] Connection closed: {address}")
def start_threaded_server(host='127.0.0.1', port=65432):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(5)
print(f"Server listening on {host}:{port}")
try:
while True:
client_socket, address = server.accept()
# Spawn a new thread for each client
thread = threading.Thread(
target=handle_client,
args=(client_socket, address),
daemon=True # Thread dies when main thread exits
)
thread.start()
print(f"Active connections: {threading.active_count() - 1}")
except KeyboardInterrupt:
print("\nShutting down server")
finally:
server.close()
start_threaded_server()
Interactive Client
import socket
def interactive_client(host='127.0.0.1', port=65432):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.connect((host, port))
print(f"Connected to {host}:{port}. Type 'quit' to exit.")
while True:
message = input("You: ")
if message.lower() == 'quit':
break
client.sendall(message.encode())
response = client.recv(1024).decode()
print(f"Server: {response}")
interactive_client()
Building a Chat Server
Here is a more complete example: a multi-client chat room where messages from one client are broadcast to all others.
Chat Server
import socket
import threading
class ChatServer:
def __init__(self, host='127.0.0.1', port=65432):
self.host = host
self.port = port
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.clients: dict[socket.socket, str] = {} # socket -> nickname
self.lock = threading.Lock()
def broadcast(self, message, sender_socket=None):
"""Send a message to all connected clients except the sender."""
with self.lock:
for client_socket in list(self.clients):
if client_socket != sender_socket:
try:
client_socket.sendall(message.encode())
except (BrokenPipeError, ConnectionResetError):
self.remove_client(client_socket)
def remove_client(self, client_socket):
"""Remove a client from the server."""
if client_socket in self.clients:
nickname = self.clients.pop(client_socket)
client_socket.close()
self.broadcast(f"[Server] {nickname} has left the chat.")
print(f"[-] {nickname} disconnected")
def handle_client(self, client_socket, address):
"""Handle messages from a single client."""
try:
# First message is the nickname
client_socket.sendall("Enter your nickname: ".encode())
nickname = client_socket.recv(1024).decode().strip()
with self.lock:
self.clients[client_socket] = nickname
welcome = f"[Server] {nickname} has joined the chat!"
print(f"[+] {nickname} connected from {address}")
self.broadcast(welcome, sender_socket=client_socket)
client_socket.sendall("[Server] Welcome to the chat!\n".encode())
while True:
data = client_socket.recv(1024)
if not data:
break
message = data.decode().strip()
if message:
formatted = f"[{nickname}] {message}"
print(formatted)
self.broadcast(formatted, sender_socket=client_socket)
except (ConnectionResetError, BrokenPipeError):
pass
finally:
self.remove_client(client_socket)
def start(self):
self.server.bind((self.host, self.port))
self.server.listen(10)
print(f"Chat server running on {self.host}:{self.port}")
try:
while True:
client_socket, address = self.server.accept()
thread = threading.Thread(
target=self.handle_client,
args=(client_socket, address),
daemon=True
)
thread.start()
except KeyboardInterrupt:
print("\nShutting down chat server")
finally:
with self.lock:
for client_socket in list(self.clients):
client_socket.close()
self.server.close()
if __name__ == '__main__':
ChatServer().start()
Chat Client
import socket
import threading
def receive_messages(sock):
"""Continuously receive and print messages from the server."""
while True:
try:
message = sock.recv(1024).decode()
if not message:
print("\nDisconnected from server.")
break
print(f"\r{message}\nYou: ", end='', flush=True)
except (ConnectionResetError, OSError):
print("\nConnection lost.")
break
def chat_client(host='127.0.0.1', port=65432):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.connect((host, port))
# Receive the nickname prompt
prompt = client.recv(1024).decode()
nickname = input(prompt)
client.sendall(nickname.encode())
# Start receiving messages in a background thread
receiver = threading.Thread(
target=receive_messages,
args=(client,),
daemon=True
)
receiver.start()
# Send messages from the main thread
while True:
message = input("You: ")
if message.lower() == '/quit':
break
client.sendall(message.encode())
if __name__ == '__main__':
chat_client()
Using selectors for Scalable I/O
Threading works fine for moderate numbers of clients, but for high concurrency, the selectors module provides event-driven I/O multiplexing:
import socket
import selectors
import types
sel = selectors.DefaultSelector()
def accept_connection(server_socket):
client_socket, address = server_socket.accept()
print(f"Connected: {address}")
client_socket.setblocking(False)
# Store per-connection state
data = types.SimpleNamespace(addr=address, inb=b'', outb=b'')
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(client_socket, events, data=data)
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024)
if recv_data:
data.outb += recv_data # Echo back
else:
print(f"Closing connection to {data.addr}")
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
sent = sock.send(data.outb)
data.outb = data.outb[sent:]
def start_selector_server(host='127.0.0.1', port=65432):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen()
server.setblocking(False)
sel.register(server, selectors.EVENT_READ, data=None)
print(f"Selector server listening on {host}:{port}")
try:
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
# This is the server socket -- accept new connection
accept_connection(key.fileobj)
else:
# This is a client socket -- handle data
service_connection(key, mask)
except KeyboardInterrupt:
print("\nShutting down")
finally:
sel.close()
start_selector_server()
UDP Sockets
UDP (User Datagram Protocol) is connectionless and does not guarantee delivery or order. It is faster than TCP and useful for real-time applications like gaming, DNS, and streaming.
import socket
# UDP Server
def udp_server(host='127.0.0.1', port=65433):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
server.bind((host, port))
print(f"UDP server listening on {host}:{port}")
while True:
data, client_address = server.recvfrom(1024)
message = data.decode()
print(f"Received from {client_address}: {message}")
response = f"Echo: {message}"
server.sendto(response.encode(), client_address)
# UDP Client
def udp_client(host='127.0.0.1', port=65433):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
message = "Hello, UDP!"
client.sendto(message.encode(), (host, port))
data, server_address = client.recvfrom(1024)
print(f"Received: {data.decode()}")
# Note: No connect(), listen(), or accept() needed for UDP
Key Differences Between TCP and UDP
# TCP (SOCK_STREAM):
# - Connection-oriented (connect -> send/recv -> close)
# - Reliable, ordered delivery
# - Stream-based (no message boundaries)
# - Use for: HTTP, FTP, SSH, database connections
# UDP (SOCK_DGRAM):
# - Connectionless (just send/recv)
# - No guaranteed delivery or ordering
# - Message-based (each send is one datagram)
# - Use for: DNS, gaming, video streaming, IoT
Sending Structured Data
Raw sockets deal in bytes. To send structured messages, you need a protocol for framing:
import socket
import json
import struct
def send_message(sock, data):
"""Send a JSON message with a length prefix."""
message = json.dumps(data).encode()
# Pack the length as a 4-byte unsigned integer
length_prefix = struct.pack('>I', len(message))
sock.sendall(length_prefix + message)
def recv_message(sock):
"""Receive a length-prefixed JSON message."""
# Read the 4-byte length prefix
raw_length = recv_exact(sock, 4)
if not raw_length:
return None
message_length = struct.unpack('>I', raw_length)[0]
# Read exactly that many bytes
raw_message = recv_exact(sock, message_length)
if not raw_message:
return None
return json.loads(raw_message.decode())
def recv_exact(sock, num_bytes):
"""Receive exactly num_bytes from the socket."""
data = b''
while len(data) < num_bytes:
chunk = sock.recv(num_bytes - len(data))
if not chunk:
return None
data += chunk
return data
# Usage in a server handler:
# send_message(client_socket, {"status": "ok", "result": 42})
# data = recv_message(client_socket)
# print(data) # {'status': 'ok', 'result': 42}
Socket Options and Timeouts
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Set a timeout (seconds) -- applies to connect, recv, send
sock.settimeout(10.0)
# Non-blocking mode
sock.setblocking(False)
# Reuse address (avoids "Address already in use" errors)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Keep-alive (detect dead connections)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Get the socket's own address after binding
sock.bind(('0.0.0.0', 0)) # Port 0 = OS picks a free port
print(f"Bound to: {sock.getsockname()}")
Wrapping Up
Socket programming is the foundation of all network communication in Python. Start with socket.socket() and subprocess.run()-style simplicity: create, connect, send, receive, close. Use threading or selectors to handle multiple clients. For most real applications, you will use libraries built on top of sockets (like asyncio, aiohttp, or requests), but understanding the underlying socket layer makes debugging network issues and building custom protocols much easier.
Related articles
- Python Python Concurrency: asyncio vs threading vs multiprocessing
Compare Python's concurrency models side by side. Learn when to use asyncio, threading, or multiprocessing with practical benchmarks and real-world examples.
- Python Python Environments: venv, pyenv, and Poetry Guide
Master Python environment management with venv for virtual environments, pyenv for version switching, and Poetry for dependency management.
- Python Python Match Statement: Structural Pattern Matching
Master Python's match statement with structural pattern matching. Learn literal, sequence, mapping, class, guard, and OR patterns with practical examples.
- Python Pydantic v2: Data Validation and Settings Management
Learn Pydantic v2 for data validation, serialization, and settings management in Python. Covers models, validators, computed fields, and BaseSettings.