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!.
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!");
}
}
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.
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:#?}");
}
TcpStream dan mencetak data tersebutKita 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.
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();
}
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.
<!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>
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.
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();
}
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.
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
}
}
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.
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();
}
}
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.
<!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>
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.
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();
}
if dan else supaya cuma berisi kode yang emang beda di antara kedua kasus tersebutSekarang 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).