Aufbau eines einthreadigen Webservers

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Building a Single-Threaded Web Server. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir einen ein-threadigen Webserver bauen, der die HTTP- und TCP-Protokolle verwendet, um Clientanfragen zu verarbeiten und Antworten zu liefern.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} rust/string_type -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} rust/for_loop -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} rust/function_syntax -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} rust/expressions_statements -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} rust/method_syntax -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} rust/operator_overloading -.-> lab-100452{{"Aufbau eines einthreadigen Webservers"}} end

Ein ein-threadiger Webserver bauen

Wir beginnen damit, einen ein-threadigen Webserver zum Laufen zu bringen. Bevor wir beginnen, schauen wir uns einen kurzen Überblick über die Protokolle an, die bei der Erstellung von Webservern beteiligt sind. Die Details dieser Protokolle liegen außerhalb des Rahmens dieses Buches, aber ein kurzer Überblick wird Ihnen die benötigten Informationen geben.

Die beiden Hauptprotokolle, die bei Webservern beteiligt sind, sind das Hypertext Transfer Protocol (HTTP) und das Transmission Control Protocol (TCP). Beide Protokolle sind request-response -Protokolle, was bedeutet, dass ein Client Anfragen initiiert und ein Server auf die Anfragen hört und eine Antwort an den Client liefert. Der Inhalt dieser Anfragen und Antworten wird durch die Protokolle definiert.

TCP ist das niedriger Ebene liegende Protokoll, das die Details darüber beschreibt, wie Informationen von einem Server zum anderen gelangen, aber nicht angibt, was diese Informationen sind. HTTP baut auf TCP auf, indem es den Inhalt der Anfragen und Antworten definiert. Technisch ist es möglich, HTTP mit anderen Protokollen zu verwenden, aber in den vast majority of cases, sendet HTTP seine Daten über TCP. Wir werden mit den rohen Bytes von TCP- und HTTP-Anfragen und -Antworten arbeiten.

Auf das TCP-Verbindung hören

Unser Webserver muss auf eine TCP-Verbindung hören, daher ist das der erste Teil, an dem wir arbeiten werden. Die Standardbibliothek bietet ein std::net-Modul, das uns dies ermöglicht. Lassen Sie uns ein neues Projekt auf die übliche Weise erstellen:

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

Nun geben Sie den Code in Listing 20-1 in src/main.rs ein, um zu beginnen. Dieser Code wird an der lokalen Adresse 127.0.0.1:7878 auf eingehende TCP-Streams hören. Wenn er einen eingehenden Stream erhält, wird er Connection established! ausgeben.

Dateiname: src/main.rs

use std::net::TcpListener;

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

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

      4 println!("Connection established!");
    }
}

Listing 20-1: Lauschen auf eingehende Ströme und Ausgeben einer Nachricht, wenn wir einen Stream erhalten

Mit TcpListener können wir auf TCP-Verbindungen an der Adresse 127.0.0.1:7878 hören [1]. In der Adresse ist der Teil vor dem Doppelpunkt eine IP-Adresse, die Ihren Computer repräsentiert (dies ist auf jedem Computer gleich und repräsentiert nicht speziell den Computer der Autoren), und 7878 ist der Port. Wir haben diesen Port aus zwei Gründen gewählt: Normalerweise wird HTTP nicht auf diesem Port akzeptiert, daher ist es unwahrscheinlich, dass unser Server mit einem anderen Webserver auf Ihrem Computer in Konflikt tritt, und 7878 ist rust auf einem Telefon getippt.

Die bind-Funktion in diesem Szenario funktioniert wie die new-Funktion, in dem sie eine neue TcpListener-Instanz zurückgibt. Die Funktion heißt bind, weil im Netzwerkverbindung auf einen Port zu hören als "sich an einen Port binden" bekannt ist.

Die bind-Funktion gibt ein Result<T, E> zurück, was darauf hinweist, dass das Binden fehlschlagen kann. Beispielsweise erfordert das Verbinden mit Port 80 Administratorrechte (Nicht-Administratoren können nur auf Ports höher als 1023 hören), daher würde das Binden nicht funktionieren, wenn wir versuchten, uns an Port 80 zu verbinden, ohne Administrator zu sein. Das Binden würde auch nicht funktionieren, wenn wir zwei Instanzen unseres Programms ausführten und daher zwei Programme auf den gleichen Port hätten. Da wir nur ein einfachen Server für Lernzwecke schreiben, werden wir uns nicht um das Handling dieser Arten von Fehlern kümmern; stattdessen verwenden wir unwrap, um das Programm zu beenden, wenn Fehler auftreten.

Die incoming-Methode auf TcpListener gibt einen Iterator zurück, der uns eine Sequenz von Strömen gibt [2] (genauer gesagt Ströme vom Typ TcpStream). Ein einzelner Stream repräsentiert eine offene Verbindung zwischen dem Client und dem Server. Eine Verbindung ist der Name für den gesamten Anforderungs- und Antwortprozess, in dem ein Client sich an den Server verbindet, der Server eine Antwort generiert und der Server die Verbindung schließt. Daher werden wir aus dem TcpStream lesen, um zu sehen, was der Client gesendet hat, und dann unsere Antwort an den Stream schreiben, um Daten zurück an den Client zu senden. Insgesamt wird diese for-Schleife jede Verbindung nacheinander verarbeiten und eine Reihe von Strömen für uns erzeugen, die wir verarbeiten müssen.

Zur Zeit besteht unsere Behandlung des Streams darin, unwrap aufzurufen, um unser Programm zu beenden, wenn der Stream Fehler hat [3]; wenn es keine Fehler gibt, druckt das Programm eine Nachricht [4]. Wir werden im nächsten Listing für den Erfolgfall weitere Funktionalität hinzufügen. Der Grund, warum wir Fehler von der incoming-Methode erhalten können, wenn ein Client sich an den Server verbindet, ist, dass wir tatsächlich nicht über Verbindungen iterieren. Stattdessen iterieren wir über Verbindungsversuche. Die Verbindung kann aus einer Vielzahl von Gründen nicht erfolgreich sein, viele davon sind Betriebssystem-spezifisch. Beispielsweise haben viele Betriebssysteme eine Begrenzung für die Anzahl der gleichzeitig offenen Verbindungen, die sie unterstützen können; neue Verbindungsversuche über diese Anzahl hinaus werden zu einem Fehler führen, bis einige der offenen Verbindungen geschlossen werden.

Lassen Sie uns versuchen, diesen Code auszuführen! Rufen Sie in der Konsole cargo run auf und laden Sie dann 127.0.0.1:7878 in einem Webbrowser. Der Browser sollte eine Fehlermeldung wie "Connection reset" anzeigen, da der Server derzeit keine Daten zurücksendet. Aber wenn Sie sich die Konsole ansehen, sollten Sie mehrere Nachrichten sehen, die gedruckt wurden, als der Browser sich an den Server verbunden hat!

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

Manchmal werden Sie für einen Browseranforderung mehrere Nachrichten gedruckt; Der Grund kann sein, dass der Browser eine Anforderung für die Seite sowie eine Anforderung für andere Ressourcen macht, wie das favicon.ico -Symbol, das im Browser-Tab erscheint.

Es könnte auch sein, dass der Browser versucht, sich mehrmals an den Server zu verbinden, weil der Server keine Daten zurücksendet. Wenn stream außerhalb des Gültigkeitsbereichs geht und am Ende der Schleife gelöscht wird, wird die Verbindung als Teil der drop-Implementierung geschlossen. Browser behandeln manchmal geschlossene Verbindungen, indem sie versuchen, da das Problem möglicherweise vorübergehend ist. Der wichtigste Faktor ist, dass wir erfolgreich einen Zugang zu einer TCP-Verbindung erhalten haben!

Denken Sie daran, das Programm mit Strg+C zu beenden, wenn Sie mit der Ausführung einer bestimmten Version des Codes fertig sind. Starten Sie dann das Programm erneut, indem Sie nach jeder Codeänderung den Befehl cargo run aufrufen, um sicherzustellen, dass Sie den neuesten Code ausführen.

Das Lesen der Anfrage

Implementieren wir die Funktionalität, um die Anfrage aus dem Browser zu lesen! Um die Aufgaben von erstmalig das Herstellen einer Verbindung und dann das Ausführen von Aktionen mit der Verbindung zu trennen, starten wir eine neue Funktion zur Verarbeitung von Verbindungen. In dieser neuen handle_connection-Funktion werden wir Daten aus dem TCP-Strom lesen und ausgeben, damit wir die von dem Browser gesendeten Daten sehen können. Ändern Sie den Code, sodass er wie Listing 20-2 aussieht.

Dateiname: src/main.rs

1 use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

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

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

      2 handle_connection(stream);
    }
}

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

  8 println!("Request: {:#?}", http_request);
}

Listing 20-2: Lesen aus dem TcpStream und Ausgabe der Daten

Wir bringen std::io::prelude und std::io::BufReader in den Gültigkeitsbereich, um auf Traits und Typen zuzugreifen, die uns ermöglichen, in und aus dem Stream zu lesen und zu schreiben [1]. In der for-Schleife in der main-Funktion geben wir statt einer Nachricht aus, dass wir eine Verbindung hergestellt haben, jetzt die neue handle_connection-Funktion aufrufen und dem stream übergeben [2].

In der handle_connection-Funktion erstellen wir eine neue BufReader-Instanz, die eine mutabile Referenz auf den stream umschließt [3]. BufReader fügt Puffering hinzu, indem es die Aufrufe an die std::io::Read-Trait-Methoden für uns verwaltet.

Wir erstellen eine Variable namens http_request, um die Zeilen der Anfrage zu sammeln, die der Browser an unseren Server sendet. Wir geben an, dass wir diese Zeilen in einem Vektor sammeln möchten, indem wir die Vec<_>-Typangabe hinzufügen [4].

BufReader implementiert das std::io::BufRead-Trait, das die lines-Methode bereitstellt [5]. Die lines-Methode gibt einen Iterator von Result<String, std::io::Error> zurück, indem sie den Datenstrom jedes Mal, wenn sie ein Zeilenumbruch-Byte sieht, aufspaltet. Um jedes String zu erhalten, mappen und unwrap wir jedes Result [6]. Das Result kann ein Fehler sein, wenn die Daten kein gültiges UTF-8 sind oder wenn es ein Problem beim Lesen aus dem Stream gab. Wiederum sollte ein Produktionsprogramm diese Fehler eleganter behandeln, aber wir wählen, das Programm im Fehlerfall zu stoppen, um es einfacher zu halten.

Der Browser signalisiert das Ende einer HTTP-Anfrage, indem er zwei Zeilenumbrüche nacheinander sendet, daher um eine Anfrage aus dem Stream zu erhalten, nehmen wir Zeilen, bis wir eine Zeile erhalten, die die leere Zeichenkette ist [7]. Nachdem wir die Zeilen in den Vektor gesammelt haben, geben wir sie mit einer schönen Debug-Formatierung aus [8], sodass wir uns die Anweisungen ansehen können, die der Webbrowser an unseren Server sendet.

Probieren wir diesen Code aus! Starten Sie das Programm und machen Sie erneut eine Anfrage in einem Webbrowser. Beachten Sie, dass wir immer noch eine Fehlerseite im Browser erhalten, aber die Ausgabe unseres Programms in der Konsole sieht jetzt ähnlich aus wie dies:

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

Je nach Browser können Sie leicht unterschiedliche Ausgabe erhalten. Jetzt, da wir die Anfragedaten ausgeben, können wir sehen, warum wir aus einer Browseranfrage mehrere Verbindungen erhalten, indem wir uns den Pfad nach GET in der ersten Zeile der Anfrage ansehen. Wenn die wiederholten Verbindungen alle / anfordern, wissen wir, dass der Browser versucht, / wiederholt abzurufen, weil er keine Antwort von unserem Programm erhält.

Zerlegen wir diese Anfragedaten, um zu verstehen, was der Browser von unserem Programm verlangt.

Ein genaueres Esehen auf eine HTTP-Anfrage

HTTP ist ein textbasiertes Protokoll, und eine Anfrage hat dieses Format:

Methode Anfrage-URI HTTP-Version CRLF
Header CRLF
Nachrichtenkörper

Die erste Zeile ist die Anfragezeile, die Informationen darüber enthält, was der Client anfordert. Der erste Teil der Anfragezeile gibt die verwendete Methode an, wie GET oder POST, die beschreibt, wie der Client diese Anfrage stellt. Unser Client verwendete eine GET-Anfrage, was bedeutet, dass er Informationen anfordert.

Der nächste Teil der Anfragezeile ist /, was den uniform resource identifier (URI) angibt, den der Client anfordert: Ein URI ist fast, aber nicht ganz, dasselbe wie ein uniform resource locator (URL). Der Unterschied zwischen URIs und URLs ist für unsere Zwecke in diesem Kapitel nicht wichtig, aber die HTTP-Spezifikation verwendet den Begriff URI, daher können wir uns hier einfach URL für URI ersetzen.

Der letzte Teil ist die HTTP-Version, die der Client verwendet, und dann endet die Anfragezeile in einer CRLF-Sequenz. (CRLF steht für carriage return und line feed, die Begriffe aus den Schreibmaschinenzeiten!) Die CRLF-Sequenz kann auch als \r\n geschrieben werden, wobei \r ein Wagenrücklauf und \n ein Zeilenumbruch ist. Die CRLF-Sequenz trennt die Anfragezeile von den restlichen Anfragedaten. Beachten Sie, dass wenn die CRLF gedruckt wird, wir einen neuen Zeilenanfang sehen, statt \r\n.

Betrachtend die Anfragezeiledaten, die wir bisher von unserem ausgeführten Programm erhalten haben, sehen wir, dass GET die Methode, / die Anfrage-URI und HTTP/1.1 die Version ist.

Nach der Anfragezeile sind die verbleibenden Zeilen ab Host: Header. GET-Anfragen haben keinen Körper.

Versuchen Sie, eine Anfrage von einem anderen Browser zu stellen oder eine andere Adresse anzufordern, wie 127.0.0.1:7878/test, um zu sehen, wie sich die Anfragedaten ändern.

Jetzt, da wir wissen, was der Browser anfordert, senden wir etwas Daten zurück!

Das Schreiben einer Antwort

Wir werden die Implementierung von Datenübertragung als Antwort auf eine Clientanfrage vornehmen. Antworten haben das folgende Format:

HTTP-Version Status-Code Grundtext CRLF
Header CRLF
Nachrichtenkörper

Die erste Zeile ist eine Statuszeile, die die verwendete HTTP-Version in der Antwort, einen numerischen Statuscode, der das Ergebnis der Anfrage zusammenfasst, und einen Grundtext enthält, der eine textliche Beschreibung des Statuscodes liefert. Nach der CRLF-Sequenz folgen beliebige Header, eine weitere CRLF-Sequenz und der Körper der Antwort.

Hier ist ein Beispiel für eine Antwort, die HTTP-Version 1.1 verwendet, einen Statuscode von 200, einen OK-Grundtext, keine Header und keinen Körper hat:

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

Der Statuscode 200 ist die standardmäßige Erfolgsantwort. Der Text ist eine winzige erfolgreiche HTTP-Antwort. Schreiben wir dies in den Stream als Antwort auf eine erfolgreiche Anfrage! Entfernen Sie aus der handle_connection-Funktion die println!, die die Anfragedaten ausgab, und ersetzen Sie sie durch den Code in Listing 20-3.

Dateiname: src/main.rs

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

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

  2 stream.write_all(response.3 as_bytes()).unwrap();
}

Listing 20-3: Schreiben einer winzigen erfolgreichen HTTP-Antwort in den Stream

Die erste neue Zeile definiert die response-Variable, die die Daten der Erfolgsnachricht enthält [1]. Dann rufen wir as_bytes auf unserer response auf, um die Zeichenfolge-Daten in Bytes umzuwandeln [3]. Die write_all-Methode auf stream nimmt ein &[u8] und sendet diese Bytes direkt über die Verbindung [2]. Da die write_all-Operation fehlschlagen kann, verwenden wir wie zuvor unwrap bei jedem Fehlerergebnis. Wiederholen Sie, in einer echten Anwendung würden Sie hier die Fehlerbehandlung hinzufügen.

Mit diesen Änderungen führen wir unseren Code aus und stellen eine Anfrage. Wir drucken keine Daten mehr in die Konsole, daher werden wir keine Ausgabe sehen, außer der Ausgabe von Cargo. Wenn Sie 127.0.0.1:7878 in einem Webbrowser laden, sollten Sie eine leere Seite statt eines Fehlers erhalten. Sie haben gerade manuell die Empfang einer HTTP-Anfrage und das Senden einer Antwort implementiert!

Rückgabe von echten HTML

Implementieren wir die Funktionalität, um mehr als eine leere Seite zurückzugeben. Erstellen Sie die neue Datei hello.html im Stammverzeichnis Ihres Projekts, nicht im src-Verzeichnis. Sie können beliebiges HTML eingeben; Listing 20-4 zeigt eine Möglichkeit.

Dateiname: 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 20-4: Eine Beispiel-HTML-Datei, die in einer Antwort zurückgegeben wird

Dies ist eine minimale HTML5-Dokument mit einem Überschrift und etwas Text. Um dies vom Server zurückzugeben, wenn eine Anfrage empfangen wird, werden wir handle_connection wie in Listing 20-5 ändern, um die HTML-Datei zu lesen, sie als Körper zur Antwort hinzuzufügen und zu senden.

Dateiname: src/main.rs

use std::{
  1 fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};
--snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut 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();

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

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-5: Senden der Inhalte von hello.html als Körper der Antwort

Wir haben fs zum use-Statement hinzugefügt, um das Dateisystemmodul der Standardbibliothek in den Gültigkeitsbereich zu bringen [1]. Der Code zum Lesen der Inhalte einer Datei in eine Zeichenfolge sollte Ihnen vertraut sein; wir haben ihn verwendet, als wir die Inhalte einer Datei für unser I/O-Projekt in Listing 12-4 gelesen haben.

Als nächstes verwenden wir format!, um die Dateiinhalte als Körper der Erfolgsantwort hinzuzufügen [2]. Um eine gültige HTTP-Antwort zu gewährleisten, fügen wir den Content-Length-Header hinzu, der auf die Größe unseres Antwortkörpers, in diesem Fall die Größe von hello.html, gesetzt wird.

Führen Sie diesen Code mit cargo run aus und laden Sie 127.0.0.1:7878 in Ihrem Browser; Sie sollten Ihr HTML gerendert sehen!

Derzeit ignorieren wir die Anfragedaten in http_request und senden einfach die Inhalte der HTML-Datei unbedingt zurück. Das bedeutet, dass Sie, wenn Sie versuchen, 127.0.0.1:7878/something-else in Ihrem Browser anzufragen, immer noch dieselbe HTML-Antwort erhalten. Momentan ist unser Server sehr eingeschränkt und macht nicht das, was die meisten Webserver tun. Wir möchten unsere Antworten je nach Anfrage anpassen und nur die HTML-Datei für eine gut geformte Anfrage an / zurücksenden.

Validieren der Anfrage und selektiv reagieren

Im Moment gibt unser Webserver das HTML in der Datei zurück, egal was der Client angefordert hat. Fügen wir die Funktionalität hinzu, um zu überprüfen, ob der Browser / anfordert, bevor wir die HTML-Datei zurückgeben, und geben einen Fehler zurück, wenn der Browser etwas anderes anfordert. Dazu müssen wir handle_connection ändern, wie in Listing 20-6 gezeigt. Dieser neue Code überprüft den Inhalt der empfangenen Anfrage gegen das, was wir wissen, wie eine Anfrage an / aussieht, und fügt if- und else-Blöcke hinzu, um Anfragen unterschiedlich zu behandeln.

Dateiname: src/main.rs

--snip--

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

  2 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\n\
             Content-Length: {length}\r\n\r\n\
             {contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
  3 } else {
        // some other request
    }
}

Listing 20-6: Andere Behandlung von Anfragen an / als von anderen Anfragen

Wir werden nur die erste Zeile der HTTP-Anfrage betrachten, daher lesen wir nicht die gesamte Anfrage in einen Vektor, sondern rufen next auf, um das erste Element aus dem Iterator zu erhalten [1]. Die erste unwrap kümmert sich um die Option und stoppt das Programm, wenn der Iterator keine Elemente hat. Die zweite unwrap behandelt das Result und hat die gleiche Wirkung wie die unwrap, die in der map in Listing 20-2 hinzugefügt wurde.

Als nächstes überprüfen wir die request_line, um zu sehen, ob sie der Anfragezeile einer GET-Anfrage zum /-Pfad entspricht [2]. Wenn ja, gibt der if-Block die Inhalte unserer HTML-Datei zurück.

Wenn die request_line nicht der GET-Anfrage zum /-Pfad entspricht, bedeutet dies, dass wir eine andere Anfrage erhalten haben. Wir werden in einem Moment Code zum else-Block hinzufügen [3], um auf alle anderen Anfragen zu reagieren.

Führen Sie diesen Code jetzt aus und fragen Sie 127.0.0.1:7878 an; Sie sollten das HTML in hello.html erhalten. Wenn Sie eine andere Anfrage stellen, wie 127.0.0.1:7878/something-else, erhalten Sie einen Verbindungsfehler wie die, die Sie beim Ausführen des Codes in Listing 20-1 und Listing 20-2 gesehen haben.

Nun fügen wir den Code in Listing 20-7 zum else-Block hinzu, um eine Antwort mit dem Statuscode 404 zurückzugeben, was signalisiert, dass der Inhalt für die Anfrage nicht gefunden wurde. Wir werden auch etwas HTML für eine Seite zurückgeben, die im Browser gerendert werden soll, um die Antwort an den Endbenutzer anzuzeigen.

Dateiname: src/main.rs

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

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

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-7: Antworten mit Statuscode 404 und einer Fehlerseite, wenn etwas anderes als / angefordert wurde

Hier hat unsere Antwort eine Statuszeile mit dem Statuscode 404 und dem Grundtext NOT FOUND [1]. Der Körper der Antwort wird das HTML in der Datei 404.html sein [1]. Sie müssen eine 404.html-Datei neben hello.html für die Fehlerseite erstellen; Sie können wieder beliebiges HTML verwenden oder das Beispiel-HTML in Listing 20-8 verwenden.

Dateiname: 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 20-8: Beispielinhalt für die Seite, die mit jeder 404-Antwort zurückgeschickt wird

Mit diesen Änderungen führen Sie Ihren Server erneut aus. Das Anfordern von 127.0.0.1:7878 sollte die Inhalte von hello.html zurückgeben, und jede andere Anfrage, wie 127.0.0.1:7878/foo, sollte das Fehler-HTML aus 404.html zurückgeben.

Ein bisschen Refactoring

Im Moment haben die if- und else-Blöcke viel Wiederholung: Beide lesen Dateien und schreiben die Inhalte der Dateien in den Stream. Die einzigen Unterschiede sind die Statuszeile und der Dateiname. Lassen Sie uns den Code prägnanter machen, indem wir diese Unterschiede in separate if- und else-Zeilen ziehen, die die Werte der Statuszeile und des Dateinamens an Variablen zuweisen; wir können dann diese Variablen unbedingt im Code verwenden, um die Datei zu lesen und die Antwort zu schreiben. Listing 20-9 zeigt den resultierenden Code nach Ersetzung der großen if- und else-Blöcke.

Dateiname: src/main.rs

--snip--

fn handle_connection(mut stream: TcpStream) {
    --snip--

    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\n\
         Content-Length: {length}\r\n\r\n\
         {contents}"
    );

    stream.write_all(response.as_bytes()).unwrap();
}

Listing 20-9: Refactoring der if- und else-Blöcke, um nur den Code zu enthalten, der zwischen den beiden Fällen unterschiedlich ist

Jetzt geben die if- und else-Blöcke nur die passenden Werte für die Statuszeile und den Dateinamen in einem Tupel zurück; wir verwenden dann die Dekonstruktion, um diese beiden Werte an status_line und filename zuzuweisen, indem wir ein Muster im let-Statement verwenden, wie in Kapitel 18 besprochen.

Der zuvor duplizierte Code befindet sich jetzt außerhalb der if- und else-Blöcke und verwendet die Variablen status_line und filename. Dies macht es einfacher, den Unterschied zwischen den beiden Fällen zu sehen, und es bedeutet, dass wir nur an einem Ort den Code aktualisieren müssen, wenn wir die Art und Weise ändern möchten, wie das Lesen der Datei und das Schreiben der Antwort funktioniert. Das Verhalten des Codes in Listing 20-9 wird das gleiche wie das in Listing 20-8 sein.

Super! Wir haben jetzt einen einfachen Webserver in ungefähr 40 Zeilen Rust-Code, der auf eine Anfrage mit einer Seite Inhalt antwortet und auf alle anderen Anfragen mit einer 404-Antwort.

Derzeit läuft unser Server in einem einzelnen Thread, was bedeutet, dass er nur eine Anfrage pro Zeitpunkt bedienen kann. Untersuchen wir, wie das ein Problem sein kann, indem wir einige langsame Anfragen simulieren. Dann werden wir es beheben, sodass unser Server mehrere Anfragen gleichzeitig verarbeiten kann.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Building a Single-Threaded Web Server" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.