El módulo selectors ofrece una capa portable para monitorizar varios objetos de I/O y descubrir cuáles están listos para lectura o escritura. Selecciona automáticamente una implementación eficiente del sistema, como epoll, kqueue, poll o select. Es útil en servidores, proxies, clientes concurrentes y protocolos con muchas conexiones no bloqueantes.
Un selector no realiza el I/O por ti. Solo informa readiness. El programa todavía debe aceptar conexiones, manejar lecturas y escrituras parciales, mantener buffers, detectar EOF y aplicar timeouts. Para sistemas mayores, asyncio ofrece una abstracción más completa, pero selectors muestra la capa fundamental.
Crea el selector predeterminado
DefaultSelector elige la implementación recomendada para la plataforma.
import selectors
selector = selectors.DefaultSelector()
Ciérralo durante shutdown para liberar descriptores internos.
Registra un socket
El socket debe ser no bloqueante.
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)
selector.register(servidor, selectors.EVENT_READ, data=None)
Read readiness en un socket de escucha significa que probablemente puede aceptarse una conexión.
select
selector.select(timeout) devuelve pares de SelectorKey y máscara de eventos.
eventos = selector.select(timeout=1.0)
for clave, mascara in eventos:
if clave.data is None:
aceptar(clave.fileobj)
else:
atender(clave, mascara)
Un resultado vacío tras timeout es normal y permite mantenimiento.
SelectorKey
La clave contiene el objeto registrado, descriptor, eventos solicitados y datos asociados.
Usa data para guardar dirección, buffers y fase del protocolo.
Acepta todas las conexiones pendientes
En modo no bloqueante, acepta hasta recibir BlockingIOError.
def aceptar(servidor):
while True:
try:
conexion, direccion = servidor.accept()
except BlockingIOError:
break
conexion.setblocking(False)
estado = Conexion(direccion=direccion, entrada=bytearray(), salida=bytearray())
selector.register(conexion, selectors.EVENT_READ, data=estado)
Aceptar solo una puede dejar otras esperando otro ciclo.
Lee sin bloquear
Un evento de lectura indica que la operación probablemente avanzará.
try:
bloque = sock.recv(4096)
except BlockingIOError:
return
if bloque:
estado.entrada.extend(bloque)
else:
cerrar_conexion(sock)
Bytes vacíos indican cierre ordenado del peer.
TCP es un flujo
Una lectura no equivale a un mensaje. Los datos pueden llegar divididos o agrupados.
Implementa framing con delimitador, prefijo de longitud o formato documentado. Consulta socket en Python.
Buffers de entrada
Acumula bytes hasta tener un frame completo.
estado.entrada.extend(bloque)
while mensaje_completo(estado.entrada):
mensaje = extraer_mensaje(estado.entrada)
procesar(mensaje, estado)
Define un límite para impedir datos infinitos sin completar un frame.
Escritura parcial
send() puede escribir solo parte del buffer.
if estado.salida:
try:
enviados = sock.send(estado.salida)
except BlockingIOError:
enviados = 0
del estado.salida[:enviados]
Conserva los bytes restantes para el próximo evento.
Registra EVENT_WRITE solo cuando haga falta
Los sockets suelen aparecer como escribibles casi siempre. Mantener write interest activo puede crear busy loop.
Añádelo cuando el buffer de salida tenga datos y elimínalo al vaciarse.
modify
modify() cambia eventos y datos asociados.
eventos = selectors.EVENT_READ
if estado.salida:
eventos |= selectors.EVENT_WRITE
selector.modify(sock, eventos, data=estado)
Centraliza el cálculo de la máscara.
unregister y close
Retira el objeto del selector antes de cerrarlo.
def cerrar_conexion(sock):
try:
selector.unregister(sock)
except Exception:
pass
sock.close()
En producción, captura errores esperados específicamente.
Errores de conexión
Reset, broken pipe y fallos de red pueden surgir durante lectura o escritura.
Cierra la conexión afectada y conserva el loop salvo que falle el socket de escucha o el selector.
Timeouts por conexión
El timeout de select() no crea automáticamente un timeout de protocolo.
Guarda la última actividad y cierra clientes inactivos.
ahora = time.monotonic()
for clave in list(selector.get_map().values()):
estado = clave.data
if estado and ahora - estado.ultima_actividad > 30:
cerrar_conexion(clave.fileobj)
Reloj monotónico
Deadlines y duraciones deben usar time.monotonic().
Los ajustes del reloj civil no deberían cambiar timeouts.
Backpressure
Si el peer lee lentamente, el buffer de salida puede crecer.
Define límite, pausa requests nuevas, rechaza trabajo o cierra la conexión. Nunca permitas crecimiento ilimitado.
Fairness
Un cliente con mucho volumen no debe monopolizar el loop.
Limita bytes, mensajes o tiempo por conexión en cada ciclo.
Trabajo bloqueante
Base de datos, filesystem lento y CPU pesada bloquean todas las conexiones si se ejecutan en el loop.
Envía el trabajo a un pool limitado o usa arquitectura async. Despierta el loop cuando llegue el resultado.
Despierta el selector
Una thread externa puede notificar al loop mediante socketpair() o un pipe apropiado.
Registra la punta de lectura y escribe un byte para interrumpir la espera.
Threads
Realiza register, modify y close preferiblemente en la thread del loop.
Otras threads pueden enviar comandos por queue y activar el wakeup.
Integración con queue
Una queue.Queue puede transportar comandos de workers al loop.
El siguiente artículo detallará colas sincronizadas y shutdown.
TLS
Los sockets TLS no bloqueantes pueden alternar entre lectura y escritura durante handshake y operaciones.
Gestiona SSLWantReadError y SSLWantWriteError ajustando la máscara. Un framework reduce complejidad.
Limitaciones en Windows
La posibilidad de seleccionar objetos distintos de sockets varía en Windows. Pipes y archivos comunes no funcionan igual que en Unix.
Prueba la plataforma objetivo.
Archivos regulares
Los archivos comunes suelen considerarse siempre listos y ganan poco con selectors.
Usa lectura por chunks o threads para I/O de archivos.
Señales
La espera puede ser interrumpida por signals según sistema y versión.
Mantén handlers mínimos y usa una flag o wakeup para shutdown.
Cierre graceful
Deja de aceptar conexiones, permite drenar buffers, aplica deadline y cierra las restantes.
Cierra registros, socket de escucha, wakeup y selector.
Observabilidad
Registra conexiones activas, bytes, mensajes, tamaños de buffers, duración, timeouts y errores.
Métricas de salida y latencia del loop revelan backpressure y callbacks bloqueantes.
Seguridad
Limita conexiones por origen, tamaño de frame, buffer, inactividad y tasa de mensajes.
Valida campos antes de reservar memoria y no uses IP como única identidad.
Pruebas
Prueba lectura parcial, escritura parcial, varios frames juntos, EOF, reset, cliente lento, buffers llenos, timeout, wakeup, shutdown, IPv6 y Windows.
Usa sockets locales y deadlines amplios.
Errores comunes
Los fallos frecuentes son mantener EVENT_WRITE siempre activo, asumir mensajes completos, ignorar send parcial, ejecutar trabajo bloqueante, olvidar unregister, no limitar buffers y modificar el selector desde varias threads sin protocolo.
Conclusión
selectors ofrece multiplexación portable para muchos sockets no bloqueantes. Registra solo intereses necesarios, mantiene buffers por conexión, aplica timeouts y backpressure y limpia recursos explícitamente.
Consulta la documentación oficial de selectors, socket en Python y select en Python.







