El módulo socketserver simplifica las partes repetitivas de un servidor de red. En lugar de crear un socket, hacer bind, escuchar, aceptar clientes y construir manualmente un handler para cada solicitud, eliges una clase de servidor e implementas el método que procesa la petición.
Esta abstracción resulta útil en servicios internos, servidores de prueba, protocolos pequeños, agentes locales y aprendizaje. Aun así, todo servidor necesita framing, límites de tamaño, timeouts, una política de concurrencia, autenticación y apagado predecible. Esta guía cubre TCPServer, UDPServer, StreamRequestHandler, servidores con hilos y límites de producción.
Cómo se organiza socketserver
Las cuatro clases concretas básicas son TCPServer, UDPServer, UnixStreamServer y UnixDatagramServer. Las variantes Unix no están disponibles en todas las plataformas. Por defecto, estas clases procesan una solicitud cada vez.
La concurrencia se añade con ThreadingMixIn o ForkingMixIn. También existen clases listas como ThreadingTCPServer y ThreadingUDPServer. El modelo fork depende de POSIX y separa la memoria; los hilos comparten estado y requieren sincronización.
Antes de avanzar, revisa la guía del cliente TCP con sockets en Python. socketserver organiza el servidor, pero no cambia las propiedades de TCP o UDP.
Primer servidor TCP con StreamRequestHandler
StreamRequestHandler expone rfile y wfile como archivos binarios con buffer. El ejemplo usa un salto de línea como delimitador del mensaje.
import socketserver
MAX_LINE = 10_000
class EchoHandler(socketserver.StreamRequestHandler):
def handle(self) -> None:
line = self.rfile.readline(MAX_LINE + 1)
if len(line) > MAX_LINE:
self.wfile.write(b"error: mensaje demasiado grande\n")
return
if not line.endswith(b"\n"):
self.wfile.write(b"error: mensaje incompleto\n")
return
response = line.rstrip(b"\r\n").upper() + b"\n"
self.wfile.write(response)
HOST, PORT = "127.0.0.1", 9000
with socketserver.TCPServer((HOST, PORT), EchoHandler) as server:
server.serve_forever()
El límite de lectura es imprescindible. Sin él, un cliente puede enviar datos indefinidamente sin completar la línea y consumir memoria. Si el protocolo usa texto, define encoding y manejo de errores explícitamente.
TCP es un flujo
Una llamada sendall() del cliente no corresponde necesariamente a una llamada recv() del servidor. Los bytes pueden llegar divididos o agrupados. Cada protocolo debe definir framing: delimitador, prefijo de longitud, registro fijo o cierre de conexión.
La guía de struct y datos binarios en Python muestra cabeceras con longitud explícita. Para protocolos textuales pequeños, una línea limitada suele bastar.
Servidor concurrente con hilos
TCPServer procesa un cliente cada vez. Un cliente lento puede bloquear a los demás. Para conexiones orientadas a I/O, usa ThreadingTCPServer.
class SafeThreadingTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = False
block_on_close = True
with SafeThreadingTCPServer((HOST, PORT), EchoHandler) as server:
server.serve_forever()
Con daemon_threads=False, Python espera a los handlers activos antes de salir. block_on_close=True mantiene una limpieza predecible. Si eliges hilos daemon, documenta que pueden interrumpirse durante el apagado.
La guía de threading en Python explica locks y condiciones de carrera. El GIL no protege automáticamente operaciones compuestas sobre estado compartido.
Estado compartido y locks
Cada petición crea una nueva instancia del handler, pero todas acceden a self.server. Protege actualizaciones compuestas a contadores o cachés.
import threading
class CountingServer(socketserver.ThreadingTCPServer):
daemon_threads = False
def __init__(self, address, handler):
super().__init__(address, handler)
self.total_requests = 0
self.counter_lock = threading.Lock()
class CountingHandler(socketserver.StreamRequestHandler):
def handle(self) -> None:
with self.server.counter_lock:
self.server.total_requests += 1
current = self.server.total_requests
self.wfile.write(f"solicitud {current}\n".encode("utf-8"))
No mantengas sesiones críticas solo en memoria si habrá procesos, múltiples instancias o reinicios. Usa almacenamiento externo cuando el estado deba sobrevivir o compartirse.
Timeout por conexión
Los clientes lentos pueden ocupar hilos indefinidamente. Configura un timeout en el socket aceptado desde setup().
class TimedHandler(socketserver.StreamRequestHandler):
timeout_seconds = 10
def setup(self) -> None:
super().setup()
self.request.settimeout(self.timeout_seconds)
def handle(self) -> None:
try:
line = self.rfile.readline(4097)
except TimeoutError:
return
if not line or len(line) > 4096:
return
self.wfile.write(b"ok\n")
El atributo server.timeout afecta handle_request(); no impone automáticamente timeout a cada cliente ni al bucle de serve_forever().
Backpressure y cola de conexiones
request_queue_size controla aproximadamente cuántas conexiones esperan mientras el servidor está ocupado. Aumentarlo no limita la concurrencia; solo cambia dónde esperan los clientes.
class LimitedServer(socketserver.ThreadingTCPServer):
request_queue_size = 32
allow_reuse_address = True
ThreadingTCPServer crea un hilo por solicitud y no ofrece un pool fijo. Un servicio público necesita límites del sistema, firewall, timeouts, control de conexiones y, a menudo, proxy o arquitectura orientada a eventos.
Filtrar solicitudes
verify_request() permite rechazar un cliente antes del handler.
import ipaddress
ALLOWED = ipaddress.ip_network("10.10.0.0/16")
class InternalServer(socketserver.ThreadingTCPServer):
def verify_request(self, request, client_address) -> bool:
client_ip = ipaddress.ip_address(client_address[0])
return client_ip in ALLOWED
Una allowlist por IP es solo una capa. Los proxies modifican la dirección observada y una red interna puede estar comprometida. Para autenticación fuerte, usa TLS, HMAC o credenciales del protocolo. Consulta la guía de ssl y TLS en Python.
Servidor UDP
UDP procesa datagramas independientes que pueden perderse, duplicarse o llegar desordenados.
class UDPHandler(socketserver.BaseRequestHandler):
def handle(self) -> None:
data, udp_socket = self.request
if len(data) > 1024:
return
response = data.strip().upper()
udp_socket.sendto(response, self.client_address)
with socketserver.ThreadingUDPServer((HOST, 9001), UDPHandler) as server:
server.serve_forever()
No uses la dirección de origen como autenticación. Evita respuestas mucho mayores que la petición para prevenir amplificación. Los protocolos importantes necesitan IDs y protección contra replay.
Manejo de errores
Por defecto, una excepción en handle() imprime un traceback y el servidor continúa. Sobrescribe handle_error() para logs estructurados sin exponer payloads.
import logging
logger = logging.getLogger(__name__)
class LoggedServer(socketserver.ThreadingTCPServer):
def handle_error(self, request, client_address) -> None:
logger.exception(
"fallo al procesar cliente",
extra={"client_ip": client_address[0]},
)
Registra metadatos e identificadores de correlación, no tokens ni cuerpos completos.
Apagado correcto
serve_forever() se detiene tras llamar a shutdown(). Esa llamada debe realizarse desde otro hilo; hacerlo desde el mismo hilo del bucle produce deadlock.
import threading
server = SafeThreadingTCPServer((HOST, PORT), EchoHandler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
try:
thread.join()
except KeyboardInterrupt:
server.shutdown()
server.server_close()
thread.join()
En producción, deja de aceptar trabajo, espera las solicitudes activas hasta un plazo y libera los recursos.
IPv6
import socket
class IPv6TCPServer(socketserver.ThreadingTCPServer):
address_family = socket.AF_INET6
El comportamiento dual stack varía entre sistemas. Prueba direcciones IPv4 mapeadas y normaliza los clientes antes de aplicar políticas.
Cuándo usar socketserver
Es apropiado para protocolos pequeños, herramientas internas, agentes locales, mocks y aprendizaje. Para HTTP público usa un framework y servidor WSGI o ASGI. Para miles de conexiones duraderas, considera asyncio. Para CPU intensiva, procesos limitados y colas externas ofrecen mejor aislamiento.
Errores frecuentes
Los fallos habituales son leer sin límite, asumir que un receive equivale a un mensaje, usar el servidor síncrono con clientes lentos, modificar estado sin lock, crear hilos ilimitados, omitir timeouts, confiar solo en IP, llamar shutdown() desde el hilo incorrecto y exponer un servidor de demostración a internet.
Buenas prácticas
Define framing, tamaños máximos y timeouts. Elige conscientemente entre sincronía, hilos, procesos o event loop. Protege estado compartido. Añade TLS y autenticación. Implementa logs seguros, métricas y apagado probado. Coloca servicios públicos detrás de controles y límites adicionales.
Conclusión
socketserver elimina mucho código repetitivo de servidores TCP y UDP, pero no elimina las responsabilidades del protocolo. Framing, límites, concurrencia, autenticación y apagado siguen siendo esenciales. Es claro y eficaz para servicios controlados; cargas públicas necesitan pools, event loops e infraestructura de producción.
Consulta la documentación oficial de socketserver y el RFC 9293 sobre TCP.







