Budowanie jednowątkowego serwera WWW
Zaczniemy od uruchomienia jednowątkowego serwera WWW. Zanim zaczniemy, spójrzmy na szybki przegląd protokołów zaangażowanych w budowanie serwerów WWW. Szczegóły tych protokołów wykraczają poza zakres tej książki, ale krótki przegląd dostarczy ci niezbędnych informacji.
Dwa główne protokoły zaangażowane w serwery WWW to Hypertext Transfer Protocol (HTTP) i Transmission Control Protocol (TCP). Oba protokoły są protokołami typu żądanie-odpowiedź, co oznacza, że klient inicjuje żądania, a serwer nasłuchuje żądań i dostarcza klientowi odpowiedź. Zawartość tych żądań i odpowiedzi jest definiowana przez protokoły.
TCP to protokół niższego poziomu, który opisuje szczegóły, jak informacje trafiają z jednego serwera do drugiego, ale nie precyzuje, czym są te informacje. HTTP buduje na TCP, definiując zawartość żądań i odpowiedzi. Technicznie możliwe jest używanie HTTP z innymi protokołami, ale w zdecydowanej większości przypadków HTTP wysyła swoje dane przez TCP. Będziemy pracować z surowymi bajtami żądań i odpowiedzi TCP i HTTP.
Nasłuchiwanie połączeń TCP
Nasz serwer WWW musi nasłuchiwać połączeń TCP, więc to jest pierwsza część, nad którą będziemy pracować. Biblioteka standardowa oferuje moduł std::net, który nam to umożliwia. Stwórzmy nowy projekt w zwykły sposób:
$ cargo new hello
Utworzono projekt binarny (aplikacja) `hello`
$ cd hello
Teraz wprowadź kod z Listingu 21-1 do src/main.rs, aby rozpocząć. Ten kod będzie nasłuchiwał przy lokalnym adresie 127.0.0.1:7878 na przychodzące strumienie TCP. Kiedy otrzyma przychodzący strumień, wydrukuje 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!");
}
}
Używając TcpListener, możemy nasłuchiwać połączeń TCP pod adresem 127.0.0.1:7878. W adresie, sekcja przed dwukropkiem to adres IP reprezentujący twój komputer (jest taki sam na każdym komputerze i nie reprezentuje konkretnie komputera autorów), a 7878 to port. Wybraliśmy ten port z dwóch powodów: HTTP nie jest zwykle akceptowany na tym porcie, więc nasz serwer raczej nie będzie kolidował z żadnym innym serwerem WWW, który możesz mieć uruchomiony na swojej maszynie, a 7878 to rust wpisane na telefonie.
Funkcja bind w tym scenariuszu działa jak funkcja new w tym sensie, że zwróci nową instancję TcpListener. Funkcja nazywa się bind, ponieważ w sieciach połączenie z portem w celu nasłuchiwania jest znane jako „wiązanie się z portem”.
Funkcja bind zwraca Result<T, E>, co wskazuje, że wiązanie może się nie powieść, na przykład, gdybyśmy uruchomili dwie instancje naszego programu i tym samym mieli dwa programy nasłuchujące na tym samym porcie. Ponieważ piszemy podstawowy serwer wyłącznie w celach edukacyjnych, nie będziemy martwić się o obsługę tego typu błędów; zamiast tego używamy unwrap, aby zatrzymać program, jeśli wystąpią błędy.
Metoda incoming na TcpListener zwraca iterator, który dostarcza nam sekwencję strumieni (dokładniej, strumieni typu TcpStream). Pojedynczy strumień reprezentuje otwarte połączenie między klientem a serwerem. Połączenie to nazwa pełnego procesu żądanie-odpowiedź, w którym klient łączy się z serwerem, serwer generuje odpowiedź i serwer zamyka połączenie. W związku z tym będziemy czytać z TcpStream, aby zobaczyć, co klient wysłał, a następnie zapiszemy naszą odpowiedź do strumienia, aby wysłać dane z powrotem do klienta. Ogólnie rzecz biorąc, ta pętla for będzie przetwarzać każde połączenie po kolei i generować serię strumieni, które będziemy obsługiwać.
Na razie nasza obsługa strumienia polega na wywołaniu unwrap, aby zakończyć program, jeśli strumień ma jakieś błędy; jeśli błędów nie ma, program wyświetla komunikat. W następnym listingu dodamy więcej funkcjonalności dla przypadku sukcesu. Powodem, dla którego możemy otrzymać błędy z metody incoming, gdy klient łączy się z serwerem, jest to, że nie iterujemy faktycznie po połączeniach. Zamiast tego iterujemy po próbach połączenia. Połączenie może się nie powieść z wielu powodów, z których wiele jest specyficznych dla systemu operacyjnego. Na przykład, wiele systemów operacyjnych ma limit liczby jednoczesnych otwartych połączeń, które mogą obsługiwać; nowe próby połączenia powyżej tej liczby będą generować błąd, dopóki niektóre z otwartych połączeń nie zostaną zamknięte.
Spróbujmy uruchomić ten kod! Wywołaj cargo run w terminalu, a następnie załaduj 127.0.0.1:7878 w przeglądarce internetowej. Przeglądarka powinna wyświetlić komunikat o błędzie, taki jak „Connection reset”, ponieważ serwer obecnie nie wysyła żadnych danych. Ale kiedy spojrzysz na swój terminal, powinieneś zobaczyć kilka komunikatów, które zostały wydrukowane, gdy przeglądarka połączyła się z serwerem!
Uruchamianie `target/debug/hello`
Połączenie nawiązane!
Połączenie nawiązane!
Połączenie nawiązane!
Czasami zobaczysz wiele komunikatów wydrukowanych dla jednego żądania przeglądarki; powodem może być to, że przeglądarka wysyła żądanie o stronę, a także żądanie o inne zasoby, takie jak ikona favicon.ico, która pojawia się w zakładce przeglądarki.
Może się również zdarzyć, że przeglądarka próbuje połączyć się z serwerem wielokrotnie, ponieważ serwer nie odpowiada żadnymi danymi. Gdy stream wychodzi poza zakres i jest porzucony na końcu pętli, połączenie jest zamykane w ramach implementacji drop. Przeglądarki czasami radzą sobie z zamkniętymi połączeniami, ponawiając próbę, ponieważ problem może być tymczasowy.
Przeglądarki czasami otwierają również wiele połączeń z serwerem bez wysyłania żadnych żądań, aby w przypadku późniejszego wysłania żądań, te żądania mogły nastąpić szybciej. Kiedy to nastąpi, nasz serwer zobaczy każde połączenie, niezależnie od tego, czy istnieją jakiekolwiek żądania przez to połączenie. Robi tak wiele wersji przeglądarek opartych na Chrome, na przykład; możesz wyłączyć tę optymalizację, używając trybu przeglądania prywatnego lub innej przeglądarki.
Ważnym czynnikiem jest to, że z powodzeniem uzyskaliśmy uchwyt do połączenia TCP!
Pamiętaj, aby zatrzymać program, naciskając ctrl-C, gdy skończysz uruchamiać określoną wersję kodu. Następnie uruchom program ponownie, wywołując polecenie cargo run po wprowadzeniu każdej serii zmian w kodzie, aby upewnić się, że uruchamiasz najnowszy kod.
Odczytywanie żądania
Zaimplementujmy funkcjonalność do odczytywania żądania z przeglądarki! Aby oddzielić kwestie nawiązania połączenia od wykonania jakiejś akcji z nim związanej, rozpoczniemy nową funkcję do przetwarzania połączeń. W tej nowej funkcji handle_connection będziemy czytać dane ze strumienia TCP i drukować je, abyśmy mogli zobaczyć dane wysyłane z przeglądarki. Zmień kod, aby wyglądał jak na Listingu 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!("Żądanie: {http_request:#?}");
}
Wprowadzamy std::io::BufReader i std::io::prelude do zakresu, aby uzyskać dostęp do cech i typów, które pozwalają nam czytać i zapisywać do strumienia. W pętli for w funkcji main, zamiast wyświetlać komunikat informujący o nawiązaniu połączenia, wywołujemy teraz nową funkcję handle_connection i przekazujemy jej stream.
W funkcji handle_connection tworzymy nową instancję BufReader, która opakowuje referencję do stream. BufReader dodaje buforowanie, zarządzając za nas wywołaniami metod cechy std::io::Read.
Tworzymy zmienną o nazwie http_request, aby zbierać linie żądania, które przeglądarka wysyła do naszego serwera. Wskazujemy, że chcemy zebrać te linie w wektorze, dodając adnotację typu Vec<_>.
BufReader implementuje cechę std::io::BufRead, która dostarcza metodę lines. Metoda lines zwraca iterator Result<String, std::io::Error>, dzieląc strumień danych za każdym razem, gdy zobaczy znak nowej linii. Aby uzyskać każdy String, mapujemy i unwrapujemy każdy Result. Result może być błędem, jeśli dane nie są prawidłowym UTF-8 lub jeśli wystąpił problem podczas czytania ze strumienia. Ponownie, program produkcyjny powinien obsługiwać te błędy bardziej elegancko, ale my decydujemy się na zatrzymanie programu w przypadku błędu dla uproszczenia.
Przeglądarka sygnalizuje koniec żądania HTTP, wysyłając dwa znaki nowej linii z rzędu, więc aby uzyskać jedno żądanie ze strumienia, pobieramy linie, dopóki nie otrzymamy linii, która jest pustym ciągiem znaków. Po zebraniu linii do wektora, drukujemy je, używając ładnego formatowania debugowania, abyśmy mogli przyjrzeć się instrukcjom, które przeglądarka internetowa wysyła do naszego serwera.
Spróbujmy tego kodu! Uruchom program i ponownie wyślij żądanie w przeglądarce internetowej. Zauważ, że nadal otrzymamy stronę błędu w przeglądarce, ale wynik naszego programu w terminalu będzie teraz wyglądał podobnie do tego:
$ cargo run
Kompilowanie hello v0.1.0 (file:///projects/hello)
Zakończono `dev` profil [nieoptymalny + debuginfo] cel(e) w 0.42s
Uruchamianie `target/debug/hello`
Żądanie: [
"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",
]
W zależności od przeglądarki, możesz otrzymać nieco inny wynik. Teraz, gdy drukujemy dane żądania, możemy zobaczyć, dlaczego otrzymujemy wiele połączeń z jednego żądania przeglądarki, patrząc na ścieżkę po GET w pierwszej linii żądania. Jeśli powtarzające się połączenia wszystkie żądają /, wiemy, że przeglądarka próbuje wielokrotnie pobrać /, ponieważ nie otrzymuje odpowiedzi z naszego programu.
Przeanalizujmy te dane żądania, aby zrozumieć, o co przeglądarka prosi nasz program.
Bliższe spojrzenie na żądanie HTTP
HTTP to protokół tekstowy, a żądanie ma następujący format:
Metoda Identyfikator-żądania Wersja-HTTP CRLF
nagłówki CRLF
ciało-wiadomości
Pierwsza linia to linia żądania, która zawiera informacje o tym, czego klient żąda. Pierwsza część linii żądania wskazuje używaną metodę, taką jak GET lub POST, która opisuje, w jaki sposób klient wysyła to żądanie. Nasz klient użył żądania GET, co oznacza, że prosi o informacje.
Następną częścią linii żądania jest /, która wskazuje uniform resource identifier (URI), o który prosi klient: URI jest prawie, ale niezupełnie, tym samym co uniform resource locator (URL). Różnica między URI a URL nie jest istotna dla naszych celów w tym rozdziale, ale specyfikacja HTTP używa terminu URI, więc możemy tutaj po prostu mentalnie zastąpić URL przez URI.
Ostatnią częścią jest wersja HTTP używana przez klienta, a następnie linia żądania kończy się sekwencją CRLF. (CRLF to skrót od carriage return i line feed, co są terminami z czasów maszyn do pisania!) Sekwencję CRLF można również zapisać jako \r\n, gdzie \r to znak powrotu karetki, a \n to znak nowej linii. Sekwencja CRLF oddziela linię żądania od reszty danych żądania. Zauważ, że gdy CRLF jest drukowane, widzimy początek nowej linii, a nie \r\n.
Patrząc na dane linii żądania, które otrzymaliśmy po uruchomieniu naszego programu, widzimy, że GET to metoda, / to URI żądania, a HTTP/1.1 to wersja.
Po linii żądania, pozostałe linie zaczynające się od Host: to nagłówki. Żądania GET nie mają ciała.
Spróbuj wysłać żądanie z innej przeglądarki lub poprosić o inny adres, na przykład 127.0.0.1:7878/test, aby zobaczyć, jak zmieniają się dane żądania.
Teraz, gdy wiemy, o co prosi przeglądarka, wyślijmy z powrotem jakieś dane!
Pisanie odpowiedzi
Zaimplementujemy wysyłanie danych w odpowiedzi na żądanie klienta. Odpowiedzi mają następujący format:
Wersja-HTTP Kod-statusu Fraza-powodowa CRLF
nagłówki CRLF
ciało-wiadomości
Pierwsza linia to linia statusu, która zawiera wersję HTTP używaną w odpowiedzi, numeryczny kod statusu, który podsumowuje wynik żądania, oraz frazę powodową, która zawiera tekstowy opis kodu statusu. Po sekwencji CRLF następują nagłówki, kolejna sekwencja CRLF i treść odpowiedzi.
Poniżej znajduje się przykładowa odpowiedź, która używa protokołu HTTP w wersji 1.1 i ma kod statusu 200, frazę „OK”, brak nagłówków i brak treści:
HTTP/1.1 200 OK\r\n\r\n
Kod statusu 200 to standardowa odpowiedź sukcesu. Tekst to mała, udana odpowiedź HTTP. Zapiszmy to do strumienia jako naszą odpowiedź na udane żądanie! Z funkcji handle_connection usuń println!, które drukowało dane żądania, i zastąp je kodem z Listingu 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();
}
Pierwsza nowa linia definiuje zmienną response, która przechowuje dane wiadomości o sukcesie. Następnie wywołujemy as_bytes na naszym response, aby przekonwertować dane ciągu na bajty. Metoda write_all na stream przyjmuje &[u8] i wysyła te bajty bezpośrednio przez połączenie. Ponieważ operacja write_all może zakończyć się niepowodzeniem, używamy unwrap dla każdego wyniku błędu, tak jak poprzednio. Ponownie, w prawdziwej aplikacji, należałoby tutaj dodać obsługę błędów.
Dzięki tym zmianom, uruchommy nasz kod i wyślijmy żądanie. Nie drukujemy już żadnych danych do terminala, więc nie zobaczymy żadnego wyniku poza danymi z Cargo. Gdy załadujesz 127.0.0.1:7878 w przeglądarce internetowej, powinieneś otrzymać pustą stronę zamiast błędu. Właśnie ręcznie zakodowałeś odbieranie żądania HTTP i wysyłanie odpowiedzi!
Zwracanie prawdziwego kodu HTML
Zaimplementujmy funkcjonalność zwracania czegoś więcej niż pustej strony. Utwórz nowy plik hello.html w katalogu głównym swojego projektu, a nie w katalogu src. Możesz wpisać dowolny kod HTML; Listing 21-4 pokazuje jedną z możliwości.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Witaj!</title>
</head>
<body>
<h1>Witaj!</h1>
<p>Cześć z Rust</p>
</body>
</html>
Jest to minimalny dokument HTML5 z nagłówkiem i tekstem. Aby zwrócić go z serwera po otrzymaniu żądania, zmodyfikujemy handle_connection, jak pokazano na Listingu 21-5, aby odczytać plik HTML, dodać go do odpowiedzi jako ciało i wysłać.
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();
}
Dodaliśmy fs do instrukcji use, aby wprowadzić moduł systemu plików biblioteki standardowej do zakresu. Kod do odczytywania zawartości pliku do ciągu znaków powinien wyglądać znajomo; użyliśmy go, gdy czytaliśmy zawartość pliku dla naszego projektu I/O w Listingu 12-4.
Następnie używamy format! do dodania zawartości pliku jako treści odpowiedzi sukcesu. Aby zapewnić prawidłową odpowiedź HTTP, dodajemy nagłówek Content-Length, który jest ustawiony na rozmiar naszej treści odpowiedzi – w tym przypadku, rozmiar hello.html.
Uruchom ten kod za pomocą cargo run i załaduj 127.0.0.1:7878 w swojej przeglądarce; powinieneś zobaczyć wyrenderowany HTML!
Obecnie ignorujemy dane żądania w http_request i po prostu bezwarunkowo wysyłamy z powrotem zawartość pliku HTML. Oznacza to, że jeśli spróbujesz zażądać 127.0.0.1:7878/cos-innego w przeglądarce, nadal otrzymasz tę samą odpowiedź HTML. W tej chwili nasz serwer jest bardzo ograniczony i nie robi tego, co robi większość serwerów WWW. Chcemy dostosować nasze odpowiedzi w zależności od żądania i wysyłać plik HTML tylko dla dobrze sformułowanego żądania do /.
Walidacja żądania i selektywne odpowiadanie
W tej chwili nasz serwer WWW zwróci HTML w pliku niezależnie od tego, co klient zażądał. Dodajmy funkcjonalność, aby sprawdzić, czy przeglądarka żąda / przed zwróceniem pliku HTML i aby zwrócić błąd, jeśli przeglądarka zażąda czegoś innego. W tym celu musimy zmodyfikować handle_connection, jak pokazano na Listingu 21-6. Ten nowy kod sprawdza zawartość otrzymanego żądania w porównaniu z tym, jak wygląda żądanie do /, i dodaje bloki if i else, aby traktować żądania inaczej.
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 {
// inne żądanie
}
}
Będziemy tylko patrzeć na pierwszą linię żądania HTTP, więc zamiast odczytywać całe żądanie do wektora, wywołujemy next, aby pobrać pierwszy element z iteratora. Pierwszy unwrap zajmuje się Option i zatrzymuje program, jeśli iterator nie ma żadnych elementów. Drugi unwrap obsługuje Result i ma taki sam efekt jak unwrap, który został dodany w map w Listingu 21-2.
Następnie sprawdzamy request_line, aby zobaczyć, czy jest równa linii żądania GET do ścieżki /. Jeśli tak, blok if zwraca zawartość naszego pliku HTML.
Jeśli request_line nie jest równe żądaniu GET do ścieżki /, oznacza to, że otrzymaliśmy jakieś inne żądanie. Za chwilę dodamy kod do bloku else, aby odpowiedzieć na wszystkie inne żądania.
Uruchom ten kod teraz i zażądaj 127.0.0.1:7878; powinieneś otrzymać HTML z hello.html. Jeśli wyślesz jakiekolwiek inne żądanie, takie jak 127.0.0.1:7878/cos-innego, otrzymasz błąd połączenia, podobny do tych, które widziałeś, uruchamiając kod z Listingu 21-1 i Listingu 21-2.
Teraz dodajmy kod z Listingu 21-7 do bloku else, aby zwrócić odpowiedź z kodem statusu 404, który sygnalizuje, że zawartość dla żądania nie została znaleziona. Zwrócimy również kod HTML dla strony, aby przeglądarka mogła ją wyrenderować, wskazując odpowiedź użytkownikowi końcowemu.
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();
}
}
Tutaj nasza odpowiedź zawiera linię statusu z kodem statusu 404 i frazą powodu NOT FOUND. Ciałem odpowiedzi będzie HTML z pliku 404.html. Będziesz musiał utworzyć plik 404.html obok hello.html dla strony błędu; ponownie, możesz użyć dowolnego kodu HTML, jaki chcesz, lub użyć przykładowego kodu HTML z Listingu 21-8.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Witaj!</title>
</head>
<body>
<h1>Ups!</h1>
<p>Przepraszam, nie wiem, o co prosisz.</p>
</body>
</html>
Dzięki tym zmianom uruchom ponownie swój serwer. Żądanie 127.0.0.1:7878 powinno zwrócić zawartość pliku hello.html, a każde inne żądanie, takie jak 127.0.0.1:7878/foo, powinno zwrócić kod HTML błędu z 404.html.
Refaktoryzacja
W tej chwili bloki if i else mają wiele powtórzeń: oba odczytują pliki i zapisują zawartość plików do strumienia. Jedyne różnice to linia statusu i nazwa pliku. Uczyńmy kod bardziej zwięzłym, wyodrębniając te różnice w osobne linie if i else, które przypiszą wartości linii statusu i nazwy pliku do zmiennych; możemy następnie użyć tych zmiennych bezwarunkowo w kodzie do odczytu pliku i zapisu odpowiedzi. Listing 21-9 pokazuje wynikowy kod po zastąpieniu dużych bloków if i else.
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();
}
Teraz bloki if i else zwracają tylko odpowiednie wartości dla linii statusu i nazwy pliku w krotce; następnie używamy dekompozycji do przypisania tych dwóch wartości do status_line i filename za pomocą wzorca w instrukcji let, jak omówiono w Rozdziale 19.
Poprzednio zduplikowany kod znajduje się teraz poza blokami if i else i używa zmiennych status_line i filename. Ułatwia to dostrzeżenie różnicy między dwoma przypadkami, a także oznacza, że mamy tylko jedno miejsce do aktualizacji kodu, jeśli chcemy zmienić sposób działania odczytu plików i zapisu odpowiedzi. Zachowanie kodu z Listingu 21-9 będzie takie samo jak z Listingu 21-7.
Świetnie! Mamy teraz prosty serwer WWW w około 40 liniach kodu Rust, który odpowiada na jedno żądanie stroną treści i na wszystkie inne żądania odpowiedzią 404.
Obecnie nasz serwer działa w jednym wątku, co oznacza, że może obsługiwać tylko jedno żądanie na raz. Zbadajmy, jak to może być problemem, symulując kilka wolnych żądań. Następnie naprawimy to, aby nasz serwer mógł obsługiwać wiele żądań jednocześnie.