9.14. Socketit asyncion kanssa

Estävä recv()-kutsu jäädyttää koko skriptin, kunnes tavu saapuu. Estävä accept()-kutsu palvelee vain yhtä asiakasta kerrallaan. Molemmat näistä ovat juuri sitä ”odota I/O:ta” -tilannetta, jonka käsittelyä varten asyncio on olemassa. asyncio-luku käsittelee tapahtumasilmukan, korutiinit ja synkronointiprimitiivit; tämä sivu käsittelee verkkokohtaiset osat.

asyncio-moduuli tarjoaa verkkotoiminnot pienen apufunktiojoukon kautta, jotka ottavat vastaan ja palauttavat stream-objekteja – korkean tason objekteja, jotka kietovat socketin ympärilleen ja tarjoavat await-ttavia versioita lukemisesta ja kirjoittamisesta. Pohjalla oleva socket on edelleen olemassa; sovellus vain ei kosketa siihen suoraan.

9.14.1. Asiakas asyncion kanssa

asyncio.open_connection() on asyncion vastine socket.socket.connect()-metodille. Se avaa TCP-yhteyden ja palauttaa kaksi stream-objektia: lukijan (reader) ja kirjoittajan (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())

Kolme huomionarvoista asiaa:

  • Yhteyden muodostus on yksi await estävän kutsun sijaan. Kun kättely on käynnissä, tapahtumasilmukka on vapaa ajamaan muita korutiineja.

  • write() laittaa tavut lähtevään puskuriin; drain() on se await, joka luovuttaa vuoron silmukalle, kunnes nuo tavut on todella lähetetty verkon yli.

  • readline() lukee tavuja, kunnes rivinvaihto saapuu. Stream-luokka sisältää myös metodit read() (lue enintään N tavua) ja readexactly() (lue tarkalleen N tavua), jotka ratkaisevat TCP:n viestirajaongelman ilman kehystyssilmukoiden kirjoittamista käsin.

9.14.2. Palvelin asyncion kanssa

asyncio.start_server() on asyncion vastine bind/listen/accept-tanssille. Se ottaa takaisinkutsun, joka ajetaan kerran jokaista saapuvaa yhteyttä kohden, samalla lukija/kirjoittaja-parilla kuin asiakaspuoli käyttää:

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())

Jokaisesta hyväksytystä yhteydestä tulee oma handle-funktiota ajava tehtävänsä. Tapahtumasilmukka jakaa vuoroa niiden välillä luonnollisesti – yksi hidas asiakas ei voi estää muita, koska sillä aikaa kun se odottaa kohdassa await reader.read(...), silmukka voi vapaasti edistää jokaista muuta yhteyttä. Kymmenen samanaikaisen asiakkaan lisääminen ei vaadi kymmentä säiettä; sama yksisäikeinen tapahtumasilmukka ohjaa niitä kaikkia.

Tämä on käytännöllinen syy siihen, miksi asynciolle kirjoitetut kameran verkkosovellukset skaalautuvat paljon paremmin kuin vastaava estävä koodi: palvelinkuva sivulla TCP-socketit oli yksi asiakas kerrallaan; tämä on monta asiakasta yhtä aikaa ilman lisävaivaa.

9.14.3. Samanaikainen työ verkkotoimintojen rinnalla

Suuri hyöty on verkkotoimintojen sekoittaminen muun kameran työn kanssa samassa silmukassa. Kamera voi ottaa kehyksen, ajaa kuvankäsittelyn ja palvella verkkoprotokollaa, kaikki lomittain:

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() ajaa kaksi korutiinia samalla tapahtumasilmukalla. Kun kamera nukkuu sleep_ms()-kutsussa kehysten välillä, palvelin saa jakaa verkkoliikennettä. Kun palvelin odottaa seuraavaa tavua, kamera saa ottaa kuvaa. Molemmat edistyvät yhdellä MicroPython-säikeellä.

9.14.4. UDP asyncion kanssa

asyncio-moduuli ei tarjoa samoja korkean tason streameja UDP:lle – datagrammit eivät sovi streamin luku/kirjoitus-muotoon. Käytännöllinen lähestymistapa kameralla on laittaa UDP-työ omaan korutiiniinsa, asettaa socket ei-estävään tilaan ja luovuttaa vuoro tapahtumasilmukalle lukuyritysten välillä:

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 asetetaan ei-estäväksi kutsulla s.setblocking(False), joten recvfrom() nostaa OSError-poikkeuksen välittömästi, kun yhtään datagrammia ei ole odottamassa, sen sijaan että se estäisi koko tapahtumasilmukan. Tyhjän haaran await asyncio.sleep_ms(10) luovuttaa hallinnan takaisin tapahtumasilmukalle seuraavaan kyselyyn asti.

Lähettäminen noudattaa samaa muotoa: sendto() ei-estävällä socketilla joko onnistuu välittömästi tai nostaa poikkeuksen. sendallto-metodia ei ole – UDP-datagrammit ovat atomisia, joten jokainen lähetys on yksi kokonainen datagrammi tai ei mitään. Jos lähetyspuskuri on täynnä, oikea ratkaisu UDP:lle on yleensä pudottaa datagrammi ja antaa seuraavan lähteä ulos seuraavalla silmukan kierroksella:

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)

Epäonnistuva haara on käytännössä harvinainen. UDP:llä ei ole vuonohjausta, joten sendto() onnistuu lähes aina ensimmäisellä yrityksellä; except on olemassa lähinnä sitä varten, ettei lyhyt verkkohäiriö kaada korutiinia.

Asyncio-osio käsittelee laajemmat mallit estävän I/O:n sekoittamiseen asyncio-ohjelmaan; samat mallit pätevät suoraan UDP-socketiin.

9.14.5. Aikakatkaisut ja peruutus

Verkkokutsun kietominen asyncio.wait_for()-funktioon asettaa sille määräajan:

try:
    reply = await asyncio.wait_for(reader.readline(), timeout=2.0)
except asyncio.TimeoutError:
    print("server is slow")

Liian kauan kestävä korutiini voidaan myös cancel()-peruuttaa muualta. Molemmat mekanismit käsitellään yksityiskohtaisesti koordinointiluvussa; ne pätevät muuttumattomina asyncio.open_connection()- ja asyncio.start_server()-funktioiden palauttamiin streameihin.

Täydellinen Stream-viite (lukijoiden ja kirjoittajien takana oleva luokka sekä tämän sivun ohimennen käyttämät apufunktiot) löytyy kohdasta asyncio — asynkroninen I/O-ajastin.