El módulo select permite esperar hasta que sockets, pipes y otros descriptores estén listos para lectura, escritura o condiciones excepcionales. Esta técnica, llamada multiplexación de I/O, permite que un solo thread coordine muchas conexiones sin bloquearse en una sola.
Las primitivas cambian por sistema operativo: select() está ampliamente disponible, poll() aparece en Unix, epoll() es específico de Linux, kqueue() pertenece a BSD y macOS y /dev/poll existe en Solaris. Para aplicaciones portables, normalmente se prefiere selectors, que elige una implementación eficiente.
Disponibilidad no significa finalización
Un socket legible tiene alguna condición: datos, EOF o error. No garantiza que haya llegado un mensaje completo. Un socket escribible puede aceptar alguna salida, pero quizá solo una parte del buffer pendiente.
Los protocolos necesitan buffers de entrada y salida. Nunca supongas que un recv() devuelve un mensaje entero o que un send() transmite todos los bytes.
Servidor básico con select()
import select
import socket
servidor = socket.socket()
servidor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servidor.bind(("127.0.0.1", 9000))
servidor.listen()
servidor.setblocking(False)
lecturas = [servidor]
while True:
listos, _, excepciones = select.select(lecturas, [], lecturas, 1.0)
for sock in listos:
if sock is servidor:
cliente, direccion = servidor.accept()
cliente.setblocking(False)
lecturas.append(cliente)
else:
datos = sock.recv(4096)
if datos:
print(datos)
else:
lecturas.remove(sock)
sock.close()
for sock in excepciones:
if sock in lecturas:
lecturas.remove(sock)
sock.close()
Es un ejemplo didáctico. Un servidor real necesita framing, buffers limitados, colas de salida, autenticación, timeouts y manejo robusto de excepciones. Consulta socketserver en Python para una abstracción de mayor nivel.
Las tres listas
La primera lista contiene objetos esperados para lectura. La segunda, objetos esperados para escritura. La tercera representa condiciones excepcionales definidas por la plataforma. Cada entrada puede ser un file descriptor entero o un objeto con fileno().
El resultado contiene tres subconjuntos listos. Un timeout None espera sin límite; cero consulta sin bloquear; un float positivo limita la espera en segundos.
Diferencias en Windows
En Windows, select() funciona solo con sockets de WinSock. Archivos normales, stdin y pipes ordinarios no se monitorean como en Unix. Programas portables pueden necesitar threads, asyncio o APIs específicas para fuentes que no sean sockets.
El módulo no está disponible en WASI.
Por qué selectors suele ser mejor
selectors.DefaultSelector elige epoll, kqueue, poll o select según la plataforma. Su modelo permite asociar datos a cada registro y oculta diferencias de bitmasks.
Usa select directamente cuando necesites flags del kernel, edge-trigger, eventos de procesos o archivos de kqueue o integración con código de bajo nivel existente.
Lecturas parciales y framing
TCP proporciona un flujo de bytes, no límites de mensajes. La aplicación puede definir delimitador, longitud prefijada, tamaño fijo o formato estructurado.
buffers = {}
def recibir(sock):
bloque = sock.recv(4096)
if not bloque:
return False
buffer = buffers.setdefault(sock, bytearray())
buffer.extend(bloque)
while b"\n" in buffer:
linea, _, restante = buffer.partition(b"\n")
buffer[:] = restante
procesar_linea(linea)
return True
Define un tamaño máximo. Un cliente que nunca envía delimitador no debe consumir memoria indefinidamente.
Escrituras parciales y backpressure
Monitorea escritura solo mientras exista salida pendiente. Los sockets conectados suelen estar escribibles, así que registrarlos permanentemente puede generar busy loop.
salidas = {}
def enviar_pendiente(sock):
buffer = salidas.get(sock)
if not buffer:
return False
enviados = sock.send(buffer)
del buffer[:enviados]
return bool(buffer)
Limita cada cola. Si un cliente lee lentamente, pausa productores, rechaza más datos, descarta mensajes según política o cierra la conexión. Ese control evita que un cliente agote la memoria.
EOF, half-close y errores
recv() devolviendo b"" indica que el peer cerró su lado de escritura. La aplicación quizá todavía tenga salida pendiente. poll() y epoll() exponen flags como POLLHUP, EPOLLHUP y EPOLLRDHUP.
Procesa errores y hangups aunque también exista lectura. Puede haber datos finales antes del cierre.
poll()
poll() registra descriptores con máscaras como POLLIN, POLLOUT, POLLERR, POLLHUP y POLLNVAL.
import select
poller = select.poll()
poller.register(sock, select.POLLIN)
for fd, evento in poller.poll(1000):
if evento & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
cerrar_fd(fd)
elif evento & select.POLLIN:
leer_fd(fd)
El timeout está en milisegundos. Mantén un mapping entre números y objetos de conexión.
epoll en Linux
epoll() escala bien con muchos descriptores y admite level-triggered y edge-triggered. En level-triggered el evento continúa mientras la condición sea verdadera. Con EPOLLET, las notificaciones indican cambios de estado.
Edge-trigger exige sockets no bloqueantes y leer o escribir hasta EAGAIN o EWOULDBLOCK. Parar antes puede dejar datos sin una nueva notificación.
import errno
import select
with select.epoll() as ep:
ep.register(sock.fileno(), select.EPOLLIN | select.EPOLLET)
for fd, evento in ep.poll(1.0):
if evento & select.EPOLLIN:
while True:
try:
bloque = descriptores[fd].recv(65536)
except BlockingIOError as exc:
if exc.errno in {errno.EAGAIN, errno.EWOULDBLOCK}:
break
raise
if not bloque:
cerrar_fd(fd)
break
procesar(bloque)
errno en Python explica esos códigos no bloqueantes.
EPOLLONESHOT y EPOLLEXCLUSIVE
EPOLLONESHOT desactiva un descriptor después de un evento hasta rearmarlo con modify(). Puede evitar que varios workers procesen la misma conexión. EPOLLEXCLUSIVE reduce el thundering herd cuando varios epoll esperan el mismo descriptor.
Son opciones específicas de Linux que requieren tests con los kernels usados.
kqueue
kqueue() monitorea I/O, procesos, señales, timers y eventos de vnode. Los cambios se representan mediante kevent. Flags como KQ_EV_ADD, KQ_EV_DELETE, KQ_EV_ONESHOT y KQ_EV_CLEAR controlan registros.
BSD y macOS pueden informar rename, delete, write y cambios de atributos en archivos. Ese mecanismo no es portable a Linux o Windows.
Integración con señales
El artículo anterior, signal en Python, presentó set_wakeup_fd(). Un pipe o socketpair no bloqueante puede despertar al selector cuando llega SIGTERM o SIGINT.
import os
import select
import signal
rfd, wfd = os.pipe()
os.set_blocking(rfd, False)
os.set_blocking(wfd, False)
signal.set_wakeup_fd(wfd)
signal.signal(signal.SIGTERM, lambda signum, frame: None)
while True:
listos, _, _ = select.select([rfd, servidor], [], [], None)
if rfd in listos:
os.read(rfd, 4096)
break
Drena el descriptor, restaura la configuración anterior y cierra ambos extremos. El handler permanece mínimo y el loop normal realiza el shutdown.
Timeouts y reloj monotónico
Los timeouts sirven para expiración, mantenimiento y deadlines. Calcula plazos con reloj monotónico.
import time
deadline = time.monotonic() + 30
while True:
restante = max(0, deadline - time.monotonic())
listos, _, _ = select.select(lecturas, [], [], restante)
if time.monotonic() >= deadline:
expirar_conexiones()
Descriptores cerrados y reutilizados
El sistema puede reutilizar números de descriptor. Desregistra antes de cerrar y elimina todo estado asociado. Un evento atrasado no debe aplicarse a una conexión nueva con el mismo número.
En epoll, unregister() puede mostrar EBADF después de un cierre prematuro. Centraliza el orden de limpieza y hazlo idempotente.
PIPE_BUF
En Unix, select.PIPE_BUF indica el mínimo que POSIX garantiza escribir atómicamente en un pipe marcado como escribible. No aplica a sockets ni garantiza tamaños arbitrarios.
Fairness
Un descriptor siempre activo puede monopolizar el loop. Limita bytes o mensajes procesados por evento y vuelve al multiplexor.
Mueve trabajo pesado de CPU a workers, procesos o executor. El thread de I/O debe mantenerse responsivo.
Seguridad y límites
Limita conexiones, bytes por cliente, tamaño de frame, cola de salida, tiempo ocioso, tasa y CPU. Multiplexar miles de sockets no crea memoria ilimitada.
TLS no bloqueante requiere cuidado: handshake y operaciones SSL pueden necesitar alternar lectura y escritura. Consulta ssl en Python.
Pruebas recomendadas
Prueba entrada fragmentada, salida parcial, cliente lento, cierre durante envío, half-close, timeout, señales durante espera, descriptor inválido, muchos sockets, backpressure, edge-trigger no drenado y diferencias Windows/Unix.
Usa socket.socketpair() y buffers pequeños para forzar operaciones parciales. Verifica que no haya busy loop ni crecimiento ilimitado.
Errores comunes
Los fallos frecuentes son tratar disponibilidad como mensaje completo, monitorear escritura siempre, ignorar envíos parciales, permitir buffers ilimitados, usar edge-trigger sin drenar hasta EAGAIN, cerrar antes de desregistrar, asumir archivos en Windows y ejecutar CPU pesada en el loop.
Conclusión
select da acceso directo a la multiplexación del sistema. Puede coordinar muchos descriptores con pocos threads si la aplicación gestiona buffers, backpressure, cierre y diferencias de plataforma.
Prefiere selectors para portabilidad, usa primitivas específicas solo cuando sean necesarias y mantén I/O no bloqueante. Consulta la documentación oficial de select y la documentación oficial de selectors.







