Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Membikin Web Server yang Single-Threaded

Kita bakal mulai dengan ngebikin supaya sebuah web server single-threaded (satu utas/thread) bisa jalan. Sebelum kita mulai, mari kita lihat ikhtisar (overview) kilat soal protokol-protokol yang dilibatin di dalam pembuatan web servers. Detail-detail dari protokol ini emang ada di luar dari cakupan buku ini, tapi ikhtisar singkat ini bakal ngasih kita informasi yang kita butuhkan.

Dua protokol utama yang dilibatin di dalam web servers adalah Hypertext Transfer Protocol (HTTP) dan Transmission Control Protocol (TCP). Kedua protokol ini adalah protokol request-response (minta dan balas), yang artinya sebuah client (klien) ngirim requests dan sebuah server ngedengerin (listens to) requests tersebut lalu ngasih sebuah response (respons/balasan) ke si client tadi. Konten (isi) dari requests dan responses ini didefinisikan sama protokol-protokol tersebut.

TCP itu adalah protokol tingkat lebih rendah (lower-level protocol) yang mendeskripsikan detail-detail soal gimana informasi itu nyampe dari satu server ke server lainnya tapi dia tidak menentukan secara spesifik apa sebenarnya bentuk informasi itu. HTTP ngebangun di atas (builds on top of) TCP dengan cara mendefinisikan konten dari requests dan responses tersebut. Secara teknis itu mungkin aja buat memakai HTTP pakai protokol selain TCP, tapi di mayoritas kasus yang ada, HTTP ngirim datanya lewat TCP. Kita bakal kerja dengan barisan bytes mentah (raw bytes) dari requests dan responses TCP dan HTTP ini.

Mendengarkan Koneksi TCP

Web server kita perlu mendengarkan (listen to) sebuah koneksi TCP, jadi itulah bagian pertama yang bakal kita kerjain. Standard library menawarkan sebuah modul std::net yang membiarkan kita buat ngelakuin ini. Mari kita bikin project baru pakai cara yang biasa:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

Sekarang masukin kode yang ada di Listing 21-1 ke dalam src/main.rs buat memulai. Kode ini bakal dengerin di alamat lokal 127.0.0.1:7878 nyari streams (aliran data) TCP yang lagi mau masuk (incoming). Pas dia dapat sebuah stream yang masuk, dia bakal mencetak Connection established!.

Filename: src/main.rs
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}
Listing 21-1: Mendengarkan streams yang masuk dan mencetak sebuah pesan pas kita nerima sebuah stream

Memakai TcpListener, kita bisa ngedengerin nyari koneksi-koneksi TCP di alamat 127.0.0.1:7878. Di alamat tersebut, bagian sebelum titik dua itu adalah alamat IP yang merepresentasikan komputer kita (ini sama aja di setiap komputer dan tidak merepresentasikan spesifik komputernya si penulis ya), dan 7878 itu adalah port-nya. Kita udah milih port ini karena dua alasan: HTTP itu umumnya tidak diterima di port ini, jadi server kita ini punya kemungkinan kecil buat berkonflik sama web server lain yang mungkin lagi jalan di mesin komputer kita, dan 7878 itu adalah kata rust yang diketik di telepon jadul.

Fungsi bind (ikat) di dalam skenario ini bekerja kayak fungsi new di mana dia bakal mengembalikan (return) sebuah instance TcpListener yang baru. Fungsi ini dikasih nama bind karena, di dunia jaringan komputer (networking), nyambung ke sebuah port buat mulai mendengarkan ke sana itu dikenal dengan istilah “binding to a port” (ngikat ke sebuah port).

Fungsi bind ini mengembalikan sebuah Result<T, E>, yang mana mengindikasikan kalau proses binding ini mungkin aja gagal (fail). Misalnya, kalau kita ngejalanin dua instance dari program kita sehingga ada dua program yang dengerin di port yang sama persis. Karena kita ini lagi nulis sebuah server super dasar (basic) cuma buat tujuan pembelajaran aja, kita tidak bakal ambil pusing buat menangani (handling) error-error semacam ini; sebaliknya, kita bakal memakai unwrap buat ngehentiin programnya kalau error-error ini emang kejadian.

Method incoming pada TcpListener mengembalikan sebuah iterator yang ngasih kita serangkaian streams (lebih spesifiknya, streams dari tipe TcpStream). Sebuah stream tunggal itu merepresentasikan sebuah koneksi terbuka (open connection) antara si client sama si server. Sebuah connection (koneksi) itu adalah nama buat proses pemanggilan request dan response secara utuh di mana si client nyambung ke server-nya, si server ngehasilin sebuah response, lalu si server menutup koneksi tersebut. Makanya, kita bakal membaca (read) dari TcpStream ini buat tahu apa yang dikirim sama si client dan lalu menulis (write) response kita ke stream tersebut buat ngirim datanya kembali ke si client. Secara umum, loop (perulangan) for ini bakal memproses setiap koneksi secara bergantian dan menghasilkan serangkaian streams buat kita tanganin.

Buat sekarang, cara penanganan kita terhadap stream ini adalah dengan memanggil unwrap buat menghentikan (terminate) program kita kalau ternyata stream tersebut punya error apa pun; kalau tidak ada error sama sekali, programnya bakal mencetak sebuah pesan. Kita bakal nambahin fungsionalitas yang lebih buat kasus di mana program sukses (success case) di listing berikutnya. Alasan kenapa kita mungkin nerima error dari method incoming pas seorang client nyambung ke server adalah karena kita itu sebenarnya bukan beriterasi melewati koneksi-koneksi (connections). Sebaliknya, kita itu lagi beriterasi ngelewatin percobaan-percobaan koneksi (connection attempts). Koneksinya bisa aja tidak sukses karena banyak alasan, yang mana kebanyakan dari alasan itu spesifik sama sistem operasi (operating system specific) masing-masing. Misalnya, banyak sistem operasi yang punya batas seberapa banyak jumlah koneksi terbuka simultan (berbarengan) yang bisa mereka dukung; percobaan koneksi baru yang ngelebihi jumlah tersebut bakal ngehasilin error sampai ada beberapa koneksi yang udah kebuka tadi itu pada ditutupin dulu.

Mari kita cobain buat ngejalanin kode ini! Panggil cargo run di terminal dan lalu buka (load) 127.0.0.1:7878 di sebuah web browser. Web browser-nya seharusnya nampilin pesan error kayak “Connection reset” karena emang si server-nya saat ini belum ngirim balik data apa pun. Tapi pas kita ngelihat ke terminal kita, kita harusnya bisa ngelihat beberapa pesan yang tadi dicetak pas si browser ini nyambung ke servernya!

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

Kadang-kadang kita bakal ngelihat banyak pesan yang dicetak cuma buat satu request dari browser; alasannya mungkin adalah karena si browser itu lagi ngebikin request buat halaman utamanya sekaligus ngebikin request juga buat resources (sumber daya) lainnya, kayak misalnya icon favicon.ico yang suka muncul di tab browser itu.

Bisa juga karena si browser ini lagi mencoba buat nyambung ke server berkali-kali karena si server tidak ngasih respons data apa-apa. Saat stream keluar dari scope dan di-drop (dibuang) di akhir perulangannya, koneksinya secara otomatis ditutup (closed) sebagai bagian dari implementasi dari method drop tersebut. Browser kadang-kadang nanganin koneksi yang ditutup ini dengan cara mencoba ulang (retrying), karena ya mungkin masalahnya itu cuma sementara.

Browser juga kadang-kadang ngebuka koneksi yang banyak ke sebuah server tanpa ngirim permintaan apa-apa, jadi kalau nanti mereka memang ngirim request, request-nya itu bisa kejadian lebih cepet. Pas ini kejadian, server kita bakal bisa ngelihat koneksi tersebut, terlepas dari apakah ada request apa enggak yang dikirim liwat koneksi itu. Versi-versi dari browser berbasis Chrome misalnya banyak yang ngelakuin ini; kita bisa menonaktifkan optimasi ini dengan cara memakai mode private browsing (samaran) atau dengan memakai web browser yang beda.

Faktor yang penting adalah kita udah berhasil dapetin sebuah pegangan (handle) ke sebuah koneksi TCP!

Inget ya buat ngestop (stop) programnya dengan cara neken ctrl-C pas kita udah selesai ngejalanin suatu versi kode tertentu. Terus nyalain ulang programnya dengan cara manggil perintah cargo run setiap kali habis ngebikin rangkaian perubahan kode (code changes) buat mastiin kalau kita emang ngejalanin kodenya yang paling baru.

Membaca Request (Permintaan)

Mari kita implementasikan fungsionalitas buat membaca request yang asalnya dari browser! Buat misahin urusan (concerns) dari yang awalnya dapet koneksi terlebih dahulu lalu setelah itu baru ngambil beberapa tindakan tertentu sama koneksi tersebut, kita bakal bikin sebuah fungsi baru yang khusus buat memproses koneksi (processing connections). Di dalam fungsi handle_connection yang baru ini, kita bakal membaca data yang asalnya dari TCP stream tersebut dan lalu mencetaknya supaya kita bisa ngelihat data apa yang lagi dikirim sama si browser. Ubah kodenya supaya kelihatan kayak yang ada di Listing 21-2.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("Request: {http_request:#?}");
}
Listing 21-2: Membaca dari TcpStream dan mencetak data tersebut

Kita ngebawa (bring) std::io::prelude dan std::io::BufReader ke dalam scope buat dapetin akses ke trait-trait dan tipe-tipe yang membiarkan kita buat membaca dari dan nulis ke stream tersebut. Di dalam loop for yang ada di fungsi main, ketimbang kita sekadar nyetak pesan yang bilang kalau kita udah dapat koneksi, sekarang kita memanggil fungsi handle_connection yang baru lalu mengoper stream tersebut ke dalamnya.

Di dalam fungsi handle_connection, kita ngebikin sebuah instance BufReader yang membungkus referensi ke si stream tersebut. BufReader ini nambahin fitur buffering (penyangga) dengan cara mengatur (managing) pemanggilan-pemanggilan ke method-method dari trait std::io::Read secara otomatis buat kita.

Kita ngebikin sebuah variabel bernama http_request buat ngumpulin (collect) baris-baris dari request yang dikirim sama si browser ke server kita. Kita mengindikasikan kalau kita mau ngumpulin baris-baris tersebut ke dalam sebuah vector dengan cara nambahin anotasi tipe Vec<_>.

BufReader mengimplementasikan trait std::io::BufRead, yang mana menyediakan method lines. Method lines ini mengembalikan sebuah iterator dari tipe Result<String, std::io::Error> dengan cara ngebelah-belah (splitting) aliran datanya (stream of data) setiap kali dia ngelihat sebuah byte newline (baris baru). Buat bisa dapat tiap String-nya, kita memakai map dan unwrap pada masing-masing Result. Tipe Result ini mungkin aja berisi sebuah error kalau datanya ternyata bukan UTF-8 yang valid atau kalau sekiranya ada masalah pas membaca dari stream tersebut. Sekali lagi, di program level production kita seharusnya nanganin error-error kayak gini dengan jauh lebih cakep (gracefully), tapi kita lebih milih buat ngestop aja programnya di kasus error ini demi menyederhanakan contoh.

Si browser nandain akhir (end) dari sebuah request HTTP dengan cara ngirimin dua karakter baris baru (newline) secara berurutan (in a row), jadi supaya kita bisa dapet satu request dari si stream, kita ngambil barisnya terus-terusan sampai kita dapet baris yang mana itu adalah string yang kosong. Setelah kita ngumpulin semua barisnya ke dalam vector, kita nyetak mereka pakai pretty debug formatting (format debug cantik yang gampang dibaca) supaya kita bisa lihat sendiri instruksi-instruksi apa aja yang lagi dikirim sama si web browser ke server kita.

Mari kita cobain kode ini! Jalanin programnya dan coba lakuin request (ngunjungin alamat) pakai web browser lagi. Perhatikan kalau kita bakal tetep dapet halaman error di web browsernya ya, tapi sekarang output dari program kita yang ada di dalam terminal bakal kelihatan kira-kira kayak gini:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

Tergantung dari browser apa yang kita pake, kita mungkin dapat output yang agak sedikit beda. Sekarang setelah kita udah nyetak isi dari request datanya, kita bisa paham kan alasan kenapa kita dapat koneksi berkali-kali dari satu request web browser kalau kita ngelihat path (jalur) yang ada setelah kata GET di baris paling pertama dari request tersebut. Kalau koneksi-koneksi yang berulang (repeated connections) itu semuanya lagi nge-request /, kita jadi tahu kalau browser-nya itu lagi nyoba buat ngambil (fetch) / berkali-kali karena dia tidak dapat respons apa-apa dari program kita.

Mari kita bedah dan perinci (break down) data request ini supaya kita benar-benar ngerti apa yang lagi diminta (asking of) sama si browser dari program kita ini.

Ngelihat Lebih Dekat pada Request HTTP

HTTP itu adalah protokol yang berbasis teks (text-based protocol), dan sebuah request (permintaan) itu ngebentuk format kayak gini:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

Baris yang paling pertama itu disebut dengan request line (baris permintaan) yang menampung informasi tentang apa yang lagi di-request sama si client. Bagian pertama dari si request line ini mengindikasikan method (metode) apa yang lagi dipakai, kayak misalnya GET atau POST, yang mana mendeskripsikan gimana caranya si client ini melakukan request tersebut. Client kita (yakni si web browser tadi) itu memakai sebuah request GET, yang berarti dia itu lagi minta dikasihin suatu informasi.

Bagian yang selanjutnya di request line tersebut adalah /, yang mana mengindikasikan uniform resource identifier (URI) yang lagi di-request sama client tersebut: sebuah URI itu tuh hampir sekali, tapi tidak sepenuhnya sama persis, dengan sebuah uniform resource locator (URL). Perbedaan antara URI dan URL ini tidaklah penting buat tujuan pembelajaran kita di bab ini, tapi spesifikasi HTTP (HTTP spec) memakai istilah URI, jadi kita bisa dalam hati aja men-substitusikan (menggantikan) URL jadi URI di sini.

Bagian terakhirnya adalah versi HTTP yang lagi dipakai sama si client, dan kemudian si request line tersebut diakhiri pakai urutan CRLF (CRLF sequence). (CRLF singkatan dari carriage return dan line feed, yang mana ini adalah istilah yang asalnya dari jaman mesin tik lho!) Urutan CRLF ini juga bisa ditulis sebagai \r\n, di mana \r itu adalah si carriage return dan \n itu adalah si line feed (baris baru). Urutan CRLF ini memisahkan bagian request line dari sisa (rest) data request yang lainnya. Perhatikan kalau pas CRLF ini dicetak, kita ngelihatnya kayak dimulainya baris baru kan ketimbang tulisan \r\n.

Ngelihat ke data request line yang kita dapet dari hasil ngejalanin program kita sejauh ini, kita ngelihat kalau GET itu adalah method-nya, / itu adalah request URI-nya, dan HTTP/1.1 itu adalah versinya.

Setelah baris pertama (request line) tadi, baris-baris tersisa yang diawali dengan kata Host: dan seterusnya itu semuanya adalah bagian headers. Requests tipe GET itu sama sekali tidak punya body (badan/isi pesan).

Coba deh bikin sebuah request (permintaan) dari browser yang beda atau coba minta sebuah alamat yang berbeda, kayak misalnya 127.0.0.1:7878/test, dan perhatikan aja gimana isi dari data request-nya itu berubah.

Nah, sekarang karena kita udah paham apa yang sebenarnya lagi diminta sama si browser, mari kita coba buat ngirim balik beberapa data!

Nulis Sebuah Response (Respons)

Kita bakal mengimplementasikan cara mengirim data sebagai sebuah response (respons/balasan) terhadap request yang dibikin client (client request). Responses itu punya bentuk format kayak gini:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

Baris pertama itu disebut status line (baris status) yang mengandung informasi versi HTTP yang dipakai di dalam response ini, sebuah kode status berupa angka (numeric status code) yang nge-ringkas (summarizes) apa hasil akhir dari request-nya, dan juga reason phrase (frasa alasan) yang menyediakan deskripsi teks dari kode status tersebut. Setelah urutan CRLF pertama adalah headers (kalau ada), dan diikuti oleh satu urutan CRLF lagi, dan barulah kemudian body (isi badan) dari si response tersebut.

Berikut ini adalah sebuah contoh response yang memakai versi HTTP 1.1, punya kode status 200, beserta reason phrase OK, tidak punya headers, dan tidak punya body:

HTTP/1.1 200 OK\r\n\r\n

Kode status 200 itu adalah respons standar buat bilang sukses (success response). Teks barusan adalah sebuah response HTTP sukses yang ukurannya sekecil mungkin. Mari kita tulis ini ke dalam stream (aliran data) kita sebagai response ke request yang sukses! Dari dalam fungsi handle_connection tadi, silakan hapus kode println! yang fungsinya buat nyetak data request tadi dan terus ganti pakai kode yang ada di Listing 21-3.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let response = "HTTP/1.1 200 OK\r\n\r\n";

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-3: Menulis sebuah response HTTP sukses yang imut (tiny) ke dalam stream

Baris baru yang pertama mendefinisikan variabel response yang bakal menyimpan data pesan sukses kita. Terus kita panggil method as_bytes pada variabel response kita ini buat mengkonversi data string tadi jadi kumpulan bytes. Method write_all pada variabel stream itu menerima nilai tipe &[u8] (array slice of bytes) dan dia bakal ngirim bytes tersebut secara langsung nyusurin (down) koneksi tersebut. Karena operasi write_all ini berpotensi gagal, kita memakai unwrap pada segala result error kayak sebelumnya. Sekali lagi ya, di aplikasi yang rill (real application) kita seharusnya nambahin error handling (penanganan error) di sini.

Dengan adanya perubahan-perubahan ini, mari kita jalanin kode kita lalu kita bikin sebuah request lewat browser. Kita udah tidak lagi mencetak data apa pun ke terminal ya, jadi kita tidak bakal ngelihat output apa-apa selain output dari Cargo. Pas kita memuat (load) alamat 127.0.0.1:7878 di sebuah web browser, kita seharusnya ngedapetin halaman putih (blank page) ketimbang halaman error. Kita baru aja melakukan hardcode (kode manual) buat menerima request HTTP lalu mengirimkan sebuah response secara utuh!

Mengembalikan HTML yang Asli (Real HTML)

Mari kita implementasikan fungsionalitas buat ngembaliin lebih dari sekadar halaman kosong (blank page). Silakan bikin sebuah file baru bernama hello.html di directory utama (root) dari project kita, inget ya bukan di dalem folder src. Kita bisa naruh (input) kode HTML apa aja yang kita mau kok; Listing 21-4 nunjukin salah satu kemungkinan isinya.

Filename: hello.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>
Listing 21-4: Sebuah contoh file HTML sampel buat di-return (dikembalikan) di dalam sebuah response

Ini adalah sebuah dokumen HTML5 yang sangat minimal yang cuma ada heading (judul) dan sedikit teks doang. Buat ngembaliin kode ini dari server saat ada request yang diterima, kita bakal memodifikasi handle_connection seperti yang ditunjukin di Listing 21-5 supaya dia ngebaca file HTML tersebut, menambahkannya ke dalam response kita sebagai isi dari body, lalu mengirimkannya.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};
// --snip--

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-5: Mengirim isi konten (contents) dari hello.html sebagai body (isi pesan) dari response tersebut

Kita udah menambahkan fs ke dalam statement use buat membawa filesystem module (modul file sistem) kepunyaan standard library masuk ke dalam scope. Kode buat membaca isi dari sebuah file ke dalam sebuah string seharusnya udah kelihatan familier; kita sempat memakainya pas kita lagi membaca isi konten dari sebuah file buat project I/O kita balik pas di Listing 12-4.

Selanjutnya, kita memakai format! buat menambahkan konten file tersebut sebagai body dari response sukses kita tadi. Supaya pasti kalau ini adalah response HTTP yang benar-benar valid, kita juga menambahkan header Content-Length yang mana diatur isinya supaya ngepas sama ukuran (size) dari body dari response kita, di kasus ini berarti ukurannya sama dengan ukuran file hello.html.

Coba jalanin kode ini pakai cargo run terus muat (load) 127.0.0.1:7878 di browser kita; kita harusnya bisa ngelihat HTML kita dimuat (rendered)!

Saat ini, kita emang lagi mengabaikan data request yang ada di http_request dan kita cuma tanpa syarat (unconditionally) mengirimkan kembali isi (contents) dari file HTML tersebut. Itu artinya kalau kita mencoba nge-request halaman 127.0.0.1:7878/something-else (apa-aja-lainnya) di browser kita, kita bakal tetep dapet balasan response HTML yang ini-ini juga. Saat ini, server kita ini sifatnya sangat terbatas (very limited) dan masih belum berbuat apa yang mayoritas web server benar-benar lakuin. Kita mau mengkustomisasi responses kita supaya bergantung pada si request tersebut lalu cuma mengirimkan balik file HTML itu untuk request / yang formasinya bener (well-formed request).

Memvalidasi Request dan Merespons Secara Selektif

Sekarang ini, web server kita ini bakal selalu nge-return (ngembaliin) file HTML kita ini tidak peduli apa pun yang diminta sama client-nya. Mari kita tambahin fungsionalitas buat mengecek apakah browser ini benar-benar lagi nge-request rute / sebelum nge-return si file HTML, dan terus dia bakal mengembalikan pesan error kalau browser tersebut mencoba minta apa pun yang lainnya. Buat ngelakuin ini, kita perlu memodifikasi fungsi handle_connection, kayak yang ditunjukin di Listing 21-6. Kode yang baru ini bakal mengecek konten dari request yang baru diterima tersebut dan membandingkannya (against) terhadap rupa dari request GET buat path (rute) / yang kita ketahui (know), lalu nambahin blok if dan else buat memperlakukan (treat) requests itu dengan cara yang berbeda-beda.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}
Listing 21-6: Menangani requests buat rute / secara berbeda dari requests yang lain

Kita emang cuma bakal ngelihat baris pertama (first line) doang dari request HTTP-nya, jadi ketimbang baca keseluruhan request-nya dan dimasukin ke dalam sebuah vector, kita cuma manggil method next buat dapat item paling pertama (first item) dari sang iterator. Method unwrap yang pertama ngurusin nilai Option-nya dan langsung ngestop program kalau si iterator tidak punya item apa-apa. Terus unwrap yang kedua menangani nilai Result-nya yang mana efeknya persis sama kayak unwrap yang sempat ada di dalam method map pas di Listing 21-2.

Berikutnya, kita ngecek nilai request_line buat ngebuktiin apakah dia itu sama dengan request line milik sebuah request GET buat path /. Kalau emang sama (it does), blok if tersebut bakal mengembalikan konten dari file HTML kita.

Kalau nilai request_line itu tidak sama (not equal) dengan GET request ke path /, itu artinya kita udah nerima request untuk hal lain. Kita bakal nambahin kode ke dalam blok else sebentar lagi buat membalas segala macam requests lain yang masuk.

Silakan jalankan kode ini sekarang dan terus coba request alamat 127.0.0.1:7878; kita seharusnya dapetin si HTML di dalam hello.html tersebut. Kalau kita bikin request apa pun yang lainnya, kayak misalnya 127.0.0.1:7878/something-else, kita bakal dapet error koneksi kayak yang kita temui pas ngejalanin kode di Listing 21-1 dan Listing 21-2.

Sekarang mari kita tambahin kode yang ada di Listing 21-7 ke dalam blok else tersebut buat mengembalikan sebuah response (balasan) yang mana punya status kode 404, yang mengindikasikan (signals) kalau konten buat request tersebut tidak ditemukan (not found). Kita juga bakal mengembalikan (return) sebuah file HTML buat halaman (page) yang bakal ditampilin (render) ke dalam browser yang mana bakal mengindikasikan kepada sang end user (pengguna akhir) apa sebenarnya response-nya.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    // --snip--
    } else {
        let status_line = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    }
}
Listing 21-7: Membalas dengan status kode 404 dan sebuah halaman error kalau apa pun selain rute / yang di-request

Di sini, response kita punya status line (baris status) dengan status kode 404 dan reason phrase NOT FOUND. Isi (body) dari response ini bakal menjadi konten HTML dari sebuah file bernama 404.html. Kita perlu ngebikin file 404.html ini di sebelah (next to) file hello.html kita tadi buat halaman error ini; sekali lagi ya, silakan pake HTML apa aja yang kita mau secara bebas (feel free to use), atau kita juga bisa pakai HTML contoh (example HTML) yang ada di Listing 21-8.

Filename: 404.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>
Listing 21-8: Isi sampel konten buat halaman yang bakal dikirim balik buat semua response 404

Dengan perubahan-perubahan ini, jalanin server kita lagi. Nge-request 127.0.0.1:7878 seharusnya mengembalikan isi konten dari hello.html, dan nyoba request apa pun yang lainnya, kayak 127.0.0.1:7878/foo, seharusnya nge-return halaman HTML error dari 404.html.

Sentuhan Kecil Refactoring (Merombak Kode)

Saat ini, di blok if dan else ada sangat banyak kode yang ngulang (repetition): mereka berdua sama-sama lagi ngebaca file (reading files) lalu sama-sama nulis (writing) isi konten file tersebut ke dalam stream. Satu- satunya perbedaan ada di bagian status line dan nama filenya (filename). Mari kita bikin kode ini jadi jauh lebih ringkas (concise) dengan menarik keluar (pulling out) perbedaannya ke dalem baris-baris if dan else yang dipisah yang mana bakal nge-assign (ngasih) nilai-nilai dari status line dan nama filenya ke dalam variabel; kita lalu bisa memakai variabel-variabel tersebut secara mutlak (unconditionally) di dalam kodenya buat membaca file dan nulis (write) balasannya. Listing 21-9 nunjukin kode hasil (resultant code) sesudah kita nggantiin (replacing) blok if dan else yang sangat besar (large) tadi.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    // --snip--
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-9: Merombak ulang (refactoring) blok if dan else supaya cuma berisi kode yang emang beda di antara kedua kasus tersebut

Sekarang blok if dan else tersebut cuma bakal nge-return nilai yang paling pas buat status line dan nama file (filename) di dalam sebuah tuple; terus kita memakai destructuring (pemecahan) buat nge-assign kedua nilai ini ke variabel status_line dan filename memakai pattern (pola) di dalam statement let, seperti yang udah di bahas di Bab 19.

Kode yang tadinya menduplikasi (duplicated) sekarang udah ditaruh di luar (outside) blok if dan else lalu dia memakai variabel status_line dan filename tadi. Hal ini ngebikin perbedaan di antara kedua kasus ini (two cases) jadi jauh lebih gampang buat dilihat, dan ini juga berarti kita cuma perlu ngubah kode (update the code) di satu tempat aja kalau kita pengen ngubah cara kerja gimana si proses baca file (file reading) dan proses tulis balasan (response writing) ini berjalan. Perilaku kode (behavior) di Listing 21-9 ini bakal tetap persis sama kayak yang ada di Listing 21-7.

Keren sekali! (Awesome!) Nah sekarang kita udah punya sebuah web server sederhana cuma dalam waktu lebih kurang (approximately) 40 baris kode Rust yang mana merespons sebuah request tertentu dengan halaman berisi konten (page of content) dan membalas (responds to) semua requests lainnya pakai response 404.

Saat ini, server kita masih jalan di dalem single thread (satu utas/utas tunggal), yang artinya dia cuma bisa melayani (serve) satu request dalam satu waktu tertentu (at a time). Mari kita telusuri dan menguji (examine) gimana cara kerja kayak gini bisa mendatangkan sebuah masalah dengan menyimulasikan beberapa requests yang jalannya pelan (slow requests). Terus setelah itu kita bakal benerin itu supaya server kita bisa nanganin (handle) banyak requests secara sekaligus (at once).