Menunggu data dari jaringan adalah pola menunggu paling murni: paket datang kapan pun sesuai kehendak jaringan, dan thread yang memblokir socket hanya duduk menunggu. Tidak heran network I/O adalah kasus terbaik untuk async — satu thread event loop bisa melayani ribuan koneksi yang sama-sama menunggu. Chapter ini membangun pasangan server-klien TCP sungguhan di localhost memakai asyncio.start_server() dan asyncio.open_connection(), melanjutkan dasar socket pada Chapter Socket Programming ke dunia async.

Server Gema dengan start_server #

asyncio.start_server() membuat TCP server di alamat dan port yang diberikan. Untuk setiap klien yang terhubung, asyncio menjalankan sebuah coroutine handler dengan sepasang reader (untuk menerima) dan writer (untuk mengirim). Simpan kode berikut sebagai server_gema.py:

python
import asyncio

async def tangani_klien(reader, writer):
    data = await reader.read(100)
    pesan = data.decode()
    print(f"server menerima: {pesan!r}")
    writer.write(data)
    await writer.drain()
    writer.close()
    await writer.wait_closed()

async def main():
    server = await asyncio.start_server(tangani_klien, "127.0.0.1", 65432)
    print("server gema siap di 127.0.0.1:65432")
    async with server:
        await server.serve_forever()

asyncio.run(main())

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 3 (async def tangani_klien(reader, writer):): handler per klien. Parameter reader membungkus socket untuk menerima data, writer untuk mengirim — keduanya menyediakan method coroutine yang aman dipakai dengan await.
  2. Baris 4 (data = await reader.read(100)): menunggu hingga maksimal 100 byte data masuk. Selama menunggu, event loop bebas melayani klien lain.
  3. Baris 7 (writer.write(data)): mengirim balik data (perilaku “gema”). write() menyangga byte, lalu await writer.drain() menunggu sangga benar-benar terkirim.
  4. Baris 9-10 (writer.close() dan await writer.wait_closed()): menutup koneksi dengan sopan dan menunggu penutupan tuntas — pola cleanup yang kita latih pada Chapter Cancellation dan Timeouts.
  5. Baris 13 (server = await asyncio.start_server(...)): membuat server di 127.0.0.1:65432 (alamat loopback milik mesin sendiri), dengan tangani_klien sebagai handler.
  6. Baris 16 (await server.serve_forever()): menjaga server berjalan, menerima koneksi tanpa henti sampai proses dihentikan.

Jalankan server ini di satu terminal — ia akan menunggu klien:

bash
python3 server_gema.py

Klien dengan open_connection #

Di sisi lain, asyncio.open_connection() menghubungkan server dan mengembalikan pasangan reader, writer yang sama. Simpan sebagai klien_gema.py:

python
import asyncio

async def main():
    reader, writer = await asyncio.open_connection("127.0.0.1", 65432)
    writer.write("Halo dari klien asyncio".encode())
    await writer.drain()
    data = await reader.read(100)
    print(f"klien menerima: {data.decode()!r}")
    writer.close()
    await writer.wait_closed()

asyncio.run(main())

Dengan server tetap berjalan, jalankan klien di terminal kedua:

bash
python3 klien_gema.py

Output pada terminal klien:

text
klien menerima: 'Halo dari klien asyncio'

Output pada terminal server:

text
server gema siap di 127.0.0.1:65432
server menerima: 'Halo dari klien asyncio'

Alur lengkap pertukaran pesannya begini:

Server kemudian berhenti dengan Ctrl+C karena serve_forever() memang dirancang berjalan selamanya. Tekankan satu hal: setiap await di kedua program adalah titik ketika event loop bisa melayani koneksi lain — untuk melayani seribu klien sekaligus, tidak perlu seribu thread.

HTTP Client dan Connection Pooling #

Untuk HTTP, menulis sendiri protokolnya lewat open_connection bukanlah praktik umum. Dua library client async paling populer adalah httpx (dengan httpx.AsyncClient, bisa dipakai bersama asyncio) dan aiohttp (dengan aiohttp.ClientSession). Keduanya sudah dibahas pemakaiannya pada Chapter Menggunakan HTTP Client dan siap dipakai dengan async with serta async for yang kita pelajari di dua chapter sebelumnya.

Satu konsep penting di keduanya: connection pooling. Membuka koneksi TCP baru untuk setiap request itu mahal — tiap koneksi butuh handshake TCP dan (untuk HTTPS) negosiasi TLS yang menunggu bolak-balik jaringan. Client async karena itu menyimpan sekumpulan koneksi yang bisa dipakai ulang:

  • Satu AsyncClient/ClientSession dibuat sekali (biasanya lewat async with),
  • setiap request meminjam koneksi yang idle dari pool,
  • koneksi dikembalikan ke pool setelah selesai, bukan ditutup,
  • dan seluruh pool ditutup bersih saat session ditutup.

Pola ini persis alasan async with pada Chapter Async Context Managers begitu penting: satu blok untuk mengelola hidup sekumpulan resource. Menjalankan banyak request bersamaan lewat satu session dengan asyncio.gather() lalu menutupnya dengan rapi adalah kombinasi standar untuk web scraping dan pemanggilan API massal yang aman.