Topik ini memperkenalkan lapisan ekosistem yang menyusun aplikasi async nyata: axum sebagai kerangka web, reqwest sebagai klien HTTP, hyper dan tower sebagai lapisan di bawahnya, serta pola async fn dalam trait yang menjadi bagian bahasa sejak Rust 1.75. Contoh axum dan reqwest tidak dapat dijalankan pada sandbox karena membutuhkan server eksternal dan pengunduhan crate tambahan, sehingga keduanya dibahas sebagai fragmen bertanda tidak-dijalankan, sementara async trait dibuktikan dengan program kecil yang hanya memakai pustaka standar. Setelah mengikuti topik ini, peran tiap crate dapat dipetakan dengan tepat saat membaca dependensi sebuah proyek async.
Peta lapisan ekosistem#
Ekosistem async Rust tersusun berlapis, dan setiap lapisan hanya mengandalkan lapisan di bawahnya. Bahasa menyediakan trait Future beserta sintaks async/await, tokio menjalankan future dengan scheduler dan driver I/O, hyper membangun protokol HTTP di atas soket tokio, tower merumuskan middleware sebagai trait async, lalu axum dan reqwest menyediakan API tingkat aplikasi di puncak tumpukan itu. Peran tiap lapisan dirinci oleh diagram dan tabel berikut.
| Crate | peran | posisi pada tumpukan |
|---|---|---|
| tokio | runtime: scheduler, driver I/O, timer | fondasi eksekusi |
| hyper | implementasi protokol HTTP tingkat rendah | di atas tokio |
| tower | abstraksi layanan dan middleware | di atas hyper dan tokio |
| axum | kerangka web dengan router dan penangan berbasis async fn | di atas tower dan hyper |
| reqwest | klien HTTP tingkat tinggi dengan API async | di atas hyper |
| futures | kumpulan utilitas future dan stream | di atas std |
Struktur berlapis ini menjelaskan dua kebiasaan yang sering ditemui. Pertama, hampir semua crate ekosistem async memilih satu runtime (umumnya tokio) sehingga future yang dibuatnya dapat dijalankan langsung tanpa penyesuaian. Kedua, fitur pada Cargo.toml tokio menentukan komponen yang ikut terpasang, misalnya net diperlukan hyper sedangkan fs tidak, sehingga aplikasi web dan program CLI dapat memilih kombinasi yang berbeda dari runtime yang sama.
axum: kerangka web di atas tokio#
Axum memetakan alamat HTTP ke fungsi penangan yang berupa async fn biasa. Router dibangun dengan merangkai pemanggilan route, setiap penangan menerima masukan yang disebut extractor (misalnya Path untuk parameter alamat atau Json untuk isi permintaan), dan nilai kembaliannya dijadikan respons. Karena penangan adalah async fn, penantian basis data atau layanan hilir berupa titik await yang tidak memblokir worker thread, sehingga satu proses melayani banyak permintaan serentak. Contoh berikut membangun dua rute sederhana pada satu router.
// tidak-dijalankan
use axum::{routing::get, Router};
async fn halo() -> &'static str {
"halo dari axum"
}
async fn suhu() -> &'static str {
"26 derajat"
}
#[tokio::main]
async fn main() {
let aplikasi = Router::new()
.route("/", get(halo))
.route("/suhu", get(suhu));
let pendengar = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(pendengar, aplikasi).await.unwrap();
}Fragmen ini membutuhkan dependensi axum = "0.8" dan tokio dengan fitur lengkap, lalu dijalankan dengan cargo run dan diuji dengan permintaan HTTP ke 127.0.0.1:3000. Fungsi axum::serve menerima pendengar TCP milik tokio sehingga seluruh siklus koneksi, pembacaan permintaan, dan penulisan respons berlangsung pada driver I/O runtime yang sama. Dukungan state bersama disediakan lewat Router::with_state, dan middleware seperti pencatatan permintaan ditambahkan lewat layer dari tower.
reqwest: klien HTTP#
Reqwest menyediakan API klien HTTP di atas hyper: satu panggilan get menghasilkan future yang menyelesaikan pembukaan koneksi, pengiriman permintaan, dan penerimaan respons. Isi respons dibaca sebagai teks atau byte dengan future lain, sehingga kedua tahap itu sama-sama dapat digabung, dilombakan, atau dibatasi waktunya bersama future lain. Contoh berikut mengambil satu halaman web lalu mencetak panjang teksnya.
// tidak-dijalankan
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let balasan = reqwest::get("https://www.rust-lang.org").await?;
let teks = balasan.text().await?;
println!("{} byte teks diterima", teks.len());
Ok(())
}Fragmen ini membutuhkan dependensi reqwest = "0.12" dan berjalan pada runtime tokio. Tipe error khusus reqwest merangkum kegagalan jaringan, pembukaan TLS, dan penguraian isi sehingga operator ? dapat meneruskannya hingga keluar dari main. Untuk program dengan banyak permintaan, satu Client dapat dibagikan ke seluruh task sehingga koneksi dapat dipakai ulang. Crate yang sama juga menyediakan modul reqwest::blocking untuk program sinkron, dan pemilihan antara keduanya dijelaskan pada bagian kesalahan umum di bawah.
async fn dalam trait#
Dukungan native atas async fn pada trait stabil sejak Rust 1.75, sehingga trait milik sendiri dapat memuat metode async tanpa crate tambahan. Sebelum versi itu, kompiler belum sanggup menamai tipe future yang dihasilkan tiap implementasi, sehingga pola yang beredar memakai Pin<Box<dyn Future>> sebagai tipe kembalian yang seragam.
// tidak-dijalankan
use std::future::Future;
use std::pin::Pin;
trait PemrosesLama {
fn proses(&self, masukan: String)
-> Pin<Box<dyn Future<Output = String> + Send>>;
}Dukungan native menghapus pembungkusan itu pada pemakaian statis, yaitu pemanggilan metode melalui tipe konkret, bukan melalui trait object dyn. Program berikut mendefinisikan trait dengan metode async, mengimplementasikannya, lalu memanggilnya seperti async fn biasa. Karena tubuh implementasi hanya menunggu ready yang selesai pada poll pertama, satu poll sudah cukup untuk menjalankannya, dan Waker::noop() menyediakan waker tanpa efek sebagai pengisi Context pada contoh yang tidak memakai runtime.
use std::future::{ready, Future};
use std::pin::pin;
use std::task::{Context, Poll, Waker};
// Metode async pada trait: stabil sejak Rust 1.75 tanpa crate tambahan.
trait Pemroses {
async fn proses(&self, masukan: String) -> String;
}
struct Kapital;
impl Pemroses for Kapital {
async fn proses(&self, masukan: String) -> String {
// ready() selesai pada poll pertama sehingga contoh ini
// tidak membutuhkan runtime penuh
let dasar = ready(masukan).await;
dasar.to_uppercase()
}
}
fn main() {
let pemroses = Kapital;
let teks = String::from("catatan harian");
println!("sebelum memanggil proses()");
let mut tugas = pin!(pemroses.proses(teks));
println!("future dibuat, tubuh proses() belum berjalan");
// Satu poll sudah menjalankan tubuhnya sampai selesai.
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);
match tugas.as_mut().poll(&mut cx) {
Poll::Ready(hasil) => println!("hasil: {}", hasil),
Poll::Pending => unreachable!("ready() selesai pada poll pertama"),
}
}sebelum memanggil proses()
future dibuat, tubuh proses() belum berjalan
hasil: CATATAN HARIANKeluaran memperlihatkan kembali sifat lazy: pemanggilan proses hanya membangun future, dan baris ketiga tercetak setelah satu poll menjalankan tubuh metodenya. Makro pin! menyemat future ke stack agar alamatnya stabil selama dipoll, sebagaimana syarat Pin pada trait Future.
Kesalahan umum#
Kesalahan pertama adalah memakai reqwest::blocking di dalam kode async. Modul itu menjalankan runtime sendiri di balik layar dan memicu panic ketika dipanggil dari dalam runtime lain, misalnya dari dalam async fn. Gunakan klien async pada program async, atau pisahkan seluruh jalur blokir ke program tersendiri.
Kesalahan kedua adalah menganggap async mempercepat segala hal. Async memperbesar jumlah penantian yang dilayani satu thread, tetapi tidak menambah kecepatan komputasi, dan program yang seluruh kerjanya menghitung justru mendapat biaya tambahan dari penjadwalan. Gunakan thread untuk kerja padat komputasi, dan cadangkan async untuk beban yang didominasi penantian.
Kesalahan ketiga adalah memanggil metode async melalui trait object dyn. Kompiler menolaknya karena trait dengan async fn tidak bersifat dyn-compatible, bukan karena kesalahan pada kode pemanggil. Pilih crate async-trait bila kelenturan dinamis dibutuhkan, atau pemanggilan generik bila performa lebih penting.
Ringkasan#
- Ekosistem async tersusun berlapis dari std::future, tokio, hyper dan tower, sampai axum dan reqwest, dan setiap lapisan hanya bergantung pada lapisan di bawahnya.
- axum memetakan rute ke async fn dengan extractor, sedangkan reqwest menyediakan klien HTTP async di atas hyper, dan keduanya berjalan pada runtime tokio.
- async fn pada trait stabil sejak Rust 1.75 untuk pemakaian statis, sementara pemanggilan
dynmemerlukan crateasync-traitatau desain generik. - Pemilihan async ditentukan oleh beban penantian, bukan oleh anggapan bahwa async selalu lebih cepat.
Referensi#
- axum — docs.rs — Dokumentasi kerangka web axum beserta contoh router dan state.
- reqwest — docs.rs — Dokumentasi klien HTTP beserta pembatasan modul blocking.
- Announcing Rust 1.75.0 — The Rust Blog — Pengumuman rilis yang menstabilkan async fn pada trait.
- async-trait — docs.rs
— Crate yang menerjemahkan metode async untuk pemanggilan
dyn Trait.