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

Build Web Server แบบ Single-Threaded

เราจะเริ่มโดยทำให้ web server แบบ single-threaded ทำงาน ก่อนเราเริ่ม มาดู overview ด่วนของ protocol ที่เกี่ยวข้องใน build web server ราย ละเอียดของ protocol เหล่านี้เกินกว่า scope ของหนังสือ แต่ overview สั้น จะให้ข้อมูลที่คุณต้องการ

สอง protocol หลักที่เกี่ยวข้องใน web server คือ Hypertext Transfer Protocol (HTTP) และ Transmission Control Protocol (TCP) ทั้ง สอง protocol เป็น protocol request-response หมายความว่า client initiate request และ server ฟัง request และให้ response กับ client เนื้อหาของ request และ response เหล่านั้นถูกนิยามโดย protocol

TCP คือ protocol ระดับต่ำกว่าที่อธิบายรายละเอียดของวิธีที่ข้อมูลได้จาก server หนึ่งไปยังอีก server แต่ไม่ระบุว่าข้อมูลนั้นคืออะไร HTTP build บน TCP โดยนิยามเนื้อหาของ request และ response มันเทคนิคเป็นไปได้ที่ จะใช้ HTTP กับ protocol อื่น แต่ในกรณีส่วนใหญ่ HTTP ส่งข้อมูลของมัน ผ่าน TCP เราจะทำงานกับ byte raw ของ request และ response TCP และ HTTP

ฟัง TCP Connection

Web server ของเราต้องฟัง TCP connection ดังนั้นนั่นคือส่วนแรกที่เราจะ ทำงาน Standard library เสนอโมดูล std::net ที่ให้เราทำสิ่งนี้ มาทำ project ใหม่ในแฟชั่นปกติ:

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

ตอนนี้ใส่โค้ดใน Listing 21-1 ใน src/main.rs เพื่อเริ่ม โค้ดนี้จะ ฟังที่ address local 127.0.0.1:7878 สำหรับ stream TCP ที่เข้ามา เมื่อ มันได้ stream ที่เข้ามา มันจะ print 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: ฟัง stream ที่เข้ามาและ print ข้อความเมื่อเรารับ stream

ใช้ TcpListener เราฟัง TCP connection ที่ address 127.0.0.1:7878 ได้ ใน address ส่วนก่อน colon คือ IP address ที่ represent computer ของคุณ (นี่เหมือนกันบนทุก computer และไม่ represent computer ของผู้ เขียนโดยเฉพาะ) และ 7878 คือ port เราเลือก port นี้สำหรับสองเหตุผล — HTTP ปกติไม่ถูกยอมรับบน port นี้ ดังนั้น server ของเราไม่น่าจะ conflict กับ web server อื่นที่คุณอาจรันบนเครื่องของคุณ และ 7878 คือ rust พิมพ์บนโทรศัพท์

ฟังก์ชัน bind ใน scenario นี้ทำงานเหมือนฟังก์ชัน new ในที่มันจะ return instance TcpListener ใหม่ ฟังก์ชันถูกเรียก bind เพราะ ใน networking การเชื่อมไปยัง port เพื่อฟังรู้จักเป็น “binding ไปยัง port”

ฟังก์ชัน bind return Result<T, E> ซึ่งบ่งบอกว่ามันเป็นไปได้สำหรับ binding ที่จะ fail ตัวอย่างเช่น ถ้าเรารันสอง instance ของโปรแกรมของ เราและดังนั้นมีสองโปรแกรมฟังที่ port เดียวกัน เพราะเรากำลังเขียน server พื้นฐานเพียงสำหรับจุดประสงค์เรียน เราจะไม่กังวลเกี่ยวกับการ จัดการ error ชนิดเหล่านี้ — แทน เราใช้ unwrap เพื่อหยุดโปรแกรมถ้า error เกิด

เมธอด incoming บน TcpListener return iterator ที่ให้เราลำดับของ stream (เจาะจง stream ของ type TcpStream) stream เดียว represent connection ที่เปิดระหว่าง client และ server Connection คือชื่อ สำหรับกระบวนการ request และ response เต็มที่ client เชื่อมกับ server, server generate response และ server ปิด connection ดังนั้น เราจะอ่าน จาก TcpStream เพื่อดูสิ่งที่ client ส่งและแล้วเขียน response ของเรา ไปยัง stream เพื่อส่งข้อมูลกลับให้ client โดยรวม loop for นี้จะ process แต่ละ connection ตามลำดับและผลิตชุดของ stream ให้เราจัดการ

ตอนนี้ การจัดการของเราของ stream ประกอบด้วยการเรียก unwrap เพื่อสิ้น สุดโปรแกรมของเราถ้า stream มี error ใด — ถ้าไม่มี error โปรแกรม print ข้อความ เราจะเพิ่ม functionality เพิ่มสำหรับกรณี success ใน listing ถัดไป เหตุผลที่เราอาจรับ error จากเมธอด incoming เมื่อ client เชื่อม กับ server คือเราไม่ได้ iterate ผ่าน connection จริง ๆ แทน เรากำลัง iterate ผ่าน ความพยายาม connection Connection อาจไม่สำเร็จเพราะ เหตุผลหลายอย่าง หลายอันเฉพาะ operating system ตัวอย่างเช่น operating system หลายตัวมี limit สำหรับจำนวนของ connection ที่เปิดพร้อมกันที่ พวกมันสนับสนุน — ความพยายาม connection ใหม่เกินจำนวนนั้นจะผลิต error จนกระทั่ง connection ที่เปิดบางตัวถูกปิด

มาลองรันโค้ดนี้! Invoke cargo run ใน terminal แล้วโหลด 127.0.0.1:7878 ใน web browser browser ควรแสดงข้อความ error เช่น “Connection reset” เพราะ server ปัจจุบันไม่ส่งข้อมูลกลับใด ๆ แต่เมื่อ คุณดูที่ terminal คุณควรเห็นข้อความหลายตัวที่ print เมื่อ browser เชื่อมกับ server!

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

บางครั้งคุณจะเห็นหลายข้อความ print สำหรับหนึ่ง browser request — เหตุผล อาจเป็นว่า browser กำลังทำ request สำหรับหน้ารวมทั้ง request สำหรับ resource อื่น เช่น icon favicon.ico ที่ปรากฏใน browser tab

มันอาจเป็นว่า browser กำลังพยายามเชื่อมกับ server หลายครั้งเพราะ server ไม่ตอบกับข้อมูลใด เมื่อ stream ออกจาก scope และถูก drop ที่ ตอนจบของ loop, connection ถูกปิดเป็นส่วนของ implementation drop Browser บางครั้งจัดการ connection ที่ปิดโดยลองใหม่ เพราะปัญหาอาจชั่ว คราว

Browser ก็บางครั้งเปิดหลาย connection ไปยัง server โดยไม่ส่ง request ใดเพื่อให้ถ้าพวกเขา จริง ๆ ส่ง request ภายหลัง request เหล่านั้น เกิดได้เร็วขึ้น เมื่อสิ่งนี้เกิด server ของเราจะเห็นแต่ละ connection ไม่ว่ามี request ใดผ่าน connection นั้น Version หลายตัวของ browser ที่ ตาม Chrome ทำสิ่งนี้ ตัวอย่างเช่น — คุณปิด optimization นั้นได้โดยใช้ private browsing mode หรือใช้ browser ต่างได้

ปัจจัยสำคัญคือเราสำเร็จได้ handle ไปยัง TCP connection!

จำหยุดโปรแกรมโดยกด ctrl-C เมื่อคุณเสร็จรัน version เฉพาะของโค้ด แล้ว restart โปรแกรมโดย invoke คำสั่ง cargo run หลังคุณทำชุดการเปลี่ยนแปลงโค้ดแต่ละชุดเพื่อให้แน่ใจว่าคุณกำลังรัน โค้ดใหม่ที่สุด

อ่าน Request

มา implement functionality เพื่ออ่าน request จาก browser! เพื่อแยก concern ของการได้ connection ก่อนและแล้วทำ action กับ connection เราจะเริ่มฟังก์ชันใหม่สำหรับการ process connection ในฟังก์ชัน handle_connection ใหม่นี้ เราจะอ่านข้อมูลจาก TCP stream และ print มันเพื่อให้เราเห็นข้อมูลที่ส่งจาก browser เปลี่ยนโค้ดให้ดูเหมือน 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: อ่านจาก TcpStream และ print ข้อมูล

เรานำ std::io::BufReader และ std::io::prelude เข้า scope เพื่อ เข้าถึง trait และ type ที่ให้เราอ่านจากและเขียนไปยัง stream ใน loop for ในฟังก์ชัน main แทนการ print ข้อความที่บอกว่าเราทำ connection ตอนนี้เราเรียกฟังก์ชัน handle_connection ใหม่และส่ง stream ให้มัน

ในฟังก์ชัน handle_connection เราสร้าง instance BufReader ใหม่ที่ ห่อ reference ของ stream BufReader เพิ่ม buffering โดยจัดการการ เรียกเมธอด trait std::io::Read ให้เรา

เราสร้างตัวแปรชื่อ http_request เพื่อ collect บรรทัดของ request ที่ browser ส่งให้ server ของเรา เราบ่งบอกว่าเราต้องการ collect บรรทัด เหล่านี้ใน vector โดยเพิ่ม type annotation Vec<_>

BufReader implement trait std::io::BufRead ซึ่งให้เมธอด lines เมธอด lines return iterator ของ Result<String, std::io::Error> โดย split stream ของข้อมูลเมื่อใดก็ตามที่มันเห็น byte newline เพื่อ ได้แต่ละ String เรา map และ unwrap แต่ละ Result Result อาจ เป็น error ถ้าข้อมูลไม่ valid UTF-8 หรือถ้ามีปัญหาในการอ่านจาก stream อีกครั้ง โปรแกรม production ควรจัดการ error เหล่านี้อย่าง graceful มากขึ้น แต่เรากำลังเลือกหยุดโปรแกรมในกรณี error เพื่อความเรียบง่าย

Browser signal ตอนจบของ HTTP request โดยส่งสอง character newline ติด กัน ดังนั้นเพื่อได้หนึ่ง request จาก stream เราเอาบรรทัดจนเราได้บรรทัด ที่เป็น string ว่าง เมื่อเรา collect บรรทัดเข้า vector เรากำลัง print พวกมันโดยใช้การ format debug แบบสวยเพื่อให้เราดูที่คำสั่งที่ web browser ส่งให้ server ของเรา

มาลองโค้ดนี้! เริ่มโปรแกรมและทำ request ใน web browser อีก สังเกตว่า เราจะยังได้หน้า error ใน browser แต่ output ของโปรแกรมของเราใน terminal ตอนนี้จะดูคล้ายกับนี้:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [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",
]

ขึ้นกับ browser ของคุณ คุณอาจได้ output ต่างเล็กน้อย ตอนนี้เรากำลัง print ข้อมูล request เราเห็นได้ว่าทำไมเราได้หลาย connection จากหนึ่ง browser request โดยดูที่ path หลัง GET ในบรรทัดแรกของ request ถ้า connection ที่ซ้ำขอ / ทั้งหมด เรารู้ browser กำลังพยายามดึง / ซ้ำ เพราะมันไม่ได้รับ response จากโปรแกรมของเรา

มาแยกข้อมูล request นี้ลงเพื่อเข้าใจสิ่งที่ browser ขอจากโปรแกรมของเรา

ดู HTTP Request ใกล้ขึ้น

HTTP คือ protocol แบบ text-based และ request รับ format นี้:

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

บรรทัดแรกคือ บรรทัด request ที่บรรจุข้อมูลเกี่ยวกับสิ่งที่ client กำลังขอ ส่วนแรกของบรรทัด request บ่งบอก method ที่ถูกใช้ เช่น GET หรือ POST ซึ่งอธิบายวิธีที่ client ทำ request นี้ Client ของเราใช้ request GET ซึ่งหมายถึงมันกำลังขอข้อมูล

ส่วนถัดของบรรทัด request คือ / ซึ่งบ่งบอก uniform resource identifier (URI) ที่ client ขอ — URI เกือบแต่ไม่เป๊ะเหมือนกับ uniform resource locator (URL) ความแตกต่างระหว่าง URI และ URL ไม่ สำคัญสำหรับจุดประสงค์ของเราในบทนี้ แต่ HTTP spec ใช้คำ URI ดังนั้น เราเพียงเปลี่ยนใจ URL เป็น URI ที่นี่ได้

ส่วนสุดท้ายคือ version HTTP ที่ client ใช้ และแล้วบรรทัด request จบใน ลำดับ CRLF (CRLF ย่อมาจาก carriage return และ line feed ซึ่งเป็น คำจากยุค typewriter!) ลำดับ CRLF ก็ถูกเขียนเป็น \r\n ได้ ที่ \r คือ carriage return และ \n คือ line feed ลำดับ CRLF แยกบรรทัด request จากข้อมูล request ที่เหลือ สังเกตว่าเมื่อ CRLF ถูก print เรา เห็นบรรทัดใหม่เริ่มแทน \r\n

ดูที่ข้อมูลบรรทัด request ที่เรารับจากการรันโปรแกรมของเราตอนนี้ เรา เห็นว่า GET คือ method, / คือ URI request และ HTTP/1.1 คือ version

หลังบรรทัด request บรรทัดที่เหลือเริ่มจาก Host: ต่อไปเป็น header request GET ไม่มี body

ลองทำ request จาก browser ต่างหรือขอ address ต่างเช่น 127.0.0.1:7878/test เพื่อดูว่าข้อมูล request เปลี่ยนยังไง

ตอนนี้เรารู้สิ่งที่ browser ขอ มาส่งข้อมูลบางอย่างกลับ!

เขียน Response

เราจะ implement การส่งข้อมูลใน response กับ request ของ client Response มี format ต่อไปนี้:

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

บรรทัดแรกคือ บรรทัด status ที่บรรจุ version HTTP ที่ใช้ใน response, status code ตัวเลขที่สรุปผลของ request และวลีเหตุผลที่ให้คำอธิบาย text ของ status code หลังลำดับ CRLF เป็น header ใด ลำดับ CRLF อีก และ body ของ response

นี่คือตัวอย่าง response ที่ใช้ HTTP version 1.1 และมี status code 200 วลีเหตุผล OK ไม่มี header และไม่มี body:

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

Status code 200 คือ response success มาตรฐาน Text คือ response HTTP success เล็กจิ๋ว มาเขียนนี้ไปยัง stream เป็น response ของเรากับ request success! จากฟังก์ชัน handle_connection ลบ println! ที่ print ข้อมูล request และแทนด้วยโค้ดใน 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: เขียน response HTTP success เล็กจิ๋วไปยัง stream

บรรทัดใหม่แรกนิยามตัวแปร response ที่บรรจุข้อมูลของข้อความ success แล้ว เราเรียก as_bytes บน response ของเราเพื่อแปลงข้อมูล string เป็น byte เมธอด write_all บน stream รับ &[u8] และส่ง byte เหล่านั้นโดยตรงลงไป connection เพราะ operation write_all fail ได้ เราใช้ unwrap บนผล error ใดเหมือนก่อน อีกครั้ง ใน application จริง คุณจะเพิ่มการจัดการ error ที่นี่

ด้วยการเปลี่ยนแปลงเหล่านี้ มารันโค้ดของเราและทำ request เราไม่ได้ print ข้อมูลใดไปยัง terminal อีก ดังนั้นเราจะไม่เห็น output ใดนอกจาก output จาก Cargo เมื่อคุณโหลด 127.0.0.1:7878 ใน web browser คุณควร ได้หน้าว่างแทน error คุณเพิ่ง handcoded รับ HTTP request และส่ง response!

Return HTML จริง

มา implement functionality สำหรับการ return มากกว่าหน้าว่าง สร้างไฟล์ ใหม่ hello.html ที่ root ของไดเรกทอรี project ของคุณ ไม่ใช่ใน ไดเรกทอรี src คุณ input HTML ใดที่ต้องการได้ — Listing 21-4 แสดง หนึ่งความเป็นไปได้

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: ไฟล์ HTML ตัวอย่างที่จะ return ใน response

นี่คือเอกสาร HTML5 ขั้นต่ำกับ heading และ text เพื่อ return นี้จาก server เมื่อ request ถูกรับ เราจะแก้ handle_connection ดังที่แสดง ใน Listing 21-5 เพื่ออ่านไฟล์ HTML เพิ่มมันให้ response เป็น body และ ส่งมัน

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: ส่งเนื้อหาของ hello.html เป็น body ของ response

เราเพิ่ม fs ให้ statement use เพื่อนำโมดูล filesystem ของ standard library เข้า scope โค้ดสำหรับการอ่านเนื้อหาของไฟล์ไปยัง string ควรดู คุ้นเคย — เราใช้มันเมื่อเราอ่านเนื้อหาของไฟล์สำหรับ project I/O ของเรา ใน Listing 12-4

ถัดไป เราใช้ format! เพื่อเพิ่มเนื้อหาของไฟล์เป็น body ของ response success เพื่อรับประกัน response HTTP ที่ valid เราเพิ่ม header Content-Length ซึ่งตั้งเป็นขนาดของ body response ของเรา — ในกรณีนี้ ขนาดของ hello.html

รันโค้ดนี้ด้วย cargo run และโหลด 127.0.0.1:7878 ใน browser ของ คุณ — คุณควรเห็น HTML ของคุณ render!

ปัจจุบัน เรากำลัง ignore ข้อมูล request ใน http_request และเพียงส่ง กลับเนื้อหาของไฟล์ HTML โดยไม่มีเงื่อนไข นั่นหมายความว่าถ้าคุณลองขอ 127.0.0.1:7878/something-else ใน browser ของคุณ คุณจะยังได้ response HTML เดียวกันกลับ ที่ขณะนี้ server ของเรามีจำกัดมากและไม่ทำสิ่งที่ web server ส่วนใหญ่ทำ เราต้องการ customize response ของเราขึ้นกับ request และส่งกลับไฟล์ HTML เพียงสำหรับ request ที่ form ดีไปยัง /

Validate Request และ Response เลือก

ตอนนี้ web server ของเราจะ return HTML ในไฟล์ไม่ว่า client ขออะไร มา เพิ่ม functionality เพื่อ check ว่า browser กำลังขอ / ก่อน return ไฟล์ HTML และ return error ถ้า browser ขออื่นใด สำหรับสิ่งนี้เราต้อง แก้ handle_connection ดังที่แสดงใน Listing 21-6 โค้ดใหม่นี้ check เนื้อหาของ request ที่รับกับสิ่งที่เรารู้ว่า request สำหรับ / ดู เหมือน และเพิ่ม block if และ else เพื่อ treat request ต่างกัน

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: จัดการ request ไปยัง / ต่างจาก request อื่น

เราจะดูเพียงบรรทัดแรกของ HTTP request ดังนั้นแทนการอ่าน request ทั้งหมดเข้า vector เรากำลังเรียก next เพื่อได้ item แรกจาก iterator unwrap แรกดูแล Option และหยุดโปรแกรมถ้า iterator ไม่มี item unwrap ที่สองจัดการ Result และมีผลเหมือน unwrap ที่อยู่ใน map ที่เพิ่มใน Listing 21-2

ถัดไป เรา check request_line เพื่อดูว่ามันเท่ากับบรรทัด request ของ GET request ไปยัง path / ถ้ามัน block if return เนื้อหาของไฟล์ HTML ของเรา

ถ้า request_line ไม่ เท่ากับ GET request ไปยัง path / มันหมายถึง เรารับ request อื่นบางอย่าง เราจะเพิ่มโค้ดให้ block else ในชั่วครู่ เพื่อ respond ให้ request อื่นทั้งหมด

รันโค้ดนี้ตอนนี้และขอ 127.0.0.1:7878 — คุณควรได้ HTML ใน hello.html ถ้าคุณทำ request อื่นใด เช่น 127.0.0.1:7878/something-else คุณจะได้ error connection เช่นที่คุณเห็นเมื่อรันโค้ดใน Listing 21-1 และ Listing 21-2

ตอนนี้มาเพิ่มโค้ดใน Listing 21-7 ให้ block else เพื่อ return response กับ status code 404 ซึ่ง signal ว่าเนื้อหาสำหรับ request ไม่ พบ เรายัง return HTML บ้างสำหรับหน้าที่ render ใน browser บ่งบอก response กับ user สุดท้าย

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: Respond ด้วย status code 404 และหน้า error ถ้าอื่นใดนอกจาก / ถูกขอ

ที่นี่ response ของเรามีบรรทัด status กับ status code 404 และวลีเหตุผล NOT FOUND Body ของ response จะเป็น HTML ในไฟล์ 404.html คุณจะต้อง สร้างไฟล์ 404.html ข้าง ๆ hello.html สำหรับหน้า error อีกครั้ง รู้สึกอิสระที่จะใช้ HTML ใดที่คุณต้องการ หรือใช้ตัวอย่าง HTML ใน 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: เนื้อหาตัวอย่างสำหรับหน้าที่จะส่งกลับด้วย response 404 ใด

ด้วยการเปลี่ยนแปลงเหล่านี้ รัน server ของคุณอีก ขอ 127.0.0.1:7878 ควร return เนื้อหาของ hello.html และ request อื่นใด เช่น 127.0.0.1:7878/foo ควร return HTML error จาก 404.html

Refactoring

ที่ขณะนี้ block if และ else มีการซ้ำซ้อนเยอะ — พวกเขาทั้งสองอ่าน ไฟล์และเขียนเนื้อหาของไฟล์ไปยัง stream ความแตกต่างเดียวคือบรรทัด status และชื่อไฟล์ มาทำโค้ดให้กระชับมากขึ้นโดยดึงความแตกต่างเหล่านั้น ออกมาในบรรทัด if และ else แยกที่จะ assign ค่าของบรรทัด status และ ชื่อไฟล์ให้ตัวแปร — แล้วเราใช้ตัวแปรเหล่านั้นโดยไม่มีเงื่อนไขในโค้ด เพื่ออ่านไฟล์และเขียน response ได้ Listing 21-9 แสดงโค้ดผลลัพธ์หลัง การแทน block if และ else ใหญ่

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: Refactor block if และ else ให้บรรจุเพียงโค้ดที่ต่างระหว่างสองกรณี

ตอนนี้ block if และ else เพียง return ค่าเหมาะสมสำหรับบรรทัด status และชื่อไฟล์ใน tuple — เราแล้วใช้การ destructuring เพื่อ assign สองค่าเหล่านี้ให้ status_line และ filename โดยใช้ pattern ใน statement let ตามที่พูดถึงในบทที่ 19

โค้ดที่ duplicate ก่อนหน้าตอนนี้อยู่นอก block if และ else และใช้ ตัวแปร status_line และ filename นี่ทำให้ง่ายขึ้นในการเห็นความแตก ต่างระหว่างสองกรณี และมันหมายความว่าเรามีเพียงหนึ่งที่ที่จะอัพเดทโค้ด ถ้าเราต้องการเปลี่ยนวิธีที่การอ่านไฟล์และเขียน response ทำงาน พฤติกรรมของโค้ดใน Listing 21-9 จะเหมือนกับใน Listing 21-7

เจ๋ง! ตอนนี้เรามี web server ง่ายในประมาณ 40 บรรทัดของโค้ด Rust ที่ ตอบกับหนึ่ง request ด้วยหน้าของเนื้อหาและตอบกับ request อื่นทั้งหมด ด้วย response 404

ปัจจุบัน server ของเรารันในเธรดเดียว หมายความว่ามันสามารถ serve เพียง หนึ่ง request ในเวลาเดียว มาตรวจสอบว่านั่นเป็นปัญหาได้ยังไงโดยจำลอง request ช้าบ้าง แล้ว เราจะ fix มันเพื่อให้ server ของเราจัดการหลาย request พร้อมกันได้