9.14. Socket dengan asyncio

Panggilan recv() yang memblokir akan membekukan seluruh skrip hingga satu byte tiba. Panggilan accept() yang memblokir hanya melayani satu klien pada satu waktu. Keduanya adalah jenis situasi "menunggu I/O" yang asyncio diciptakan untuk menanganinya. Bab asyncio mencakup event loop, coroutine, dan primitif sinkronisasi; halaman ini mencakup bagian khusus jaringan.

Modul asyncio mengekspos jaringan melalui sejumlah kecil helper yang menerima dan mengembalikan stream -- objek tingkat tinggi yang membungkus socket dan menawarkan versi read dan write yang dapat di-await. Socket yang mendasarinya masih ada; aplikasi hanya tidak menyentuhnya secara langsung.

9.14.1. Klien dengan asyncio

asyncio.open_connection() adalah padanan asyncio dari socket.socket.connect(). Ini membuka koneksi TCP dan mengembalikan dua objek stream: sebuah reader dan 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())

Tiga hal yang perlu diperhatikan:

  • Pengaturan koneksi adalah satu await alih-alih panggilan yang memblokir. Sementara handshake sedang berlangsung, event loop bebas menjalankan coroutine lain.

  • write() menempatkan byte ke dalam buffer keluar; drain() adalah await yang menyerahkan kontrol ke loop hingga byte-byte tersebut benar-benar terkirim melalui jaringan.

  • readline() membaca byte hingga newline tiba. Kelas stream mencakup read() (baca hingga N byte) dan readexactly() (baca tepat N byte) juga, yang menyelesaikan masalah batas pesan TCP tanpa menulis loop framing secara manual.

9.14.2. Server dengan asyncio

asyncio.start_server() adalah padanan asyncio dari rangkaian bind/listen/accept. Ini menerima callback yang akan dijalankan sekali per koneksi masuk, dengan pasangan reader/writer yang sama seperti yang digunakan sisi klien:

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

Setiap koneksi yang diterima menjadi task-nya sendiri yang menjalankan handle. Event loop mendistribusikan di antara mereka secara alami -- satu klien yang lambat tidak dapat memblokir klien lainnya, karena saat menunggu await reader.read(...) loop bebas membuat kemajuan pada setiap koneksi lainnya. Menambahkan sepuluh klien bersamaan tidak memerlukan sepuluh thread; event loop single-threaded yang sama menjalankan semuanya.

Inilah alasan praktis mengapa aplikasi jaringan kamera yang ditulis untuk asyncio jauh lebih skalabel dibanding kode blocking yang setara: gambar server di TCP sockets adalah satu-klien-pada-satu-waktu; yang ini adalah banyak-klien-sekaligus tanpa usaha ekstra.

9.14.3. Pekerjaan bersamaan di samping jaringan

Manfaat besarnya adalah menggabungkan jaringan dengan pekerjaan kamera lainnya dalam loop yang sama. Kamera dapat menangkap bingkai, menjalankan pemrosesan citra, dan melayani protokol jaringan, semuanya diselingi:

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() menjalankan dua coroutine pada event loop yang sama. Sementara kamera tidur di sleep_ms() antar bingkai, server mendapat kesempatan untuk mendistribusikan lalu lintas jaringan. Sementara server menunggu byte berikutnya, kamera mendapat kesempatan untuk menangkap. Keduanya membuat kemajuan pada satu thread MicroPython.

9.14.4. UDP dengan asyncio

Modul asyncio tidak menawarkan stream tingkat tinggi yang sama untuk UDP -- datagram tidak cocok dengan bentuk read/write dari stream. Pendekatan praktis pada kamera adalah menempatkan pekerjaan UDP di coroutine-nya sendiri, mengalihkan socket ke mode non-blocking, dan menyerahkan kontrol ke event loop di antara percobaan baca:

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 diatur ke non-blocking dengan s.setblocking(False), sehingga recvfrom() langsung memunculkan OSError ketika tidak ada datagram yang menunggu alih-alih memblokir seluruh event loop. await asyncio.sleep_ms(10) pada cabang kosong menyerahkan kontrol kembali ke event loop hingga polling berikutnya.

Pengiriman mengikuti bentuk yang sama: sendto() pada socket non-blocking langsung berhasil atau memunculkan pengecualian. Tidak ada sendallto -- datagram UDP bersifat atomik, sehingga setiap pengiriman adalah satu datagram utuh atau tidak sama sekali. Jika buffer pengiriman penuh, langkah yang tepat untuk UDP biasanya adalah membuang datagram dan membiarkan yang berikutnya dikirim pada iterasi loop berikutnya:

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)

Cabang yang gagal jarang terjadi dalam praktiknya. UDP tidak memiliki flow control, sehingga sendto() hampir selalu berhasil pada percobaan pertama; except sebagian besar ada agar gangguan jaringan singkat tidak merusak coroutine.

Bagian Asyncio mencakup pola yang lebih luas untuk menggabungkan I/O blocking ke dalam program asyncio; pola yang sama berlaku langsung untuk socket UDP.

9.14.5. Timeout dan pembatalan

Membungkus panggilan jaringan dalam asyncio.wait_for() memberikan batas waktu:

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

Coroutine yang memakan terlalu banyak waktu juga dapat di-cancel() dari tempat lain. Kedua mekanisme ini dibahas secara detail dalam bab koordinasi; keduanya berlaku tanpa perubahan untuk stream yang dikembalikan oleh asyncio.open_connection() dan asyncio.start_server().

Untuk referensi lengkap Stream (kelas di balik reader dan writer, beserta helper yang digunakan halaman ini), lihat asyncio --- penjadwal I/O asinkron.