UDP (User Datagram Protocol) is connectionless and provides fast, low-overhead communication. This module covers building UDP servers.
UDP vs TCP Key Differences
| Feature | TCP | UDP |
|---|---|---|
| Connection | Required | Not needed |
| Reliability | Guaranteed | Best effort |
| Ordering | Guaranteed | Not guaranteed |
| Speed | Slower | Faster |
| Use case | Files, web | Gaming, streaming |
Basic UDP Server
import socket
# Create UDP socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Bind to address
server.bind(('localhost', 8080))
print("UDP server listening on port 8080...")
# Receive and send
while True:
# Receive data and client address
data, client_address = server.recvfrom(1024)
print(f"Received from {client_address}: {data.decode()}")
# Send response back
response = f"Echo: {data.decode()}".encode()
server.sendto(response, client_address)
Complete UDP Server Example
import socket
def udp_server(host='localhost', port=8080):
"""Simple UDP echo server."""
# Create UDP socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# Bind to address
server.bind((host, port))
print(f"UDP server listening on {host}:{port}")
while True:
# Receive datagram
data, client_address = server.recvfrom(1024)
print(f"Received from {client_address}: {data.decode()}")
# Process data
response = process_message(data)
# Send response
server.sendto(response, client_address)
print(f"Sent response to {client_address}")
except KeyboardInterrupt:
print("\nShutting down server...")
except Exception as e:
print(f"Error: {e}")
finally:
server.close()
def process_message(data):
"""Process incoming message and generate response."""
try:
message = data.decode('utf-8')
return f"Response: {message.upper()}".encode('utf-8')
except UnicodeDecodeError:
return b"Error: Invalid encoding"
if __name__ == '__main__':
udp_server()
UDP Server with Error Handling
import socket
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def robust_udp_server(host='localhost', port=8080, buffer_size=1024):
"""UDP server with comprehensive error handling."""
server = None
try:
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind
try:
server.bind((host, port))
except OSError as e:
if e.errno == 48: # Address already in use
logger.error(f"Port {port} already in use")
return
raise
logger.info(f"UDP server listening on {host}:{port}")
while True:
try:
# Receive datagram
data, client_address = server.recvfrom(buffer_size)
logger.info(f"Received {len(data)} bytes from {client_address}")
# Process message
try:
response = process_message(data)
# Send response
server.sendto(response, client_address)
logger.info(f"Sent response to {client_address}")
except Exception as e:
logger.error(f"Error processing message: {e}")
error_response = b"Error processing request"
server.sendto(error_response, client_address)
except socket.error as e:
logger.error(f"Socket error: {e}")
continue
except KeyboardInterrupt:
logger.info("Shutting down server...")
break
except Exception as e:
logger.error(f"Fatal error: {e}")
finally:
if server:
server.close()
logger.info("Server closed")
def process_message(data):
"""Process incoming message."""
return data.upper()
if __name__ == '__main__':
robust_udp_server()
Handling Large Datagrams
UDP has a maximum datagram size (typically 65507 bytes for IPv4). For larger data:
Option 1: Split into Multiple Datagrams
def send_large_data(sock, data, address, chunk_size=1024):
"""Send large data in multiple UDP datagrams."""
# Send number of chunks first
num_chunks = (len(data) + chunk_size - 1) // chunk_size
sock.sendto(num_chunks.to_bytes(4, 'big'), address)
# Send chunks
for i in range(num_chunks):
start = i * chunk_size
end = start + chunk_size
chunk = data[start:end]
# Prefix with chunk number
header = i.to_bytes(4, 'big')
sock.sendto(header + chunk, address)
def receive_large_data(sock):
"""Receive large data from multiple UDP datagrams."""
# Receive number of chunks
data, address = sock.recvfrom(4)
num_chunks = int.from_bytes(data, 'big')
# Receive chunks
chunks = {}
while len(chunks) < num_chunks:
data, addr = sock.recvfrom(1024 + 4) # chunk + header
if addr != address:
continue # Ignore data from different source
chunk_num = int.from_bytes(data[:4], 'big')
chunk_data = data[4:]
chunks[chunk_num] = chunk_data
# Reassemble
result = b''.join(chunks[i] for i in range(num_chunks))
return result, address
Note: UDP doesn't guarantee order, so chunk reassembly may need more sophisticated logic.
Option 2: Use TCP for Large Data
For reliable large data transfer, consider TCP instead of UDP.
Stateless Request Handling
UDP servers are naturally stateless:
def stateless_udp_server(host='localhost', port=8080):
"""Stateless UDP server - each request is independent."""
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind((host, port))
print(f"Stateless UDP server on {host}:{port}")
while True:
data, client_address = server.recvfrom(1024)
# Each request is independent - no state tracking
try:
request = data.decode()
response = handle_request(request)
server.sendto(response.encode(), client_address)
except Exception as e:
error_msg = f"Error: {e}".encode()
server.sendto(error_msg, client_address)
def handle_request(request):
"""Handle a single request (stateless)."""
# No memory of previous requests
if request == "TIME":
import datetime
return datetime.datetime.now().isoformat()
elif request.startswith("ECHO "):
return request[5:] # Return everything after "ECHO "
else:
return "Unknown command"
Broadcast Server
UDP supports broadcasting (sending to all devices on network):
def broadcast_server(port=8080):
"""UDP server that can respond to broadcast requests."""
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Enable broadcast receiving
server.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# Bind to all interfaces
server.bind(('', port)) # Empty string = all interfaces
print(f"Broadcast UDP server on port {port}")
while True:
data, client_address = server.recvfrom(1024)
print(f"Received from {client_address}: {data.decode()}")
# Respond to broadcast or unicast
response = b"Server response"
server.sendto(response, client_address)
Timeout Handling
Set timeout to make server responsive to interrupts:
def udp_server_with_timeout(host='localhost', port=8080):
"""UDP server with timeout for graceful shutdown."""
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind((host, port))
server.settimeout(1.0) # 1 second timeout
print(f"UDP server on {host}:{port}")
while True:
try:
data, address = server.recvfrom(1024)
print(f"Received: {data.decode()}")
server.sendto(b"Response", address)
except socket.timeout:
# Timeout is expected, continue loop
continue
except KeyboardInterrupt:
print("\nShutting down...")
break
server.close()
Key Concepts
recvfrom() Returns Address
Unlike TCP recv(), UDP recvfrom() returns both data and sender address:
data, address = server.recvfrom(1024)
# address is tuple: (ip, port)
sendto() Requires Address
UDP sendto() needs destination address (unlike TCP send()):
server.sendto(data, ('192.168.1.100', 8080))
No Connection State
UDP has no connection, so:
- No listen() or accept() needed
- No connection tracking
- Each datagram is independent
- Can receive from any client
Message Boundaries
UDP preserves message boundaries:
- Each recvfrom() gets one complete datagram
- Unlike TCP's byte stream
Common Issues
Issue: Datagram Too Large
Error: Message too long
Solution: Split into smaller datagrams or use TCP
Issue: Lost Datagrams
Problem: UDP doesn't guarantee delivery
Solution: - Implement application-level acknowledgments - Use TCP if reliability needed - Accept loss for real-time apps
Issue: Out-of-Order Delivery
Problem: Datagrams may arrive out of order
Solution: Add sequence numbers to messages
Key Takeaway: UDP servers are simpler than TCP (no connections) but require handling of lost/delayed datagrams. Use UDP when speed and low latency matter more than reliability.