Sockets in Python — TCP, UDP and Threading
Sockets are a software abstraction that enables communication between processes, either on the same machine or on different machines connected over a network. In Python, the socket module (part of the standard library) provides an interface to the operating system’s network APIs.
A socket is identified by the combination of:
- IP address (or hostname) of the machine
- port number on which the process is listening
- transport protocol used (TCP or UDP)
Creating a socket
The socket.socket() function accepts two main arguments:
| Option | Description |
|---|---|
AF_INET | IPv4 address family |
AF_INET6 | IPv6 address family |
SOCK_STREAM | TCP protocol (reliable, byte-stream connection) |
SOCK_DGRAM | UDP protocol (connectionless datagrams) |
import socket
# TCP socket with IPv4 (most common combination)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# UDP socket with IPv4
s_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Default parameters are AF_INET and SOCK_STREAM
s = socket.socket()
Main methods
| Method | Description |
|---|---|
bind(address) | Assigns a local (host, port) address to the socket. Used by servers. |
listen(backlog) | Puts the socket into listening mode. backlog = max queued connections. |
accept() | Accepts an incoming connection. Returns (conn, address). |
connect(address) | Establishes a connection to a server. Used by clients. |
send(data) | Sends data. Warning: may not send all bytes! |
sendall(data) | Sends all data, guaranteeing complete transmission. |
recv(bufsize) | Receives at most bufsize bytes. Returns b'' when the connection closes. |
sendto(data, addr) | Sends data to a specific address (UDP). |
recvfrom(bufsize) | Receives data and returns (data, address) (UDP). |
settimeout(secs) | Sets a timeout in seconds. None = blocking, 0 = non-blocking. |
close() | Closes the socket and releases resources. |
Warning:
send()vssendall()—send()may send fewer bytes than intended if the kernel buffer is full. Always usesendall()to guarantee complete transmission.
TCP Sockets
TCP connection lifecycle
SERVER CLIENT
socket() socket()
bind() |
listen() |
accept() <------- connect() ----+
| |
recv() <---------- sendall() -------+
| |
sendall() ---------> recv() --------+
| |
close() close()
Basic TCP server
import socket
HOST = '127.0.0.1'
PORT = 9000
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_REUSEADDR avoids 'Address already in use' after a quick restart
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f'Server listening on {HOST}:{PORT}')
conn, addr = server.accept()
print(f'Connection accepted from {addr}')
while True:
data = conn.recv(1024)
if not data:
break
print(f'Received: {data.decode()}')
conn.sendall(data) # echo
conn.close()
server.close()
TCP client
import socket
HOST = '127.0.0.1'
PORT = 9000
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
message = 'Hello, I am the client!'
client.sendall(message.encode())
response = client.recv(1024)
print(f'Server response: {response.decode()}')
client.close()
Context manager (recommended approach)
The with statement guarantees automatic socket closure even if an exception occurs:
import socket
HOST = '127.0.0.1'
PORT = 9000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.connect((HOST, PORT))
client.sendall(b'Test message')
response = client.recv(1024)
print(f'Response: {response.decode()}')
# Socket is already closed here automatically
Exception handling
import socket
HOST = '127.0.0.1'
PORT = 9000
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.settimeout(5)
client.connect((HOST, PORT))
client.sendall(b'Test')
response = client.recv(1024)
print(f'Received: {response.decode()}')
except socket.timeout:
print('Error: server did not respond within 5 seconds')
except ConnectionRefusedError:
print(f'Error: no server running on {HOST}:{PORT}')
except OSError as e:
print(f'Network error: {e}')
Multi-client TCP server with Threading
A thread is created for each connecting client, allowing multiple concurrent connections:
import socket
import threading
HOST = '127.0.0.1'
PORT = 9000
def handle_client(conn, addr):
"""Function executed in a separate thread for each client."""
print(f'[+] Connected: {addr}')
try:
conn.sendall(b'Welcome! Type something and press enter.')
while True:
data = conn.recv(1024)
if not data:
break
print(f'[{addr}] Received: {data.decode().strip()}')
conn.sendall(data) # echo
except OSError as e:
print(f'[{addr}] Error: {e}')
finally:
conn.close()
print(f'[-] Disconnected: {addr}')
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f'Server listening on {HOST}:{PORT}')
try:
while True:
conn, addr = server.accept()
t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
t.start()
print(f'Active threads: {threading.active_count() - 1}')
except KeyboardInterrupt:
print('\nServer stopped with Ctrl+C')
finally:
server.close()
Daemon threads
A daemon thread is automatically terminated when the main program finishes:
import threading
t = threading.Thread(target=my_function, daemon=True)
t.start()
# When main exits, t is automatically terminated
UDP Sockets
UDP is a connectionless protocol: data is sent as independent datagrams with no guarantee of delivery or ordering. It is faster than TCP.
TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) | |
|---|---|---|
| Connection | Oriented, reliable | Connectionless |
| Packet order | Guaranteed | Not guaranteed |
| Speed | Slower | Faster |
| Typical use | Web, email, files | Streaming, games, DNS |
UDP server
import socket
HOST = '127.0.0.1'
PORT = 9001
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, addr = server.recvfrom(1024)
print(f'Received from {addr}: {data.decode()}')
server.sendto(data, addr) # echo
UDP client
import socket
HOST = '127.0.0.1'
PORT = 9001
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
message = 'Hello via UDP!'
client.sendto(message.encode(), (HOST, PORT))
response, server_addr = client.recvfrom(1024)
print(f'Response from {server_addr}: {response.decode()}')
Useful options
Host information
import socket
print(socket.gethostname()) # local hostname
print(socket.gethostbyname(socket.gethostname())) # local IP
info = socket.getaddrinfo('www.google.com', 80, socket.AF_INET)
print(info[0][4]) # resolved IP
Timeout and non-blocking mode
s.settimeout(None) # blocking (default)
s.settimeout(5.0) # 5 second timeout
s.setblocking(False) # non-blocking (equivalent to settimeout(0))
Socket options with setsockopt
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # avoids 'Address already in use'
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # detects dropped connections
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # disables Nagle's algorithm
Exercises
Basic level
- Modify the TCP client to send 5 messages in sequence (
'Message 1','Message 2', …) and print each response. - Create a TCP server that always replies with the received message converted to UPPERCASE.
- Test the UDP server by sending three consecutive messages from the client.
Intermediate level
- Create a network calculator: the client sends a string like
'10 + 5'and the server replies with the result. Handle division by zero and invalid operators. - Modify the multi-client server to count connected clients and include the count in the welcome message.
- Implement a timeout in the client: if the server doesn’t respond within 3 seconds, print an error message.
Advanced level
- Create a file server: the client sends a filename and the server responds with its contents.
- Implement a multi-user chat: the server receives messages from each client and broadcasts them to all others.
Summary
| Concept | Remember |
|---|---|
sendall() | Always use instead of send() |
SO_REUSEADDR | Always set on servers |
with statement | Guarantees automatic closure |
| Exceptions | Always handle with try/except |
| Daemon threads | Terminate automatically with the main process |
| TCP vs UDP | TCP = reliable; UDP = fast |
EC