9.14. Sockety s asyncio¶
Blokující volání recv() zmrazí celý skript, dokud nedorazí byte. Blokující volání accept() obsluhuje vždy jen jednoho klienta. Obě tyto situace jsou přesně tím druhem „čekání na I/O“, k jehož řešení asyncio existuje. Kapitola o asyncio pokrývá smyčku událostí, korutiny a synchronizační primitiva; tato stránka pokrývá části specifické pro síť.
Modul asyncio zpřístupňuje síťování prostřednictvím malého počtu pomocníků, kteří přijímají a vracejí streamy – objekty vysoké úrovně, které obalují socket a nabízejí await-ovatelné verze čtení a zápisu. Podkladový socket tam stále je; aplikace se ho jen přímo nedotýká.
9.14.1. Klient s asyncio¶
asyncio.open_connection() je protějškem socket.socket.connect() v asyncio. Otevře TCP spojení a vrátí dva objekty streamu: reader a 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())
Tři věci, kterých si všimnout:
Navázání spojení je jediné
awaitmísto blokujícího volání. Zatímco handshake probíhá, smyčka událostí může spouštět jiné korutiny.write()vkládá byty do odchozího bufferu;drain()je onoawait, které předá řízení smyčce, dokud tyto byty nejsou skutečně odeslány po síti.readline()čte byty, dokud nedorazí znak nového řádku. Třída streamu zahrnuje takéread()(přečte až N bytů) areadexactly()(přečte přesně N bytů), které řeší problém hranic zpráv v TCP, aniž byste museli rámovací smyčky psát ručně.
9.14.2. Server s asyncio¶
asyncio.start_server() je protějškem tance bind/listen/accept v asyncio. Přijímá callback, který se spustí jednou pro každé příchozí spojení, se stejnou dvojicí reader/writer, jakou používá klientská strana:
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())
Každé přijaté spojení se stává vlastní úlohou spouštějící handle. Smyčka událostí mezi nimi přirozeně přepíná – jeden pomalý klient nemůže blokovat ostatní, protože zatímco čeká na await reader.read(...), smyčka může pokračovat v práci na každém dalším spojení. Přidání deseti souběžných klientů nevyžaduje deset vláken; všechny pohání tatáž jednovláknová smyčka událostí.
To je praktický důvod, proč se kamerové síťové aplikace napsané pro asyncio škálují mnohem lépe než ekvivalentní blokující kód: obrázek serveru na TCP sokety zvládal jednoho klienta naráz; tento zvládá mnoho klientů naráz bez jakéhokoli úsilí navíc.
9.14.3. Souběžná práce vedle síťování¶
Velkou výhodou je míchání síťování se zbytkem práce kamery ve stejné smyčce. Kamera může pořizovat snímek, provádět zpracování obrazu a obsluhovat síťový protokol, vše prokládaně:
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() spouští obě korutiny na stejné smyčce událostí. Zatímco kamera spí v sleep_ms() mezi snímky, server dostává prostor odbavovat síťový provoz. Zatímco server čeká na další byte, kamera dostává prostor pořizovat snímek. Oba postupují vpřed na jediném vláknu MicroPythonu.
9.14.4. UDP s asyncio¶
Modul asyncio nenabízí stejné streamy vysoké úrovně pro UDP – datagramy se nehodí do podoby čtení/zápisu streamu. Praktický přístup na kameře je umístit práci s UDP do vlastní korutiny, přepnout socket do neblokujícího režimu a mezi pokusy o čtení předávat řízení smyčce událostí:
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)
Socket je nastaven jako neblokující pomocí s.setblocking(False), takže recvfrom() vyvolá OSError okamžitě, když žádný datagram nečeká, místo aby blokoval celou smyčku událostí. await asyncio.sleep_ms(10) v prázdné větvi předá řízení zpět smyčce událostí až do dalšího dotazu.
Odesílání má stejnou podobu: sendto() na neblokujícím socketu buď okamžitě uspěje, nebo vyvolá výjimku. Neexistuje žádné sendallto – UDP datagramy jsou atomické, takže každé odeslání je buď celý datagram, nebo žádný. Pokud je odesílací buffer plný, správným krokem u UDP je obvykle datagram zahodit a nechat ten další odejít při příštím průchodu smyčkou:
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)
Selhávající větev je v praxi vzácná. UDP nemá řízení toku, takže sendto() téměř vždy uspěje na první pokus; except existuje hlavně proto, aby krátkodobá síťová porucha korutinu neshodila.
Sekce Asyncio pokrývá širší vzory pro míchání blokujícího I/O do programu s asyncio; stejné vzory platí přímo i pro UDP socket.
9.14.5. Časové limity a zrušení¶
Obalení síťového volání do asyncio.wait_for() mu nastaví časový limit:
try:
reply = await asyncio.wait_for(reader.readline(), timeout=2.0)
except asyncio.TimeoutError:
print("server is slow")
Korutinu, která trvá příliš dlouho, lze také cancel()-novat odjinud. Oba mechanismy jsou podrobně popsány v kapitole o koordinaci; platí beze změny pro streamy vracené asyncio.open_connection() a asyncio.start_server().
Úplnou referenci Stream (třída za readery a writery plus pomocníci, které tato stránka mimochodem použila) najdete v asyncio — asynchronní I/O plánovač.