9.14. Sockets com asyncio¶
Uma chamada recv() bloqueante congela todo o script até chegar um byte. Uma chamada accept() bloqueante serve apenas um cliente de cada vez. Ambas são exatamente o tipo de situação de «espera em I/O» que o asyncio existe para gerir. O capítulo asyncio aborda o ciclo de eventos, as corrotinas e os primitivos de sincronização; esta página aborda as partes específicas de rede.
O módulo asyncio expõe as funcionalidades de rede através de um pequeno número de auxiliares que recebem e devolvem streams – objetos de alto nível que encapsulam um socket e oferecem versões com await-able de leitura e escrita. O socket subjacente ainda está lá; a aplicação simplesmente não o utiliza diretamente.
9.14.1. Um cliente com asyncio¶
asyncio.open_connection() é o equivalente asyncio de socket.socket.connect(). Abre uma ligação TCP e devolve dois objetos stream: um reader e um writer
import asyncio
async def client():
reader, writer = await asyncio.open_connection("192.168.1.20", 9000)
writer.write(b"hello\n")
await writer.drain() # wait until bytes have been sent
reply = await reader.readline()
print("reply:", reply)
writer.close()
await writer.wait_closed()
asyncio.run(client())
Três aspetos a notar:
A configuração da ligação é um único
awaitem vez de uma chamada bloqueante. Enquanto o handshake está em curso, o ciclo de eventos está livre para executar outras corrotinas.write()coloca bytes num buffer de saída;drain()é oawaitque cede ao ciclo até que esses bytes tenham sido efetivamente enviados pela rede.readline()lê bytes até chegar uma nova linha. A classe stream inclui tambémread()(ler até N bytes) ereadexactly()(ler exatamente N bytes), que resolvem o problema dos limites de mensagem do TCP sem escrever os ciclos de enquadramento manualmente.
9.14.2. Um servidor com asyncio¶
asyncio.start_server() é o equivalente asyncio da sequência bind/listen/accept. Recebe um callback que será executado uma vez por cada ligação recebida, com o mesmo par reader/writer que o lado cliente utiliza:
import asyncio
async def handle(reader, writer):
addr = writer.get_extra_info("peername")
print("connection from", addr)
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data) # echo back
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle, "0.0.0.0", 9000)
print("listening on", server.sockets[0].getsockname())
async with server:
await server.serve_forever()
asyncio.run(main())
Cada ligação aceite torna-se a sua própria tarefa a executar handle. O ciclo de eventos distribui entre elas naturalmente – um cliente lento não pode bloquear os outros, pois enquanto aguarda em await reader.read(...) o ciclo está livre para progredir em todas as outras ligações. Adicionar dez clientes simultâneos não requer dez threads; o mesmo ciclo de eventos com uma única thread conduz todos eles.
Esta é a razão prática pela qual as aplicações de rede de câmara escritas para asyncio escalam muito melhor do que o código bloqueante equivalente: o exemplo de servidor em Sockets TCP era um-cliente-de-cada-vez; este é muitos-clientes-ao-mesmo-tempo sem esforço adicional.
9.14.3. Trabalho simultâneo a par da rede¶
A grande vantagem é misturar as operações de rede com o restante trabalho da câmara no mesmo ciclo. A câmara pode capturar um fotograma, executar processamento de imagem, e servir um protocolo de rede, tudo intercalado:
import asyncio
async def capture_loop():
while True:
img = await camera.snapshot()
# process img ...
await asyncio.sleep_ms(100)
async def handle(reader, writer):
...
async def main():
server = await asyncio.start_server(handle, "0.0.0.0", 9000)
await asyncio.gather(
server.serve_forever(),
capture_loop(),
)
asyncio.run(main())
asyncio.gather() executa as duas corrotinas no mesmo ciclo de eventos. Enquanto a câmara está em espera em sleep_ms() entre fotogramas, o servidor pode processar tráfego de rede. Enquanto o servidor aguarda pelo próximo byte, a câmara pode capturar. Ambos progridem numa única thread MicroPython.
9.14.4. UDP com asyncio¶
O módulo asyncio não oferece as mesmas streams de alto nível para UDP – os datagramas não se enquadram na forma de leitura/escrita de uma stream. A abordagem prática na câmara é colocar o trabalho UDP na sua própria corrotina, colocar o socket em modo não bloqueante, e ceder ao ciclo de eventos entre tentativas de leitura:
import asyncio
import socket
async def udp_listener(port):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setblocking(False)
s.bind(("0.0.0.0", port))
while True:
try:
data, src = s.recvfrom(1024)
except OSError:
await asyncio.sleep_ms(10)
continue
print("got", data, "from", src)
O socket é colocado em modo não bloqueante com s.setblocking(False), pelo que recvfrom() levanta OSError imediatamente quando não há datagramas à espera, em vez de bloquear todo o ciclo de eventos. O await asyncio.sleep_ms(10) no ramo vazio devolve o controlo ao ciclo de eventos até à próxima sondagem.
O envio segue a mesma forma: sendto() num socket não bloqueante ou tem sucesso imediatamente ou levanta uma exceção. Não existe sendallto – os datagramas UDP são atómicos, pelo que cada envio é um datagrama completo ou nenhum. Se o buffer de envio estiver cheio, a ação correta para UDP é normalmente descartar o datagrama e deixar o seguinte sair na próxima iteração do ciclo:
async def udp_telemetry(target_addr, period_ms):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setblocking(False)
while True:
payload = collect_telemetry()
try:
s.sendto(payload, target_addr)
except OSError:
pass # buffer full -- skip this one
await asyncio.sleep_ms(period_ms)
O ramo de falha é raro na prática. O UDP não tem controlo de fluxo, pelo que sendto() quase sempre tem sucesso na primeira tentativa; o except existe principalmente para que uma breve falha de rede não cause a falha da corrotina.
A secção Asyncio aborda os padrões mais amplos para misturar I/O bloqueante num programa asyncio; os mesmos padrões aplicam-se diretamente a um socket UDP.
9.14.5. Timeouts e cancelamento¶
Envolver uma chamada de rede em asyncio.wait_for() define um prazo para ela:
try:
reply = await asyncio.wait_for(reader.readline(), timeout=2.0)
except asyncio.TimeoutError:
print("server is slow")
Uma corrotina que está a demorar demasiado pode também ser cancel()-ada de outro local. Ambos os mecanismos são abordados em detalhe no capítulo de coordenação; aplicam-se sem alterações às streams devolvidas por asyncio.open_connection() e asyncio.start_server().
Para a referência completa de Stream (a classe por trás dos readers e writers, mais os auxiliares que esta página utilizou), consulte asyncio — escalonador de I/O assíncrono.