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.

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:

rust
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:

text
file ditulis: /tmp/w5d-laporan.txt
isi file: penjualan kuartal: Rp150.000.000

Mari kita bedah kode di atas:

  1. 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; join menyusun path berkas kita di atasnya.
  2. Baris 7-9 (fs::write(&path, ...).await.expect(...)): tokio::fs::write mengembalikan future yang selesai setelah seluruh isi tertulis ke disk. .await menunggunya tanpa memblok thread, dan .expect membongkar Result<(), io::Error> karena kegagalan menulis (disk penuh, izin ditolak) dilaporkan lewat io::Error — persis Result dari Section Penanganan Error.
  3. Baris 12 (let contents = fs::read_to_string(&path).await...): read_to_string membaca seluruh berkas sebagai String melalui future; hasilnya berisi teks yang barusan kita tulis, terbukti di output.
  4. Output file ditulis: /tmp/w5d-laporan.txt: nilai path dicetak dengan path.display() — Path tidak mengimplementasikan Display secara langsung, dan display() 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:

rust
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:

text
sukses: harga dari gudang
waktu tunggu habis

Mari kita bedah kode di atas:

  1. Baris 12 (let fast = timeout(Duration::from_secs(2), fetch_price("gudang", 1)).await;): timeout menerima durasi tenggat dan future, lalu mengembalikan future pembungkus. Pemanggilan fetch_price 1 detik selesai di bawah tenggat 2 detik, sehingga hasilnya Ok(String).
  2. Baris 13-16 (match fast { ... }): cabang Ok membawa nilai asli future; cabang Err membawa nilai bertipe Elapsed — galat khas tokio yang menyatakan deadline has elapsed (tenggat telah terlampaui) ketika dicetak.
  3. 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, timeout menyatakan kalah waktu dan future yang dibungkus dibatalkan, sehingga baris penantinan 3 detik tidak pernah ditunggu sampai habis.
  4. 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:

rust
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:

text
server mendengarkan di 127.0.0.1:45051
klien menerima: diterima dari 127.0.0.1:42808: halo server

Nomor 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:

  1. Baris 1 (use tokio::io::{AsyncReadExt, AsyncWriteExt};): dua trait ini menyediakan metode read_to_string dan write_all untuk socket async, berperan seperti Read/Write di std::io — keduanya perlu diimpor sebelum dipakai.
  2. Baris 5 (let (mut socket, addr) = listener.accept().await?;): accept menunggu koneksi masuk lalu mengembalikan TcpStream milik klien tersebut beserta alamatnya. ? meneruskan io::Error ke pemanggil — operator tanya bekerja persis seperti di fungsi sinkron dari Section Penanganan Error, kini di dunia async.
  3. Baris 7 (socket.read_to_string(&mut message).await?;): membaca terus sampai klien menutup sisi kirimnya, semuanya non-blocking — task server beristirahat di titik .await setiap kali data belum tersedia.
  4. Baris 14 (async fn main() -> std::io::Result<()>): main async boleh mengembalikan Result; makro #[tokio::main] meneruskannya sehingga ? juga sah langsung di main.
  5. Baris 15 (TcpListener::bind("127.0.0.1:0").await?;): alamat loopback dengan port 0 mengikat listener lokal pada port acak yang dipilih OS; local_addr() kemudian membaca alamat sesungguhnya agar klien tahu harus ke mana.
  6. Baris 19 (let server = tokio::spawn(run_server(listener));): server diluncurkan sebagai task mandiri lewat tokio::spawn dari Chapter Spawn Task — listener berpindah kepemilikan ke task tersebut melalui parameter fungsi, sehingga main bebas berperan sebagai klien.
  7. 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.
  8. Baris 29 (server.await.unwrap()?;): menunggu task server selesai dan meneruskan galat io::Error-nya bila ada; hasil run_server berupa 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.