O módulo selectors oferece uma camada portátil para monitorar vários objetos de I/O e descobrir quais estão prontos para leitura ou escrita. Ele escolhe automaticamente a melhor implementação disponível no sistema, como epoll, kqueue, poll ou select. É útil em servidores, proxies, clientes concorrentes, consoles e protocolos com muitas conexões não bloqueantes.
Um selector não executa operações por você. Ele apenas informa readiness. O programa ainda precisa aceitar conexões, ler parcialmente, manter buffers, lidar com EOF, controlar escrita e aplicar timeouts. Para aplicações maiores, asyncio pode oferecer uma abstração mais completa, mas selectors ensina e resolve a camada fundamental.
Crie o selector padrão
DefaultSelector escolhe a implementação recomendada para a plataforma.
import selectors
selector = selectors.DefaultSelector()
Feche o selector no final para liberar descritores internos.
Registre um socket
O socket deve estar em modo não 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)
O evento de leitura no socket de escuta significa que uma conexão pode ser aceita.
select
selector.select(timeout) retorna pares de SelectorKey e máscara de eventos.
eventos = selector.select(timeout=1.0)
for chave, mascara in eventos:
if chave.data is None:
aceitar(chave.fileobj)
else:
atender(chave, mascara)
Um resultado vazio após timeout é normal e permite executar manutenção.
SelectorKey
A chave contém o objeto registrado, descritor, eventos de interesse e dados associados.
Use data para armazenar estado da conexão, como endereço, buffers e fase do protocolo.
Aceite todas as conexões pendentes
Em modo não bloqueante, aceite até receber BlockingIOError.
def aceitar(servidor):
while True:
try:
conexao, endereco = servidor.accept()
except BlockingIOError:
break
conexao.setblocking(False)
estado = Conexao(endereco=endereco, entrada=bytearray(), saida=bytearray())
selector.register(conexao, selectors.EVENT_READ, data=estado)
Aceitar apenas uma conexão pode deixar outras aguardando até o próximo ciclo.
Leia sem bloquear
Um evento de leitura indica que a operação provavelmente avançará.
try:
bloco = sock.recv(4096)
except BlockingIOError:
return
if bloco:
estado.entrada.extend(bloco)
else:
fechar_conexao(sock)
Bytes vazios indicam fechamento ordenado do peer.
TCP é fluxo
Uma leitura não corresponde a uma mensagem. Dados podem chegar divididos ou agrupados.
Implemente framing com delimitador, tamanho prefixado ou formato definido. Consulte socket no Python.
Buffers de entrada
Acumule bytes até existir uma mensagem completa.
estado.entrada.extend(bloco)
while mensagem_completa(estado.entrada):
mensagem = extrair_mensagem(estado.entrada)
processar(mensagem, estado)
Defina limite para impedir que um cliente envie dados sem fim sem completar um frame.
Escrita parcial
send() pode escrever apenas parte do buffer.
if estado.saida:
try:
enviados = sock.send(estado.saida)
except BlockingIOError:
enviados = 0
del estado.saida[:enviados]
Mantenha os bytes restantes para o próximo evento de escrita.
Registre EVENT_WRITE somente quando necessário
Sockets normalmente aparecem como graváveis quase o tempo todo. Manter EVENT_WRITE sempre ativo pode criar busy loop.
Adicione o interesse quando o buffer de saída ficar não vazio e remova quando ele for drenado.
modify
modify() altera eventos e dados de um registro.
eventos = selectors.EVENT_READ
if estado.saida:
eventos |= selectors.EVENT_WRITE
selector.modify(sock, eventos, data=estado)
Centralize essa atualização para manter a máscara consistente.
unregister e close
Remova o objeto do selector antes de fechá-lo.
def fechar_conexao(sock):
try:
selector.unregister(sock)
except Exception:
pass
sock.close()
Capture apenas erros esperados em código real e registre estado inconsistente.
Exceções de conexão
Reset, broken pipe e erros de rede podem surgir durante leitura ou escrita.
Feche a conexão individual e mantenha o loop principal ativo, a menos que o erro afete o selector ou socket de escuta.
Timeouts por conexão
O timeout de select() não é automaticamente um timeout de protocolo.
Registre a última atividade em cada estado e feche conexões que excederem o prazo.
agora = time.monotonic()
for chave in list(selector.get_map().values()):
estado = chave.data
if estado and agora - estado.ultima_atividade > 30:
fechar_conexao(chave.fileobj)
Use relógio monotônico
Deadlines e durações devem usar time.monotonic().
Ajustes no relógio civil não devem prolongar ou encurtar timeouts de conexão.
Backpressure
Se o peer lê devagar, o buffer de saída pode crescer.
Defina um limite, pause a leitura de novas requisições, descarte trabalho ou feche a conexão. Nunca permita crescimento ilimitado.
Fairness
Um cliente com grande volume não deve monopolizar o loop.
Limite bytes, mensagens ou tempo processado por conexão em cada ciclo e volte ao selector.
Trabalho bloqueante
Banco de dados, filesystem lento e CPU pesada bloqueiam todas as conexões se executados no loop.
Envie trabalho a um pool limitado ou use uma arquitetura assíncrona. Ao receber o resultado, acorde o loop de forma segura.
Acorde o selector
Uma thread externa pode precisar notificar o loop. Um padrão portátil usa socketpair() quando disponível ou um pipe compatível.
Registre a ponta de leitura; a thread escreve um byte para interromper a espera.
Threads
Mantenha registros, modificações e fechamento preferencialmente na thread do loop.
Outras threads devem enviar comandos por uma fila e acordar o selector, reduzindo condições de corrida.
Integração com queue
Uma queue.Queue pode transportar comandos do worker para o loop.
O próximo artigo detalhará filas sincronizadas, shutdown e backpressure.
TLS
Sockets TLS não bloqueantes podem precisar alternar entre leitura e escrita durante handshake ou operações.
Trate exceções como SSLWantReadError e SSLWantWriteError ajustando a máscara. Para produção, uma biblioteca de alto nível reduz a complexidade.
Limitações no Windows
A capacidade de selecionar objetos diferentes de sockets varia. Não presuma que pipes e arquivos comuns funcionam como em Unix.
Teste na plataforma de destino e use APIs apropriadas ao Windows quando necessário.
Arquivos regulares
Arquivos comuns geralmente são considerados sempre prontos e não ganham muito com selectors.
Use leitura em chunks ou threads para I/O de arquivo quando necessário.
Sinais e interrupções
A espera pode ser interrompida por sinais conforme plataforma e versão.
Mantenha handlers mínimos e use uma flag ou mecanismo de wakeup para shutdown.
Encerramento gracioso
Pare de aceitar novas conexões, marque conexões existentes para fechar após drenar buffers, aplique deadline e finalize restantes.
Feche registros, socket de escuta, mecanismo de wakeup e selector.
Observabilidade
Registre conexões ativas, bytes, mensagens, tamanho de buffers, duração, timeouts e erros.
Métricas de fila de saída e tempo no loop revelam backpressure e callbacks bloqueantes.
Segurança
Limite conexões por origem, tamanho de frame, buffer, tempo ocioso e taxa de mensagens.
Valide protocolo antes de alocar memória e não use endereço IP sozinho como identidade.
Testes
Teste leitura parcial, escrita parcial, vários frames juntos, EOF, reset, cliente lento, buffers cheios, timeout, wakeup, shutdown, IPv6 e Windows.
Use sockets locais e deadlines amplos em vez de sleeps exatos.
Erros comuns
Os erros mais frequentes são manter EVENT_WRITE sempre ativo, presumir mensagens completas, ignorar send parcial, executar trabalho bloqueante no loop, esquecer unregister, não limitar buffers e modificar o selector concorrentemente sem protocolo.
Conclusão
selectors fornece multiplexação portátil para muitos sockets não bloqueantes. Registre interesses mínimos, mantenha buffers por conexão, aplique timeouts e backpressure e faça cleanup explícito.
Consulte a documentação oficial de selectors, socket no Python e select no Python.







