Sockets #
Almost all network communication — HTTP, databases, email, chat — is built on top of sockets. A socket is an abstraction representing the endpoint of a network connection: you can open a socket, send data through it, and read incoming data from it, just like reading and writing to a file. Python’s socket module gives you direct access to the operating system’s socket API, letting you build network servers and clients from the most basic level.
How Sockets Work #
Before writing code, it’s important to understand the socket lifecycle and the different roles of servers and clients.
flowchart LR
subgraph Server["TCP LIFECYCLE (Server)"]
S1["socket() (create socket)"] --> S2["bind() (bind to IP:port)"]
S2 --> S3["listen() (start listening)"]
S3 --> S4["accept() (accept client connection)"]
S4 --> S5["recv() (read data from client)"]
S5 --> S6["send() (send response)"]
S6 --> S7["close() (close client connection)"]
end
subgraph Client["TCP LIFECYCLE (Client)"]
C1["socket() (create socket)"] --> C2["connect() (connect to server)"]
C2 --> C3["send() (send data)"]
C3 --> C4["recv() (receive data)"]
C4 --> C5["close() (close connection)"]
end
C2 -. "connect" .-> S4
C3 -. "send data" .-> S5
S6 -. "send response" .-> C4The two main protocols running on top of sockets:
| Feature | TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) |
|---|---|---|
| Connection | Persistent | Connectionless |
| Data Order | Guaranteed | Not guaranteed |
| Delivery | Acknowledged | No acknowledgment |
| Speed | Slower (handshake overhead) | Faster |
| Characteristic | Reliable | Best-effort |
| Use Cases | HTTP, databases, file transfer | Video streaming, online gaming, DNS |
TCP Sockets #
TCP guarantees data arrives in the correct order. A TCP server waits for client connections, then both sides can exchange data until one of them closes the connection.
Basic TCP Server #
import socket
def run_server(host="127.0.0.1", port=65432):
# AF_INET = IPv4, SOCK_STREAM = TCP
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
# SO_REUSEADDR: allow reusing the port after the server restarts
# without this, a restart fails with "Address already in use"
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(5) # maximum queue of 5 connections
print(f"TCP server running at {host}:{port}")
while True:
conn, addr = server.accept()
print(f"Connection from: {addr}")
handle_client(conn, addr)
def handle_client(conn, addr):
with conn:
while True:
data = conn.recv(1024) # read at most 1024 bytes
if not data:
break # the client closed the connection
message = data.decode("utf-8")
print(f"[{addr}] Received: {message}")
reply = f"Echo: {message}"
conn.sendall(reply.encode("utf-8"))
print(f"Connection from {addr} closed.")
if __name__ == "__main__":
run_server()
TCP Client #
import socket
def run_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}")
message_list = ["Hello server!", "This is the second message", "Last message"]
for message in message_list:
client.sendall(message.encode("utf-8"))
data = client.recv(1024)
print(f"Server response: {data.decode('utf-8')}")
if __name__ == "__main__":
run_client()
SO_REUSEADDRis mandatory on the server. Without this option, after the server is stopped, the port stays inTIME_WAITstate for several minutes. When you try to run the server again, you getOSError: [Errno 98] Address already in use. Always addsetsockopt(SOL_SOCKET, SO_REUSEADDR, 1)beforebind().
Multi-Client TCP Server #
The server above can only serve one client at a time — the second client has to wait until the first one finishes. This is the most common anti-pattern in socket programming.
# ANTI-PATTERN: single-threaded server
# the second client is blocked until the first one finishes
while True:
conn, addr = server.accept()
handle_client(conn, addr) # blocks here until finished
# CORRECT: spawn a new thread for each client
import threading
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.daemon = True # thread dies when the main program exits
thread.start()
Complete multi-client TCP server implementation:
import socket
import threading
active_clients = {}
lock = threading.Lock()
def handle_client(conn, addr):
with lock:
active_clients[addr] = conn
print(f"[+] New client: {addr} | Total: {len(active_clients)}")
try:
with conn:
while True:
data = conn.recv(1024)
if not data:
break
message = data.decode("utf-8")
print(f"[{addr}] {message}")
conn.sendall(f"Echo: {message}".encode("utf-8"))
except ConnectionResetError:
print(f"[!] Client {addr} forcibly disconnected.")
finally:
with lock:
active_clients.pop(addr, None)
print(f"[-] Client {addr} left | Remaining: {len(active_clients)}")
def run_server(host="127.0.0.1", port=65432):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen()
print(f"Multi-client server running at {host}:{port}")
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.daemon = True
thread.start()
if __name__ == "__main__":
run_server()
Timeouts and Error Handling #
Network connections can hang indefinitely — a client crashes, the network drops, or the server doesn’t respond. Without a timeout, a program can hang forever.
import socket
def client_with_timeout(host="127.0.0.1", port=65432):
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(5.0) # 5-second timeout for connect, recv, send operations
try:
client.connect((host, port))
client.sendall("Hello!".encode("utf-8"))
data = client.recv(1024)
print(f"Response: {data.decode('utf-8')}")
except socket.timeout:
print("Connection timed out — the server didn't respond within 5 seconds.")
except ConnectionRefusedError:
print("Connection refused — the server isn't running or the port is wrong.")
except OSError as e:
print(f"Network error: {e}")
finally:
client.close()
client_with_timeout()
A timeout can also be set on the server side so idle client connections don’t hang onto resources:
def handle_client_with_timeout(conn, addr):
conn.settimeout(30.0) # the client must send data within 30 seconds
with conn:
try:
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
except socket.timeout:
print(f"[!] Client {addr} idle too long, closing the connection.")
UDP Sockets #
UDP doesn’t establish a connection — the server directly receives packets from anyone, and the client sends directly without a handshake first. Faster, but with no guarantee the data arrives.
UDP Server #
import socket
def udp_server(host="127.0.0.1", port=65433):
# SOCK_DGRAM = UDP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
server.bind((host, port))
print(f"UDP server running at {host}:{port}")
while True:
# recvfrom: receive data + sender address at once
data, sender_addr = server.recvfrom(1024)
message = data.decode("utf-8")
print(f"[UDP] From {sender_addr}: {message}")
# sendto: send to a specific address (no connection needed)
reply = f"Received: {message}"
server.sendto(reply.encode("utf-8"), sender_addr)
if __name__ == "__main__":
udp_server()
UDP Client #
import socket
def udp_client(host="127.0.0.1", port=65433):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
client.settimeout(3.0)
server_addr = (host, port)
message_list = ["Packet 1", "Packet 2", "Packet 3"]
for message in message_list:
client.sendto(message.encode("utf-8"), server_addr)
try:
data, _ = client.recvfrom(1024)
print(f"Response: {data.decode('utf-8')}")
except socket.timeout:
print(f"Timeout — packet '{message}' may have been lost.")
if __name__ == "__main__":
udp_client()
When to Choose TCP vs UDP #
Use TCP when:
✓ Data must arrive complete and in order
✓ Implementing application protocols: HTTP, FTP, SSH, databases
✓ Transferring files or documents
✓ Chat or API communication
Use UDP when:
✓ Speed matters more than reliability
✓ Losing a few packets is tolerable
✓ Real-time audio/video streaming
✓ Online games (player positions, frame states)
✓ DNS queries (small packets, send once)
✓ Broadcasting to many receivers at once
Summary #
- A socket is an abstraction of a network connection endpoint — you open, send, receive, and close it like a file.
- Always add
SO_REUSEADDRbeforebind()on the server so the port can be reused immediately after a restart.- A single-threaded server is an anti-pattern — the second client gets blocked; use
threading.ThreadorThreadPoolExecutorto serve many clients concurrently.- Always set a timeout with
socket.settimeout()— without it, a program can hang forever when the network misbehaves.- TCP for communication needing reliability and data ordering; UDP for speed when losing a few packets is tolerable.
- Use
with socket.socket(...) as s:so the socket is always closed automatically even when an error occurs.- Socket data is bytes, not strings — always
.encode()when sending and.decode()when receiving.