Seluruh jeda di chapter-chapter sebelumnya adalah simulasi: tokio::time::sleep meniru server lambat tanpa benar-benar berbicara dengan siapa pun. Manfaat sesungguhnya async baru terasa ketika penantinan itu nyata — membaca berkas dari disk yang sibuk, menunggu respons server di jaringan, membatasi waktu tunggu operasi yang macet. Tokio membawa ketiga kemampuan itu lewat tokio::fs, tokio::net, dan tokio::time::timeout, semuanya dengan API async yang sama rasa dengan sleep yang sudah kita kuasai. Chapter ini menjalankan ketiganya dalam contoh nyata: menulis dan membaca berkas, membungkus operasi dengan tenggat waktu, lalu membangun koneksi TCP lokal yang saling membalas pesan.
Menulis dan Membaca File Secara Async #
Operasi berkas di std::fs bersifat blocking — thread penulisnya tidur sampai disk selesai. Tokio menyediakan tokio::fs dengan API bentuk kembar yang mengembalikan future, sehingga menunggu disk tidak lagi menyandera thread worker:
use tokio::fs;
#[tokio::main]
async fn main() {
let path = std::env::temp_dir().join("w5d-laporan.txt");
fs::write(&path, "penjualan kuartal: Rp150.000.000")
.await
.expect("gagal menulis file");
println!("file ditulis: {}", path.display());
let contents = fs::read_to_string(&path).await.expect("gagal membaca file");
println!("isi file: {}", contents);
}Output yang Dihasilkan:
file ditulis: /tmp/w5d-laporan.txt
isi file: penjualan kuartal: Rp150.000.000Mari kita bedah kode di atas:
- Baris 5 (
let path = std::env::temp_dir().join("w5d-laporan.txt");):temp_dir()mengembalikan direktori sementara sistem (di Linux umumnya/tmp) sehingga contoh tidak menulis sembarangan di proyek;joinmenyusun path berkas kita di atasnya. - Baris 7-9 (
fs::write(&path, ...).await.expect(...)):tokio::fs::writemengembalikan future yang selesai setelah seluruh isi tertulis ke disk..awaitmenunggunya tanpa memblok thread, dan.expectmembongkarResult<(), io::Error>karena kegagalan menulis (disk penuh, izin ditolak) dilaporkan lewatio::Error— persisResultdari Section Penanganan Error. - Baris 12 (
let contents = fs::read_to_string(&path).await...):read_to_stringmembaca seluruh berkas sebagaiStringmelalui future; hasilnya berisi teks yang barusan kita tulis, terbukti di output. - Output
file ditulis: /tmp/w5d-laporan.txt: nilai path dicetak denganpath.display()—Pathtidak mengimplementasikanDisplaysecara langsung, dandisplay()mengadaptasinya untuk pencetakan yang ramah segala platform.
Di balik layar, tokio menjalankan operasi berkas ini di kumpulan thread khusus untuk pekerjaan blocking (mekanisme spawn_blocking), lalu membangunkan future ketika thread tersebut selesai — API-nya async, sementara disk yang memang tidak bisa diajak async ditangani di ruang tersendiri sehingga thread worker utama tetap bebas melayani task lain.
Membatasi Waktu Tunggu dengan timeout #
Operasi I/O nyata punya kebiasaan buruk: kadang operasi itu tidak pernah selesai — server tidak menjawab, koneksi menggantung. Fungsi tokio::time::timeout membungkus future apa pun dengan tenggat waktu dan mengubah hasilnya menjadi Result:
use std::time::Duration;
use tokio::time::timeout;
async fn fetch_price(source: &str, seconds: u64) -> String {
// simulasi server yang lambat merespons
tokio::time::sleep(Duration::from_secs(seconds)).await;
format!("harga dari {}", source)
}
#[tokio::main]
async fn main() {
let fast = timeout(Duration::from_secs(2), fetch_price("gudang", 1)).await;
match fast {
Ok(result) => println!("sukses: {}", result),
Err(_) => println!("waktu tunggu habis"),
}
let slow = timeout(Duration::from_secs(1), fetch_price("kantor pusat", 3)).await;
match slow {
Ok(result) => println!("sukses: {}", result),
Err(_) => println!("waktu tunggu habis"),
}
}Output yang Dihasilkan:
sukses: harga dari gudang
waktu tunggu habisMari kita bedah kode di atas:
- Baris 12 (
let fast = timeout(Duration::from_secs(2), fetch_price("gudang", 1)).await;):timeoutmenerima durasi tenggat dan future, lalu mengembalikan future pembungkus. Pemanggilanfetch_price1 detik selesai di bawah tenggat 2 detik, sehingga hasilnyaOk(String). - Baris 13-16 (
match fast { ... }): cabangOkmembawa nilai asli future; cabangErrmembawa nilai bertipeElapsed— galat khas tokio yang menyatakandeadline has elapsed(tenggat telah terlampaui) ketika dicetak. - Baris 18 (
timeout(Duration::from_secs(1), fetch_price("kantor pusat", 3))): sekarang perbandingannya terbalik — future berjeda 3 detik menghadapi tenggat 1 detik. Setelah 1 detik,timeoutmenyatakan kalah waktu dan future yang dibungkus dibatalkan, sehingga baris penantinan 3 detik tidak pernah ditunggu sampai habis. - Output baris kedua (
waktu tunggu habis): bukti pembatalan — program total hanya berjalan sekitar 2 detik (1 detik sukses + 1 detik tenggat), bukan 4 detik seandainya kedua penantinan ditunggu penuh.
Jaringan Non-Blocking dengan TcpListener #
Bentuk I/O yang paling natural bagi async adalah jaringan: tokio::net::TcpListener dan TcpStream versi non-blocking. Contoh berikut membangun server kecil di komputer lokal yang menerima satu koneksi, membaca pesan, lalu membalasnya — sementara itu main berperan sebagai klien:
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
async fn run_server(listener: TcpListener) -> std::io::Result<String> {
let (mut socket, addr) = listener.accept().await?;
let mut message = String::new();
socket.read_to_string(&mut message).await?;
let reply = format!("diterima dari {}: {}", addr, message.trim());
socket.write_all(reply.as_bytes()).await?;
Ok(reply)
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
println!("server mendengarkan di {}", addr);
let server = tokio::spawn(run_server(listener));
let mut client = TcpStream::connect(addr).await?;
client.write_all(b"halo server").await?;
client.shutdown().await?;
let mut reply = String::new();
client.read_to_string(&mut reply).await?;
println!("klien menerima: {}", reply);
server.await.unwrap()?;
Ok(())
}Output yang Dihasilkan:
server mendengarkan di 127.0.0.1:45051
klien menerima: diterima dari 127.0.0.1:42808: halo serverNomor port pada output akan berbeda setiap kali program dijalankan, karena port 0 memerintahkan sistem operasi memilih port kosong yang tersedia saat itu — tujuannya agar contoh tidak pernah bentrok dengan layanan lain di mesin pembaca.
Mari kita bedah kode di atas:
- Baris 1 (
use tokio::io::{AsyncReadExt, AsyncWriteExt};): dua trait ini menyediakan metoderead_to_stringdanwrite_alluntuk socket async, berperan sepertiRead/Writedistd::io— keduanya perlu diimpor sebelum dipakai. - Baris 5 (
let (mut socket, addr) = listener.accept().await?;):acceptmenunggu koneksi masuk lalu mengembalikanTcpStreammilik klien tersebut beserta alamatnya.?meneruskanio::Errorke pemanggil — operator tanya bekerja persis seperti di fungsi sinkron dari Section Penanganan Error, kini di dunia async. - Baris 7 (
socket.read_to_string(&mut message).await?;): membaca terus sampai klien menutup sisi kirimnya, semuanya non-blocking — task server beristirahat di titik.awaitsetiap kali data belum tersedia. - Baris 14 (
async fn main() -> std::io::Result<()>):mainasync boleh mengembalikanResult; makro#[tokio::main]meneruskannya sehingga?juga sah langsung dimain. - Baris 15 (
TcpListener::bind("127.0.0.1:0").await?;): alamat loopback dengan port0mengikat listener lokal pada port acak yang dipilih OS;local_addr()kemudian membaca alamat sesungguhnya agar klien tahu harus ke mana. - Baris 19 (
let server = tokio::spawn(run_server(listener));): server diluncurkan sebagai task mandiri lewattokio::spawndari Chapter Spawn Task — listener berpindah kepemilikan ke task tersebut melalui parameter fungsi, sehinggamainbebas berperan sebagai klien. - Baris 23 (
client.shutdown().await?;): klien menutup sisi kirimnya setelah pesan terkirim (pola half-close). Tanpa baris ini, server akan terus menunggu data tambahan karena koneksi masih terbuka penuh, sementara klien sibuk menunggu balasan — dua task saling menunggu tanpa ada yang bergerak. - Baris 29 (
server.await.unwrap()?;): menunggu task server selesai dan meneruskan galatio::Error-nya bila ada; hasilrun_serverberupa balasan yang sama yang diterima klien.
Ringkasan #
tokio::fs menyajikan operasi berkas dengan API async — tulis dan baca w5d-laporan.txt berjalan lewat .await tanpa menyandera thread worker, dengan pekerjaan blocking ditangani thread pool khusus di belakang layar. timeout membungkus future apa pun dengan tenggat: operasi 1 detik lolos di bawah batas 2 detik, operasi 3 detik dipangkas pada detik pertama dan dilaporkan sebagai Err bertipe Elapsed. Kombinasi TcpListener, TcpStream, AsyncReadExt, dan pola half-close menghasilkan server-klien TCP lokal yang bertukar pesan — bukti pola spawn + .await bekerja pada I/O jaringan sungguhan. Ketika sumber data yang harus direspons lebih dari satu sekaligus, select! di chapter berikutnya mengambil alih peran wasit.