Commit 1 Reflection notes
Dalam fungsi handle_connection, kita memproses request yang masuk dari browser. Berikut adalah penjelasan metode yang digunakan:
-
BufReader::new(&mut stream): Kita membungkusTcpStreammenggunakanBufReader. Ini bertujuan untuk menambahkan buffering, sehingga pembacaan data menjadi lebih efisien karena kita bisa memanggil fungsi seperti.lines()untuk membaca data baris per baris secara langsung, daripada membaca byte demi byte secara manual. -
.lines(): MengubahBufReadermenjadi iterator yang mengembalikan setiap baris teks dari stream. -
.map(|result| result.unwrap()): Karena setiap baris yang dibaca mengembalikan tipeResult(bisa sukses atau error), kita menggunakan.map()dan.unwrap()untuk mengekstrak nilaiStringdariResult::Oksecara langsung (mengasumsikan tidak ada error saat pembacaan demi kesederhanaan). -
.take_while(|line| !line.is_empty()): HTTP request header selalu diakhiri dengan dua buah newline (baris kosong). Fungsi ini akan terus membaca baris dari stream sampai menemukan baris kosong tersebut, lalu berhenti. Ini mencegah program hang karena menunggu data yang tidak akan dikirim lagi oleh browser. -
.collect(): Mengumpulkan semua baris teks yang sudah diproses di atas ke dalam sebuah koleksi vektor (Vec<_>), yang kemudian kita simpan ke variabelhttp_requestuntuk di-print.
Commit 2 Reflection notes
Pada code yang baru ditambahkan di handle_connection, server sekarang mengirimkan response yang valid sesuai HTTP Protocol. Berikut hal baru yang dipelajari:
-
fs::read_to_string("hello.html"): Digunakan untuk membaca seluruh isi file HTML dan mengubahnya menjadi tipe dataStringdi Rust agar bisa dikirimkan sebagai body dari HTTP response. -
HTTP/1.1 200 OK: Ini adalah status line standar dari HTTP yang memberitahu browser bahwa request berhasil diproses. -
Content-Length: Header HTTP yang wajib disertakan agar browser tahu seberapa besar ukuran data (dalam byte) yang akan diterima. Kita menghitungnya menggunakancontents.len(). -
Format Response (
\r\n\r\n): Protokol HTTP mensyaratkan penggunaan CRLF (\r\n) untuk memisahkan setiap baris header. Selain itu, harus ada dua buah CRLF (\r\n\r\n) yang berfungsi sebagai pemisah mutlak antara bagian Headers dan bagian Body (isi konten HTML). -
stream.write_all: Mengirimkan seluruh string balasan yang sudah diformat tadi (diubah menjadi bytes) kembali melalui TCP stream ke browser.
Commit 3 Reflection notes
Pada tahap ini, kita memvalidasi request HTTP yang masuk agar server tidak merespons semua request dengan hello.html.
-
Cara memisahkan respons: Kita membaca baris pertama dari HTTP request (
request_line). Jika baris tersebut sama persis dengan"GET / HTTP/1.1", kita mengembalikanhello.htmldengan status200 OK. Jika request line berupa hal lain (misalnya/bad), kita masuk ke blokelsedan mengembalikan404.htmldengan status404 NOT FOUND. -
Kenapa refactoring diperlukan: Pada awalnya, blok
ifdanelsememiliki banyak duplikasi kode (seperti pemanggilanfs::read_to_string, penghitungan length, dan format response). Membiarkan kode duplikat bertentangan dengan prinsip Clean Code (DRY - Don't Repeat Yourself). Dengan melakukan refactoring, kita hanya menggunakan blokif-elseuntuk menentukanstatus_linedanfilename, lalu mengeksekusi logika pembacaan file dan pengiriman respons secara universal di luar blok kondisi tersebut. Ini membuat kode lebih bersih, mudah dibaca, dan mudah di-maintenance jika nanti kita ingin menambahkan modifikasi pada format respons HTTP.
Commit 4 Reflection notes
Pada tahap ini, kita menambahkan rute /sleep yang akan melakukan simulasi proses lambat dengan menghentikan thread selama 10 detik (thread::sleep).
Dari hasil percobaan membuka dua browser window, ketika kita mengakses /sleep di window pertama dan segera mengakses / di window kedua, window kedua tertahan (loading terus) dan baru memuat halaman setelah window pertama selesai.
Kenapa ini terjadi?
Ini terjadi karena web server yang kita bangun saat ini bersifat Single-Threaded. Artinya, server hanya memproses satu request pada satu waktu secara berurutan. Ketika rute /sleep diakses, satu-satunya thread yang dimiliki program akan delay selama 10 detik. Selama masa itu, thread tersebut blocked dan tidak bisa menerima atau memproses koneksi baru yang masuk (seperti request ke / dari window kedua). Window kedua harus menunggu dalam antrean sistem operasi (TCP queue) sampai thread utama selesai memproses rute /sleep. Ini menunjukkan bahwa server single-thread sangat tidak optimal dan tidak tangguh untuk menangani banyak koneksi (karena bottleneck).
Commit 5 Reflection notes
Pada tahap ini, kita menyelesaikan masalah single-thread bottleneck dengan mengimplementasikan ThreadPool.
Bagaimana ThreadPool Bekerja:
- Saat program berjalan,
ThreadPool::new(4)akan langsung membuat 4 thread pekerja (Worker) yang berjalan di latar belakang. - Kita menggunakan struktur komunikasi Channel (
mpsc::channel) di Rust.ThreadPoolbertindak sebagai pengirim pesan (Sender), dan setiapWorkerberbagi akses sebagai penerima pesan (Receiver) melaluiArc<Mutex<Receiver>>agar aman dari race condition antar thread. - Setiap kali ada koneksi masuk dari browser, fungsi
mainakan memanggilpool.execute(...)dan mengirimkan tugas (closure/pekerjaan yang harus dieksekusi) ke dalam channel. - Worker yang sedang menganggur akan menerima tugas tersebut dari channel dan langsung mengeksekusinya.
- Dengan arsitektur ini, jika ada pengguna yang mengakses rute lambat seperti
/sleep, hanya satu Worker yang akan tertidur. Tiga Worker lainnya tetap standby dan bisa langsung melayani koneksi baru yang masuk secara bersamaan (karena itu tes membuka 2 window secara bersamaan sekarang berhasil tanpa harus antre).
Commit Bonus Reflection notes
Pada bagian bonus ini, saya mengganti fungsi new pada ThreadPool menjadi fungsi build. Berikut adalah perbandingan dan alasan perubahannya:
-
Fungsi
new(Sebelumnya): Di Rust, fungsinewmemiliki conventional yang tidak tertulis, bahwa ia harus selalu berhasil membuat dan mengembalikan instance dari sebuah struct. Pada kode sebelumnya, jika kita memasukkan parametersizebernilai0, program akan memanggil makroassert!dan seketika mengalami crash. Hal ini bukanlah design pattern yang baik untuk sebuah library atau public API, karena dapat mematikan program utama pengguna secara paksa. -
Fungsi
build(Perbaikan): Ketika proses pembuatan instance memiliki kemungkinan gagal (misalnya karena validasi input yang salah sepertisize == 0), lebih idiomatic di Rust untuk menggunakan fungsi bernamabuildyang mengembalikan tipe dataResult. Dengan cara ini, jika input bernilai0, kita mengembalikanErr(PoolCreationError)alih-alih melakukan panic. -
Dampak: Penggunaan
buildmemberikan fleksibilitas kepada pemanggil (dalam hal inimain.rs) untuk menangani error tersebut dengan elegan (misalnya dengan mencetak pesan error ke stderr dan keluar menggunakanstd::process::exit(1)), membuat server menjadi jauh lebih robust dan aman dari crash mendadak.

