Création d'un serveur web mono-fil

RustRustBeginner
Pratiquer maintenant

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

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Building a Single-Threaded Web Server. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons construire un serveur web mono-fil qui utilise les protocoles HTTP et TCP pour traiter les requêtes des clients et fournir des réponses.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) 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/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{{"Création d'un serveur web mono-fil"}} rust/string_type -.-> lab-100452{{"Création d'un serveur web mono-fil"}} rust/for_loop -.-> lab-100452{{"Création d'un serveur web mono-fil"}} rust/function_syntax -.-> lab-100452{{"Création d'un serveur web mono-fil"}} rust/expressions_statements -.-> lab-100452{{"Création d'un serveur web mono-fil"}} rust/method_syntax -.-> lab-100452{{"Création d'un serveur web mono-fil"}} rust/operator_overloading -.-> lab-100452{{"Création d'un serveur web mono-fil"}} end

Building a Single-Threaded Web Server

Nous allons commencer par faire fonctionner un serveur web mono-fil. Avant de commencer, jetons un coup d'œil rapide sur les protocoles impliqués dans la construction de serveurs web. Les détails de ces protocoles sont en dehors du champ d'étude de ce livre, mais une vue d'ensemble rapide vous donnera les informations dont vous avez besoin.

Les deux principaux protocoles impliqués dans les serveurs web sont le Hypertext Transfer Protocol (HTTP) et le Transmission Control Protocol (TCP). Les deux protocoles sont des protocoles demande-réponse, ce qui signifie qu'un client initie des requêtes et qu'un serveur écoute les requêtes et fournit une réponse au client. Le contenu de ces requêtes et réponses est défini par les protocoles.

TCP est le protocole de niveau inférieur qui décrit les détails de la manière dont l'information passe d'un serveur à l'autre, mais ne spécifie pas ce que cette information est. HTTP s'appuie sur TCP en définissant le contenu des requêtes et des réponses. Technologiquement, il est possible d'utiliser HTTP avec d'autres protocoles, mais dans la grande majorité des cas, HTTP envoie ses données via TCP. Nous travaillerons avec les octets bruts des requêtes et réponses TCP et HTTP.

Écouter la connexion TCP

Notre serveur web doit écouter une connexion TCP, c'est donc la première partie sur laquelle nous allons travailler. La bibliothèque standard propose un module std::net qui nous permet de le faire. Créons un nouveau projet de la manière habituelle :

$ cargo new hello
     Créé un projet binaire (application) `hello`
$ cd hello

Maintenant, entrez le code de la Liste 20-1 dans src/main.rs pour commencer. Ce code écoutera l'adresse locale 127.0.0.1:7878 pour les flux TCP entrants. Lorsqu'il reçoit un flux entrant, il imprimera Connection established!.

Nom du fichier : 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!");
    }
}

Liste 20-1 : Écoute des flux entrants et impression d'un message lorsqu'un flux est reçu

En utilisant TcpListener, nous pouvons écouter les connexions TCP à l'adresse 127.0.0.1:7878 [1]. Dans l'adresse, la partie avant le deux-points est une adresse IP représentant votre ordinateur (c'est la même sur chaque ordinateur et ne représente pas spécifiquement l'ordinateur des auteurs), et 7878 est le port. Nous avons choisi ce port pour deux raisons : HTTP n'est normalement pas accepté sur ce port, de sorte que notre serveur est peu susceptible de entrer en conflit avec tout autre serveur web que vous pourriez avoir exécuté sur votre machine, et 7878 est rust tapé sur un téléphone.

La fonction bind dans ce scénario fonctionne comme la fonction new en ce sens qu'elle retournera une nouvelle instance de TcpListener. La fonction est appelée bind car, en réseau, se connecter à un port pour l'écouter est connu sous le nom de "liaison à un port".

La fonction bind retourne un Result<T, E>, ce qui indique qu'il est possible que la liaison échoue. Par exemple, se connecter au port 80 nécessite des privilèges d'administrateur (les non-administrateurs ne peuvent écouter que sur des ports supérieurs à 1023), de sorte que si nous essayions de nous connecter au port 80 sans être administrateur, la liaison ne fonctionnerait pas. La liaison ne fonctionnerait pas non plus, par exemple, si nous exécutions deux instances de notre programme et avions donc deux programmes écoutant le même port. Comme nous écrivons un serveur de base uniquement à des fins d'apprentissage, nous n'aurons pas à nous soucier de gérer ce genre d'erreurs ; au lieu de cela, nous utilisons unwrap pour arrêter le programme si des erreurs se produisent.

La méthode incoming sur TcpListener retourne un itérateur qui nous donne une séquence de flux [2] (plus précisément, des flux de type TcpStream). Un seul flux représente une connexion ouverte entre le client et le serveur. Une connexion est le nom donné au processus complet de demande et de réponse dans lequel un client se connecte au serveur, le serveur génère une réponse et le serveur ferme la connexion. Ainsi, nous allons lire le TcpStream pour voir ce que le client a envoyé et puis écrire notre réponse dans le flux pour renvoyer des données au client. Dans l'ensemble, cette boucle for traitera chaque connexion tour à tour et produira une série de flux pour nous à traiter.

Pour l'instant, notre traitement du flux consiste à appeler unwrap pour terminer notre programme si le flux présente des erreurs [3] ; s'il n'y a pas d'erreurs, le programme imprime un message [4]. Nous ajouterons plus de fonctionnalités pour le cas de réussite dans la prochaine liste. La raison pour laquelle nous pouvons recevoir des erreurs de la méthode incoming lorsqu'un client se connecte au serveur est que nous n'itérons pas réellement sur les connexions. Au lieu de cela, nous itérons sur les tentatives de connexion. La connexion peut ne pas réussir pour un certain nombre de raisons, dont de nombreuses sont spécifiques au système d'exploitation. Par exemple, de nombreux systèmes d'exploitation ont une limite au nombre de connexions ouvertes simultanées qu'ils peuvent prendre en charge ; de nouvelles tentatives de connexion au-delà de ce nombre produiront une erreur jusqu'à ce que certaines des connexions ouvertes soient fermées.

Essayons d'exécuter ce code! Appelez cargo run dans le terminal puis chargez 127.0.0.1:7878 dans un navigateur web. Le navigateur devrait afficher un message d'erreur comme "Connection reset" car le serveur ne renvoie actuellement aucune donnée. Mais lorsque vous regardez votre terminal, vous devriez voir plusieurs messages qui ont été imprimés lorsque le navigateur s'est connecté au serveur!

     Exécution de `target/debug/hello`
Connection established!
Connection established!
Connection established!

Parfois, vous verrez plusieurs messages imprimés pour une seule demande de navigateur ; la raison peut être que le navigateur effectue une demande pour la page ainsi qu'une demande pour d'autres ressources, comme l'icône favicon.ico qui apparaît dans l'onglet du navigateur.

Il se peut également que le navigateur essaie de se connecter au serveur plusieurs fois car le serveur ne répond pas avec de données. Lorsque stream sort de portée et est supprimé à la fin de la boucle, la connexion est fermée en tant que partie de la mise en œuvre de drop. Les navigateurs traitent parfois les connexions fermées en réessayant, car le problème peut être temporaire. Le facteur important est que nous avons obtenu avec succès un contrôle sur une connexion TCP!

N'oubliez pas d'arrêter le programme en appuyant sur ctrl-C lorsque vous avez fini d'exécuter une version particulière du code. Ensuite, redémarrez le programme en invoquant la commande cargo run après avoir effectué chaque ensemble de modifications de code pour vous assurer que vous exécutez le code le plus récent.

Lecture de la requête

Implémentons la fonctionnalité pour lire la requête envoyée par le navigateur! Pour séparer les préoccupations liées à la première étape consistant à obtenir une connexion et à celle consistant à prendre des mesures avec cette connexion, nous allons commencer une nouvelle fonction pour traiter les connexions. Dans cette nouvelle fonction handle_connection, nous allons lire les données du flux TCP et les imprimer pour voir les données envoyées par le navigateur. Modifions le code pour qu'il ressemble à la Liste 20-2.

Nom du fichier : 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);
}

Liste 20-2 : Lecture du TcpStream et impression des données

Nous importons std::io::prelude et std::io::BufReader pour avoir accès à des traits et des types qui nous permettent de lire et d'écrire dans le flux [1]. Dans la boucle for de la fonction main, au lieu d'imprimer un message indiquant que nous avons établi une connexion, nous appelons maintenant la nouvelle fonction handle_connection et lui passons le stream [2].

Dans la fonction handle_connection, nous créons une nouvelle instance de BufReader qui encapsule une référence mutable au stream [3]. BufReader ajoute un buffering en gérant les appels aux méthodes du trait std::io::Read pour nous.

Nous créons une variable nommée http_request pour collecter les lignes de la requête que le navigateur envoie à notre serveur. Nous indiquons que nous souhaitons collecter ces lignes dans un vecteur en ajoutant l'annotation de type Vec<_> [4].

BufReader implémente le trait std::io::BufRead, qui fournit la méthode lines [5]. La méthode lines renvoie un itérateur de Result<String, std::io::Error> en divisant le flux de données chaque fois qu'elle voit un octet de nouvelle ligne. Pour obtenir chaque String, nous appliquons une fonction de transformation et utilisons unwrap sur chaque Result [6]. Le Result peut être une erreur si les données ne sont pas de la forme UTF-8 valide ou s'il y a eu un problème lors de la lecture du flux. Encore une fois, un programme de production devrait gérer ces erreurs de manière plus élégante, mais nous choisissons de stopper le programme en cas d'erreur pour la simplicité.

Le navigateur indique la fin d'une requête HTTP en envoyant deux caractères de nouvelle ligne l'un après l'autre, donc pour obtenir une seule requête à partir du flux, nous prenons les lignes jusqu'à ce que nous obtenions une ligne vide [7]. Une fois que nous avons collecté les lignes dans le vecteur, nous les imprimons en utilisant un formatage de débogage agréable [8] pour pouvoir examiner les instructions que le navigateur web envoie à notre serveur.

Essayons ce code! Démarrez le programme et effectuez à nouveau une requête dans un navigateur web. Notez que nous obtiendrons toujours une page d'erreur dans le navigateur, mais la sortie de notre programme dans le terminal ressemblera maintenant à ceci :

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

Selon votre navigateur, vous pouvez obtenir une sortie légèrement différente. Maintenant que nous imprimons les données de la requête, nous pouvons voir pourquoi nous obtenons plusieurs connexions à partir d'une seule requête de navigateur en examinant le chemin après GET dans la première ligne de la requête. Si les connexions répétées demandent toutes /, nous savons que le navigateur essaie de récupérer / plusieurs fois parce qu'il ne reçoit pas de réponse de notre programme.

Analysons ces données de requête pour comprendre ce que le navigateur demande à notre programme.

Un examen plus approfondi d'une requête HTTP

HTTP est un protocole basé sur le texte, et une requête suit ce format :

Méthode URI-Version HTTP CRLF
en-têtes CRLF
corps du message

La première ligne est la ligne de requête qui contient des informations sur ce que le client demande. La première partie de la ligne de requête indique la méthode utilisée, telle que GET ou POST, qui décrit comment le client effectue cette requête. Notre client a utilisé une requête GET, ce qui signifie qu'il demande des informations.

La partie suivante de la ligne de requête est / qui indique l'identifiant de ressource uniforme (URI) que le client demande : une URI est presque, mais pas tout à fait, la même chose qu'un localisateur de ressource uniforme (URL). La différence entre les URIs et les URLs n'est pas importante pour nos besoins dans ce chapitre, mais la spécification HTTP utilise le terme URI, donc nous pouvons simplement substituer mentalement URL à URI ici.

La dernière partie est la version HTTP utilisée par le client, puis la ligne de requête se termine par une séquence CRLF. (CRLF signifie retour chariot et saut de ligne, qui sont des termes issus des temps de la machine à écrire!) La séquence CRLF peut également être écrite comme \r\n, où \r est un retour chariot et \n est un saut de ligne. La séquence CRLF sépare la ligne de requête du reste des données de la requête. Notez que lorsqu'on imprime la séquence CRLF, on voit un nouveau ligne commencer plutôt que \r\n.

En examinant les données de la ligne de requête que nous avons reçues en exécutant notre programme jusqu'à présent, on voit que GET est la méthode, / est l'URI de la requête et HTTP/1.1 est la version.

Après la ligne de requête, les lignes suivantes à partir de Host: sont des en-têtes. Les requêtes GET n'ont pas de corps.

Essayez d'effectuer une requête à partir d'un autre navigateur ou de demander une autre adresse, telle que 127.0.0.1:7878/test, pour voir comment les données de la requête changent.

Maintenant que nous savons ce que le navigateur demande, envoyons quelques données en retour!

Écrire une réponse

Nous allons implémenter l'envoi de données en réponse à une demande du client. Les réponses ont le format suivant :

Version-HTTP Code-État Phrase-Raison CRLF
en-têtes CRLF
corps du message

La première ligne est une ligne d'état qui contient la version HTTP utilisée dans la réponse, un code d'état numérique qui résume le résultat de la demande et une phrase de raison qui fournit une description textuelle du code d'état. Après la séquence CRLF viennent tous les en-têtes, une autre séquence CRLF et le corps de la réponse.

Voici un exemple de réponse qui utilise la version HTTP 1.1, a un code d'état de 200, une phrase de raison OK, aucun en-tête et aucun corps :

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

Le code d'état 200 est la réponse de succès standard. Le texte est une petite réponse HTTP réussie. Écrivons cela dans le flux en tant que réponse à une demande réussie! Dans la fonction handle_connection, supprimez l'instruction println! qui affichait les données de la requête et remplacez-la par le code de la Liste 20-3.

Nom du fichier : 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();
}

Liste 20-3 : Écriture d'une petite réponse HTTP réussie dans le flux

La première nouvelle ligne définit la variable response qui contient les données du message de succès [1]. Ensuite, nous appelons as_bytes sur notre response pour convertir les données de chaîne en octets [3]. La méthode write_all sur stream prend un &[u8] et envoie directement ces octets sur la connexion [2]. Comme l'opération write_all pourrait échouer, nous utilisons unwrap sur tout résultat d'erreur comme auparavant. Encore une fois, dans une application réelle, vous ajouteriez la gestion des erreurs ici.

Avec ces modifications, exécutons notre code et effectuons une demande. Nous n'affichons plus aucune donnée dans le terminal, donc nous ne verrons aucun résultat autre que la sortie de Cargo. Lorsque vous chargez 127.0.0.1:7878 dans un navigateur web, vous devriez obtenir une page blanche au lieu d'une erreur. Vous venez d'écrire manuellement la réception d'une requête HTTP et l'envoi d'une réponse!

Retourner du HTML réel

Implémentons la fonctionnalité pour retourner plus qu'une page blanche. Créez le nouveau fichier hello.html dans le répertoire racine de votre projet, pas dans le répertoire src. Vous pouvez entrer tout le HTML que vous voulez ; la Liste 20-4 montre une possibilité.

Nom du fichier : 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>

Liste 20-4 : Un fichier HTML exemple à retourner dans une réponse

Il s'agit d'un document HTML5 minimal avec un titre et quelques textes. Pour le retourner depuis le serveur lorsqu'une requête est reçue, nous modifierons handle_connection comme indiqué dans la Liste 20-5 pour lire le fichier HTML, l'ajouter au corps de la réponse et l'envoyer.

Nom du fichier : 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();
}

Liste 20-5 : Envoi du contenu de hello.html comme corps de la réponse

Nous avons ajouté fs à l'instruction use pour inclure le module de système de fichiers de la bibliothèque standard dans la portée [1]. Le code pour lire le contenu d'un fichier en tant que chaîne devrait vous paraître familier ; nous l'avons utilisé lorsque nous avons lu le contenu d'un fichier pour notre projet d'E/S dans la Liste 12-4.

Ensuite, nous utilisons format! pour ajouter le contenu du fichier comme corps de la réponse de succès [2]. Pour assurer une réponse HTTP valide, nous ajoutons l'en-tête Content-Length qui est défini sur la taille de notre corps de réponse, dans ce cas la taille de hello.html.

Exécutez ce code avec cargo run et chargez 127.0.0.1:7878 dans votre navigateur ; vous devriez voir votre HTML affiché!

Actuellement, nous ignorons les données de la requête dans http_request et envoyons simplement le contenu du fichier HTML sans condition. Cela signifie que si vous essayez de demander 127.0.0.1:7878/something-else dans votre navigateur, vous recevrez toujours cette même réponse HTML. Pour l'instant, notre serveur est très limité et ne fait pas ce que la plupart des serveurs web font. Nous voulons personnaliser nos réponses en fonction de la requête et n'envoyer le fichier HTML que pour une requête correctement formée à /.

Validation de la requête et réponse sélective

En ce moment, notre serveur web renvoie le HTML contenu dans le fichier quoi que le client ait demandé. Ajoutons une fonctionnalité pour vérifier que le navigateur demande / avant de renvoyer le fichier HTML, et renvoyer une erreur si le navigateur demande autre chose. Pour cela, nous devons modifier handle_connection, comme indiqué dans la Liste 20-6. Ce nouveau code vérifie le contenu de la requête reçue par rapport à ce que nous savons qu'une requête à / ressemble et ajoute des blocs if et else pour traiter les requêtes différemment.

Nom du fichier : 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
    }
}

Liste 20-6 : Traitement des requêtes à / différemment des autres requêtes

Nous allons seulement examiner la première ligne de la requête HTTP, donc au lieu de lire toute la requête dans un vecteur, nous appelons next pour obtenir le premier élément de l'itérateur [1]. Le premier unwrap gère l'Option et arrête le programme si l'itérateur n'a aucun élément. Le second unwrap gère le Result et a le même effet que l'unwrap qui était dans la fonction map ajoutée dans la Liste 20-2.

Ensuite, nous vérifions la request_line pour voir si elle est égale à la ligne de requête d'une requête GET vers le chemin / [2]. Si c'est le cas, le bloc if renvoie le contenu de notre fichier HTML.

Si la request_line n'est pas égale à la requête GET vers le chemin /, cela signifie que nous avons reçu une autre requête. Nous ajouterons du code au bloc else [3] dans un instant pour répondre à toutes les autres requêtes.

Exécutez maintenant ce code et demandez 127.0.0.1:7878 ; vous devriez obtenir le HTML contenu dans hello.html. Si vous effectuez n'importe quelle autre requête, telle que 127.0.0.1:7878/something-else, vous obtiendrez une erreur de connexion comme celles que vous avez vues en exécutant le code dans la Liste 20-1 et la Liste 20-2.

Maintenant, ajoutons le code de la Liste 20-7 au bloc else pour renvoyer une réponse avec le code d'état 404, qui indique que le contenu de la requête n'a pas été trouvé. Nous renverrons également un peu de HTML pour une page à afficher dans le navigateur indiquant la réponse à l'utilisateur final.

Nom du fichier : 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();
}

Liste 20-7 : Renvoyer un code d'état 404 et une page d'erreur si une autre chose que / est demandée

Ici, notre réponse a une ligne d'état avec le code d'état 404 et la phrase de raison NOT FOUND [1]. Le corps de la réponse sera le HTML contenu dans le fichier 404.html [1]. Vous devrez créer un fichier 404.html à côté de hello.html pour la page d'erreur ; n'hésitez pas à utiliser tout le HTML que vous voulez, ou utilisez l'HTML exemple dans la Liste 20-8.

Nom du fichier : 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>

Liste 20-8 : Contenu d'échantillonnage pour la page à renvoyer avec toute réponse 404

Avec ces modifications, exécutez à nouveau votre serveur. Demander 127.0.0.1:7878 devrait renvoyer le contenu de hello.html, et n'importe quelle autre requête, comme 127.0.0.1:7878/foo, devrait renvoyer le HTML d'erreur contenu dans 404.html.

Un peu de refactoring

En ce moment, les blocs if et else ont beaucoup de répétitions : ils lisent tous les deux des fichiers et écrivent le contenu des fichiers dans le flux. Les seules différences sont la ligne d'état et le nom du fichier. Rendre le code plus concise en extraisant ces différences dans des lignes if et else séparées qui assigneront les valeurs de la ligne d'état et du nom du fichier à des variables ; nous pourrons ensuite utiliser ces variables de manière inconditionnelle dans le code pour lire le fichier et écrire la réponse. La Liste 20-9 montre le code résultant après avoir remplacé les grands blocs if et else.

Nom du fichier : 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();
}

Liste 20-9 : Refactoring des blocs if et else pour ne contenir que le code différent entre les deux cas

Maintenant, les blocs if et else ne renvoient que les valeurs appropriées pour la ligne d'état et le nom du fichier dans un tuple ; nous utilisons ensuite la déstructuration pour assigner ces deux valeurs à status_line et filename à l'aide d'un motif dans l'instruction let, comme discuté au chapitre 18.

Le code précédemment dupliqué est maintenant en dehors des blocs if et else et utilise les variables status_line et filename. Cela facilite la compréhension des différences entre les deux cas, et cela signifie que nous n'avons qu'un seul endroit où mettre à jour le code si nous voulons changer la manière dont la lecture du fichier et l'écriture de la réponse fonctionnent. Le comportement du code dans la Liste 20-9 sera le même que celui dans la Liste 20-8.

Génial! Nous avons maintenant un serveur web simple en environ 40 lignes de code Rust qui répond à une demande avec une page de contenu et répond à toutes les autres demandes avec une réponse 404.

En ce moment, notre serveur s'exécute dans un seul fil, ce qui signifie qu'il ne peut servir qu'une demande à la fois. Examinons comment cela peut être un problème en simulant quelques demandes lentes. Ensuite, nous le corrigerons pour que notre serveur puisse gérer plusieurs demandes simultanément.

Sommaire

Félicitations! Vous avez terminé le laboratoire de construction d'un serveur web mono-fil. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.