Xây Dựng Một Web Server Đơn Luồng

Chúng ta sẽ bắt đầu bằng việc xây dựng một web server đơn luồng hoạt động. Trước khi bắt đầu, hãy xem qua tổng quan nhanh về các giao thức liên quan đến việc xây dựng web server. Chi tiết của các giao thức này nằm ngoài phạm vi của cuốn sách này, nhưng một tổng quan ngắn gọn sẽ cung cấp cho bạn thông tin cần thiết.

Hai giao thức chính liên quan đến web server là Hypertext Transfer Protocol (HTTP)Transmission Control Protocol (TCP). Cả hai giao thức đều là các giao thức yêu cầu-phản hồi, nghĩa là khách hàng (client) khởi tạo yêu cầu và máy chủ (server) lắng nghe các yêu cầu và cung cấp phản hồi cho khách hàng. Nội dung của các yêu cầu và phản hồi đó được định nghĩa bởi các giao thức.

TCP là giao thức cấp thấp hơn mô tả chi tiết về cách thông tin được truyền từ một máy chủ sang máy chủ khác nhưng không chỉ định thông tin đó là gì. HTTP xây dựng trên TCP bằng cách định nghĩa nội dung của các yêu cầu và phản hồi. Về mặt kỹ thuật, có thể sử dụng HTTP với các giao thức khác, nhưng trong phần lớn trường hợp, HTTP gửi dữ liệu qua TCP. Chúng ta sẽ làm việc với các byte thô của yêu cầu và phản hồi TCP và HTTP.

Lắng Nghe Kết Nối TCP

Web server của chúng ta cần lắng nghe kết nối TCP, vì vậy đó là phần đầu tiên mà chúng ta sẽ xử lý. Thư viện chuẩn cung cấp một module std::net cho phép chúng ta làm điều này. Hãy tạo một dự án mới theo cách thông thường:

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

Bây giờ nhập mã trong Listing 21-1 vào src/main.rs để bắt đầu. Mã này sẽ lắng nghe tại địa chỉ cục bộ 127.0.0.1:7878 để tìm các luồng TCP đến. Khi nó nhận được một luồng đến, nó sẽ in ra 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!");
    }
}

Sử dụng TcpListener, chúng ta có thể lắng nghe các kết nối TCP tại địa chỉ 127.0.0.1:7878. Trong địa chỉ, phần trước dấu hai chấm là địa chỉ IP đại diện cho máy tính của bạn (điều này giống nhau trên mọi máy tính và không đại diện cụ thể cho máy tính của tác giả), và 7878 là cổng. Chúng ta đã chọn cổng này vì hai lý do: HTTP thường không được chấp nhận trên cổng này nên server của chúng ta khó có thể xung đột với bất kỳ web server nào khác mà bạn có thể đang chạy trên máy của mình, và 7878 là rust được gõ trên điện thoại.

Hàm bind trong tình huống này hoạt động giống như hàm new vì nó sẽ trả về một thể hiện TcpListener mới. Hàm này được gọi là bind vì trong mạng, việc kết nối với một cổng để lắng nghe được gọi là "binding to a port" (liên kết với một cổng).

Hàm bind trả về một Result<T, E>, cho thấy rằng việc liên kết có thể thất bại. Ví dụ, kết nối với cổng 80 yêu cầu quyền quản trị viên (người dùng không phải quản trị viên chỉ có thể lắng nghe trên các cổng cao hơn 1023), vì vậy nếu chúng ta cố kết nối với cổng 80 mà không phải là quản trị viên, việc liên kết sẽ không hoạt động. Việc liên kết cũng sẽ không hoạt động, ví dụ, nếu chúng ta chạy hai thể hiện của chương trình và do đó có hai chương trình lắng nghe cùng một cổng. Bởi vì chúng ta đang viết một server cơ bản chỉ để học, chúng ta sẽ không lo lắng về việc xử lý các loại lỗi này; thay vào đó, chúng ta sử dụng unwrap để dừng chương trình nếu có lỗi xảy ra.

Phương thức incoming trên TcpListener trả về một iterator cung cấp cho chúng ta một chuỗi các luồng (cụ thể hơn, các luồng thuộc loại TcpStream). Một luồng (stream) đơn lẻ đại diện cho một kết nối mở giữa khách hàng và máy chủ. Một kết nối (connection) là tên cho toàn bộ quá trình yêu cầu và phản hồi trong đó khách hàng kết nối với máy chủ, máy chủ tạo ra phản hồi, và máy chủ đóng kết nối. Do đó, chúng ta sẽ đọc từ TcpStream để xem khách hàng đã gửi gì và sau đó viết phản hồi của chúng ta vào luồng để gửi dữ liệu trở lại cho khách hàng. Nhìn chung, vòng lặp for này sẽ xử lý từng kết nối lần lượt và tạo ra một chuỗi các luồng để chúng ta xử lý.

Hiện tại, việc xử lý luồng của chúng ta bao gồm việc gọi unwrap để chấm dứt chương trình của chúng ta nếu luồng có bất kỳ lỗi nào; nếu không có lỗi nào, chương trình sẽ in ra một thông báo. Chúng ta sẽ thêm chức năng cho trường hợp thành công trong danh sách tiếp theo. Lý do chúng ta có thể nhận được lỗi từ phương thức incoming khi khách hàng kết nối với máy chủ là vì chúng ta không thực sự lặp qua các kết nối. Thay vào đó, chúng ta đang lặp qua các nỗ lực kết nối. Kết nối có thể không thành công vì nhiều lý do, nhiều lý do phụ thuộc vào hệ điều hành. Ví dụ, nhiều hệ điều hành có giới hạn về số lượng kết nối đồng thời mở mà chúng có thể hỗ trợ; các nỗ lực kết nối mới vượt quá số đó sẽ tạo ra lỗi cho đến khi một số kết nối mở được đóng lại.

Hãy thử chạy mã này! Gọi cargo run trong terminal và sau đó tải 127.0.0.1:7878 trong trình duyệt web. Trình duyệt sẽ hiển thị một thông báo lỗi như "Connection reset" vì máy chủ hiện không gửi lại bất kỳ dữ liệu nào. Nhưng khi bạn nhìn vào terminal của mình, bạn sẽ thấy một số thông báo đã được in ra khi trình duyệt kết nối với máy chủ!

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

Đôi khi bạn sẽ thấy nhiều thông báo được in ra cho một yêu cầu trình duyệt; lý do có thể là trình duyệt đang gửi yêu cầu cho trang cũng như yêu cầu cho các tài nguyên khác, như biểu tượng favicon.ico xuất hiện trong tab trình duyệt.

Cũng có thể là trình duyệt đang cố gắng kết nối với máy chủ nhiều lần vì máy chủ không phản hồi với bất kỳ dữ liệu nào. Khi stream ra khỏi phạm vi và bị drop vào cuối vòng lặp, kết nối sẽ bị đóng như một phần của việc triển khai drop. Trình duyệt đôi khi xử lý các kết nối bị đóng bằng cách thử lại, vì vấn đề có thể là tạm thời.

Trình duyệt đôi khi cũng mở nhiều kết nối đến máy chủ mà không gửi bất kỳ yêu cầu nào, để nếu sau đó họ gửi yêu cầu, chúng có thể xảy ra nhanh hơn. Khi điều này xảy ra, máy chủ của chúng ta sẽ thấy mỗi kết nối, bất kể có bất kỳ yêu cầu nào qua kết nối đó hay không. Nhiều phiên bản của trình duyệt dựa trên Chrome làm điều này, ví dụ; bạn có thể tắt tối ưu hóa đó bằng cách sử dụng chế độ duyệt web riêng tư hoặc sử dụng trình duyệt khác.

Yếu tố quan trọng là chúng ta đã thành công trong việc có được một handle cho kết nối TCP!

Hãy nhớ dừng chương trình bằng cách nhấn ctrl-c khi bạn đã chạy xong một phiên bản cụ thể của mã. Sau đó khởi động lại chương trình bằng cách gọi lệnh cargo run sau khi bạn đã thực hiện mỗi bộ thay đổi mã để đảm bảo bạn đang chạy mã mới nhất.

Đọc Yêu Cầu

Hãy triển khai chức năng để đọc yêu cầu từ trình duyệt! Để tách biệt các mối quan tâm về việc đầu tiên là nhận được kết nối và sau đó thực hiện một số hành động với kết nối, chúng ta sẽ bắt đầu một hàm mới để xử lý các kết nối. Trong hàm handle_connection mới này, chúng ta sẽ đọc dữ liệu từ luồng TCP và in nó ra để chúng ta có thể thấy dữ liệu được gửi từ trình duyệt. Thay đổi mã để trông giống như 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:#?}");
}

Chúng ta đưa std::io::preludestd::io::BufReader vào phạm vi để có quyền truy cập vào các traits và kiểu cho phép chúng ta đọc từ và viết vào luồng. Trong vòng lặp for trong hàm main, thay vì in một thông báo cho biết chúng ta đã thiết lập kết nối, bây giờ chúng ta gọi hàm handle_connection mới và truyền stream vào nó.

Trong hàm handle_connection, chúng ta tạo một thể hiện BufReader mới bao bọc một tham chiếu đến stream. BufReader thêm bộ đệm bằng cách quản lý các cuộc gọi đến các phương thức của trait std::io::Read cho chúng ta.

Chúng ta tạo một biến có tên http_request để thu thập các dòng của yêu cầu mà trình duyệt gửi đến máy chủ của chúng ta. Chúng ta chỉ ra rằng chúng ta muốn thu thập các dòng này trong một vector bằng cách thêm chú thích kiểu Vec<_>.

BufReader triển khai trait std::io::BufRead, cung cấp phương thức lines. Phương thức lines trả về một iterator của Result<String, std::io::Error> bằng cách chia luồng dữ liệu bất cứ khi nào nó thấy một byte xuống dòng. Để lấy mỗi String, chúng ta map và unwrap mỗi Result. Result có thể là một lỗi nếu dữ liệu không phải là UTF-8 hợp lệ hoặc nếu có vấn đề khi đọc từ luồng. Một lần nữa, một chương trình sản xuất nên xử lý các lỗi này một cách thanh lịch hơn, nhưng chúng ta chọn dừng chương trình trong trường hợp lỗi để đơn giản.

Trình duyệt báo hiệu kết thúc của một yêu cầu HTTP bằng cách gửi hai ký tự xuống dòng liên tiếp, vì vậy để nhận một yêu cầu từ luồng, chúng ta lấy các dòng cho đến khi chúng ta nhận được một dòng là chuỗi rỗng. Một khi chúng ta đã thu thập các dòng vào vector, chúng ta in chúng ra bằng cách sử dụng định dạng gỡ lỗi đẹp để chúng ta có thể xem xét các hướng dẫn mà trình duyệt web đang gửi đến máy chủ của chúng ta.

Hãy thử mã này! Khởi động chương trình và tạo yêu cầu trong trình duyệt web một lần nữa. Lưu ý rằng chúng ta vẫn sẽ nhận được một trang lỗi trong trình duyệt, nhưng đầu ra của chương trình của chúng ta trong terminal bây giờ sẽ trông giống như thế này:

$ 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",
]

Tùy thuộc vào trình duyệt của bạn, bạn có thể nhận được đầu ra hơi khác. Bây giờ chúng ta đang in dữ liệu yêu cầu, chúng ta có thể thấy tại sao chúng ta nhận được nhiều kết nối từ một yêu cầu trình duyệt bằng cách nhìn vào đường dẫn sau GET trong dòng đầu tiên của yêu cầu. Nếu các kết nối lặp lại đều yêu cầu /, chúng ta biết rằng trình duyệt đang cố gắng tìm nạp / nhiều lần vì nó không nhận được phản hồi từ chương trình của chúng ta.

Hãy phân tích dữ liệu yêu cầu này để hiểu những gì trình duyệt đang yêu cầu từ chương trình của chúng ta.

Xem Xét Kỹ Hơn về Yêu Cầu HTTP

HTTP là một giao thức dựa trên văn bản, và một yêu cầu có định dạng này:

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

Dòng đầu tiên là dòng yêu cầu (request line) chứa thông tin về những gì khách hàng đang yêu cầu. Phần đầu tiên của dòng yêu cầu chỉ ra phương thức (method) được sử dụng, chẳng hạn như GET hoặc POST, mô tả cách khách hàng đang thực hiện yêu cầu này. Khách hàng của chúng ta sử dụng yêu cầu GET, có nghĩa là nó đang yêu cầu thông tin.

Phần tiếp theo của dòng yêu cầu là /, chỉ ra bộ nhận dạng tài nguyên thống nhất (URI) mà khách hàng đang yêu cầu: một URI gần như, nhưng không hoàn toàn, giống như định vị tài nguyên thống nhất (URL). Sự khác biệt giữa URI và URL không quan trọng cho mục đích của chúng ta trong chương này, nhưng đặc tả HTTP sử dụng thuật ngữ URI, vì vậy chúng ta có thể chỉ thay thế URL cho URI ở đây.

Phần cuối cùng là phiên bản HTTP mà khách hàng sử dụng, và sau đó dòng yêu cầu kết thúc bằng một chuỗi CRLF. (CRLF là viết tắt của carriage returnline feed, là các thuật ngữ từ thời máy đánh chữ!) Chuỗi CRLF cũng có thể được viết là \r\n, trong đó \r là carriage return và \n là line feed. Chuỗi CRLF phân tách dòng yêu cầu khỏi phần còn lại của dữ liệu yêu cầu. Lưu ý rằng khi CRLF được in, chúng ta thấy một dòng mới bắt đầu thay vì \r\n.

Nhìn vào dữ liệu dòng yêu cầu mà chúng ta đã nhận được từ việc chạy chương trình của mình cho đến nay, chúng ta thấy rằng GET là phương thức, / là URI yêu cầu, và HTTP/1.1 là phiên bản.

Sau dòng yêu cầu, các dòng còn lại bắt đầu từ Host: trở đi là các header. Yêu cầu GET không có phần thân.

Hãy thử tạo một yêu cầu từ một trình duyệt khác hoặc yêu cầu một địa chỉ khác, chẳng hạn như 127.0.0.1:7878/test, để xem dữ liệu yêu cầu thay đổi như thế nào.

Bây giờ chúng ta biết trình duyệt đang yêu cầu gì, hãy gửi lại một số dữ liệu!

Viết Phản Hồi

Chúng ta sẽ triển khai việc gửi dữ liệu để đáp ứng yêu cầu của khách hàng. Các phản hồi có định dạng sau:

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

Dòng đầu tiên là dòng trạng thái (status line) chứa phiên bản HTTP được sử dụng trong phản hồi, một mã trạng thái số tóm tắt kết quả của yêu cầu, và một cụm từ lý do cung cấp mô tả văn bản của mã trạng thái. Sau chuỗi CRLF là bất kỳ header nào, một chuỗi CRLF khác, và thân của phản hồi.

Dưới đây là một ví dụ về phản hồi sử dụng phiên bản HTTP 1.1, có mã trạng thái 200, cụm từ lý do OK, không có header, và không có thân:

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

Mã trạng thái 200 là phản hồi thành công tiêu chuẩn. Văn bản là một phản hồi HTTP thành công nhỏ. Hãy viết điều này vào luồng như phản hồi của chúng ta cho một yêu cầu thành công! Từ hàm handle_connection, xóa println! đã in dữ liệu yêu cầu và thay thế nó bằng mã trong 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();
}

Dòng mới đầu tiên định nghĩa biến response giữ dữ liệu của thông báo thành công. Sau đó chúng ta gọi as_bytes trên response để chuyển đổi dữ liệu chuỗi thành byte. Phương thức write_all trên stream nhận một &[u8] và gửi các byte đó trực tiếp qua kết nối. Vì thao tác write_all có thể thất bại, chúng ta sử dụng unwrap trên bất kỳ kết quả lỗi nào như trước đây. Một lần nữa, trong một ứng dụng thực tế, bạn sẽ thêm xử lý lỗi ở đây.

Với những thay đổi này, hãy chạy mã của chúng ta và tạo một yêu cầu. Chúng ta không còn in bất kỳ dữ liệu nào vào terminal, vì vậy chúng ta sẽ không thấy bất kỳ đầu ra nào ngoài đầu ra từ Cargo. Khi bạn tải 127.0.0.1:7878 trong trình duyệt web, bạn sẽ nhận được một trang trống thay vì lỗi. Bạn vừa viết mã bằng tay để nhận yêu cầu HTTP và gửi phản hồi!

Trả về HTML Thực Sự

Hãy triển khai chức năng để trả về nhiều hơn một trang trống. Tạo file mới hello.html trong thư mục gốc của dự án của bạn, không phải trong thư mục src. Bạn có thể nhập bất kỳ HTML nào bạn muốn; Listing 21-4 hiển thị một khả năng.

<!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>

Đây là một tài liệu HTML5 tối thiểu với một tiêu đề và một số văn bản. Để trả về từ máy chủ khi một yêu cầu được nhận, chúng ta sẽ sửa đổi handle_connection như trong Listing 21-5 để đọc file HTML, thêm nó vào phản hồi dưới dạng thân, và gửi nó.

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();
}

Chúng ta đã thêm fs vào câu lệnh use để đưa module filesystem của thư viện chuẩn vào phạm vi. Mã để đọc nội dung của một file vào một chuỗi sẽ trông quen thuộc; chúng ta đã sử dụng nó khi chúng ta đọc nội dung của một file cho dự án I/O của chúng ta trong Listing 12-4.

Tiếp theo, chúng ta sử dụng format! để thêm nội dung của file dưới dạng thân của phản hồi thành công. Để đảm bảo một phản hồi HTTP hợp lệ, chúng ta thêm header Content-Length được thiết lập bằng kích thước của thân phản hồi, trong trường hợp này là kích thước của hello.html.

Chạy mã này với cargo run và tải 127.0.0.1:7878 trong trình duyệt của bạn; bạn sẽ thấy HTML của bạn được hiển thị!

Hiện tại, chúng ta đang bỏ qua dữ liệu yêu cầu trong http_request và chỉ đơn giản gửi lại nội dung của file HTML một cách vô điều kiện. Điều đó có nghĩa là nếu bạn thử yêu cầu 127.0.0.1:7878/something-else trong trình duyệt của bạn, bạn vẫn sẽ nhận lại cùng một phản hồi HTML. Ở thời điểm hiện tại, máy chủ của chúng ta rất hạn chế và không làm những gì hầu hết các máy chủ web làm. Chúng ta muốn tùy chỉnh các phản hồi của mình tùy thuộc vào yêu cầu và chỉ gửi lại file HTML cho một yêu cầu được định dạng tốt tới /.

Xác Thực Yêu Cầu và Phản Hồi Có Chọn Lọc

Hiện tại, máy chủ web của chúng ta sẽ trả về HTML trong file bất kể khách hàng yêu cầu gì. Hãy thêm chức năng để kiểm tra xem trình duyệt có yêu cầu / trước khi trả về file HTML và trả về lỗi nếu trình duyệt yêu cầu bất cứ thứ gì khác. Để làm điều này, chúng ta cần sửa đổi handle_connection, như trong Listing 21-6. Mã mới này kiểm tra nội dung của yêu cầu nhận được so với những gì chúng ta biết một yêu cầu cho / trông như thế nào và thêm các khối ifelse để xử lý các yêu cầu khác nhau.

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
    }
}

Chúng ta chỉ xem xét dòng đầu tiên của yêu cầu HTTP, vì vậy thay vì đọc toàn bộ yêu cầu vào vector, chúng ta đang gọi next để lấy mục đầu tiên từ iterator. unwrap đầu tiên xử lý Option và dừng chương trình nếu iterator không có mục nào. unwrap thứ hai xử lý Result và có cùng tác dụng với unwrap đã được thêm vào map trong Listing 21-2.

Tiếp theo, chúng ta kiểm tra request_line để xem nó có bằng với dòng yêu cầu của một yêu cầu GET đến đường dẫn / không. Nếu có, khối if trả về nội dung của file HTML của chúng ta.

Nếu request_line không bằng với yêu cầu GET đến đường dẫn /, có nghĩa là chúng ta đã nhận được một số yêu cầu khác. Chúng ta sẽ thêm mã vào khối else ngay bây giờ để phản hồi tất cả các yêu cầu khác.

Chạy mã này bây giờ và yêu cầu 127.0.0.1:7878; bạn sẽ nhận được HTML trong hello.html. Nếu bạn thực hiện bất kỳ yêu cầu nào khác, chẳng hạn như 127.0.0.1:7878/something-else, bạn sẽ nhận được lỗi kết nối giống như những lỗi bạn đã thấy khi chạy mã trong Listing 21-1 và Listing 21-2.

Bây giờ hãy thêm mã trong Listing 21-7 vào khối else để trả về phản hồi với mã trạng thái 404, báo hiệu rằng nội dung cho yêu cầu không được tìm thấy. Chúng ta cũng sẽ trả về một số HTML cho một trang để hiển thị trong trình duyệt cho biết phản hồi cho người dùng cuối.

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();
    }
}

Ở đây, phản hồi của chúng ta có một dòng trạng thái với mã trạng thái 404 và cụm từ lý do NOT FOUND. Thân của phản hồi sẽ là HTML trong file 404.html. Bạn sẽ cần tạo một file 404.html cạnh hello.html cho trang lỗi; một lần nữa, hãy tự do sử dụng bất kỳ HTML nào bạn muốn hoặc sử dụng HTML mẫu trong 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>

Với những thay đổi này, hãy chạy lại máy chủ của bạn. Yêu cầu 127.0.0.1:7878 sẽ trả về nội dung của hello.html, và bất kỳ yêu cầu nào khác, như 127.0.0.1:7878/foo, sẽ trả về HTML lỗi từ 404.html.

Một Chút Tái Cấu Trúc

Tại thời điểm này, các khối ifelse có nhiều sự lặp lại: cả hai đều đang đọc các file và viết nội dung của các file vào luồng. Những khác biệt duy nhất là dòng trạng thái và tên file. Hãy làm cho mã ngắn gọn hơn bằng cách rút ra những khác biệt đó thành các dòng ifelse riêng biệt sẽ gán giá trị của dòng trạng thái và tên file cho các biến; sau đó chúng ta có thể sử dụng vô điều kiện các biến đó trong mã để đọc file và viết phản hồi. Listing 21-9 hiển thị mã kết quả sau khi thay thế các khối ifelse lớn.

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();
}

Bây giờ các khối ifelse chỉ trả về các giá trị thích hợp cho dòng trạng thái và tên file trong một tuple; sau đó chúng ta sử dụng destructuring để gán hai giá trị này cho status_linefilename bằng cách sử dụng một mẫu trong câu lệnh let, như đã thảo luận trong Chương 19.

Mã đã được lặp lại trước đây hiện nằm ngoài các khối ifelse và sử dụng các biến status_linefilename. Điều này làm cho nó dễ dàng hơn để thấy sự khác biệt giữa hai trường hợp, và có nghĩa là chúng ta chỉ có một nơi để cập nhật mã nếu chúng ta muốn thay đổi cách đọc file và viết phản hồi. Hành vi của mã trong Listing 21-9 sẽ giống với mã trong Listing 21-7.

Tuyệt vời! Bây giờ chúng ta có một máy chủ web đơn giản trong khoảng 40 dòng mã Rust phản hồi một yêu cầu với một trang nội dung và phản hồi tất cả các yêu cầu khác với phản hồi 404.

Hiện tại, máy chủ của chúng ta chạy trong một luồng duy nhất, nghĩa là nó chỉ có thể phục vụ một yêu cầu tại một thời điểm. Hãy xem xét làm thế nào điều đó có thể là một vấn đề bằng cách mô phỏng một số yêu cầu chậm. Sau đó, chúng ta sẽ khắc phục nó để máy chủ của chúng ta có thể xử lý nhiều yêu cầu cùng một lúc.