Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Construire un serveur web monotâche

Nous allons commencer par faire fonctionner un serveur web monothread. Avant de commencer, examinons un rapide aperçu des protocoles impliqués dans la construction de serveurs web. Les détails de ces protocoles dépassent le cadre de ce livre, mais un bref aperçu 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). Ces deux protocoles sont des protocoles de type requête-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 plus bas niveau qui décrit les détails de la façon dont l’information circule d’un serveur à un autre, mais ne spécifie pas ce qu’est cette information. HTTP s’appuie sur TCP en définissant le contenu des requêtes et des réponses. Il est techniquement possible d’utiliser HTTP avec d’autres protocoles, mais dans la grande majorité des cas, HTTP envoie ses données via TCP. Nous allons travailler 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 offre un module std::net qui nous permet de faire cela. Créons un nouveau projet de la manière habituelle :

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

Maintenant, entrez le code de l’encart 21-1 dans src/main.rs pour commencer. Ce code écoutera à l’adresse locale 127.0.0.1:7878 les flux TCP entrants. Quand il recevra un flux entrant, il affichera Connection established!.

Filename: src/main.rs
use std::net::TcpListener;

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

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

        println!("Connection established!");
    }
}
Listing 21-1: Listening for incoming streams and printing a message when we receive a stream

En utilisant TcpListener, nous pouvons écouter les connexions TCP à l’adresse 127.0.0.1:7878. Dans l’adresse, la partie avant les deux-points est une adresse IP représentant votre ordinateur (c’est la même sur tous les ordinateurs 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, donc notre serveur a peu de chances d’entrer en conflit avec un autre serveur web que vous pourriez avoir en fonctionnement sur votre machine, et 7878 correspond à rust tapé sur un téléphone.

La fonction bind dans ce scénario fonctionne comme la fonction new en ce qu’elle retourné une nouvelle instance de TcpListener. La fonction s’appelle bind parce que, en réseau, se connecter à un port pour l’écouter est connu sous le nom de « liaison à un port » (binding).

La fonction bind retourné un Result<T, E>, ce qui indique qu’il est possible que la liaison échoue, par exemple si nous exécutions deux instances de notre programme et avions donc deux programmes écoutant sur le même port. Comme nous écrivons un serveur basique uniquement à des fins d’apprentissage, nous ne nous soucierons pas de gérer ce type d’erreurs ; à la place, nous utilisons unwrap pour arrêter le programme si des erreurs surviennent.

La méthode incoming sur TcpListener retourné un itérateur qui nous donne une séquence de flux (plus précisément, des flux de type TcpStream). Un seul flux (stream) représente une connexion ouverte entre le client et le serveur. Connexion est le nom du processus complet de requête et 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 lirons depuis le TcpStream pour voir ce que le client a envoyé, puis nous écrirons notre réponse dans le flux pour renvoyer des données au client. Globalement, cette boucle for traitera chaque connexion à tour de rôle et produira une série de flux à gérer.

Pour l’instant, notre gestion du flux consiste à appeler unwrap pour terminer notre programme si le flux à des erreurs ; s’il n’y a pas d’erreurs, le programme affiche un message. Nous ajouterons plus de fonctionnalités pour le cas de succès dans le prochain encart. La raison pour laquelle nous pourrions recevoir des erreurs de la méthode incoming quand un client se connecte au serveur est que nous n’itérons pas réellement sur des connexions. Au lieu de cela, nous itérons sur des tentatives de connexion. La connexion pourrait ne pas réussir pour plusieurs raisons, dont beaucoup 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 ; les 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 ! Lancez 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 quand vous regardez votre terminal, vous devriez voir plusieurs messages qui ont été affichés quand le navigateur s’est connecté au serveur !

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

Parfois, vous verrez plusieurs messages affichés pour une seule requête du navigateur ; la raison pourrait être que le navigateur fait une requête pour la page ainsi qu’une requête pour d’autres ressources, comme l’icône favicon.ico qui apparaît dans l’onglet du navigateur.

Il se pourrait aussi que le navigateur essaie de se connecter au serveur plusieurs fois parce que le serveur ne répond avec aucune donnée. Quand stream sort de la portée et est libéré à la fin de la boucle, la connexion est fermée dans le cadre de l’implémentation de drop. Les navigateurs gèrent parfois les connexions fermées en réessayant, car le problème pourrait être temporaire.

Les navigateurs ouvrent aussi parfois plusieurs connexions au serveur sans envoyer de requêtes afin que s’ils envoient effectivement des requêtes plus tard, celles-ci puissent être traitées plus rapidement. Quand cela se produit, notre serveur verra chaque connexion, qu’il y ait ou non des requêtes sur cette connexion. De nombreuses versions de navigateurs basés sur Chrome font cela, par exemple ; vous pouvez désactiver cette optimisation en utilisant le mode de navigation privée ou en utilisant un autre navigateur.

Le point important est que nous avons réussi à obtenir un handle vers une connexion TCP !

N’oubliez pas d’arrêter le programme en appuyant sur ctrl-C quand 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 du code pour vous assurer que vous exécutez le code le plus récent.

Lire la requête

Implémentons la fonctionnalité pour lire la requête depuis le navigateur ! Pour séparer les préoccupations entre d’abord obtenir une connexion et ensuite effectuer une action avec la connexion, nous allons créer une nouvelle fonction pour traiter les connexions. Dans cette nouvelle fonction handle_connection, nous lirons les données du flux TCP et les afficherons pour que nous puissions voir les données envoyées par le navigateur. Modifiez le code pour qu’il ressemble à l’encart 21-2.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}

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

    println!("Request: {http_request:#?}");
}
Listing 21-2: Reading from the TcpStream and printing the data

Nous importons std::io::BufReader et std::io::prelude dans la portée pour accéder aux traits et types qui nous permettent de lire et d’écrire dans le flux. Dans la boucle for de la fonction main, au lieu d’afficher un message disant que nous avons établi une connexion, nous appelons maintenant la nouvelle fonction handle_connection et lui passons le stream.

Dans la fonction handle_connection, nous créons une nouvelle instance de BufReader qui enveloppe une référence au stream. Le BufReader ajouté une mise en tampon 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 voulons collecter ces lignes dans un vecteur en ajoutant l’annotation de type Vec<_>.

BufReader implémente le trait std::io::BufRead, qui fournit la méthode lines. La méthode lines retourné un itérateur de Result<String, std::io::Error> en découpant le flux de données chaque fois qu’elle rencontre un octet de retour à la ligne. Pour obtenir chaque String, nous appliquons map et unwrap sur chaque Result. Le Result pourrait être une erreur si les données ne sont pas du UTF-8 valide ou s’il y a eu un problème de lecture depuis le flux. Encore une fois, un programme en production devrait gérer ces erreurs plus élégamment, mais nous choisissons d’arrêter le programme en cas d’erreur par souci de simplicité.

Le navigateur signale la fin d’une requête HTTP en envoyant deux caractères de retour à la ligne consécutifs, donc pour obtenir une requête depuis le flux, nous prenons des lignes jusqu’à obtenir une ligne qui est la chaîne vide. Une fois que nous avons collecté les lignes dans le vecteur, nous les affichons en utilisant le formatage de débogage élégant pour que nous puissions examiner les instructions que le navigateur web envoie à notre serveur.

Essayons ce code ! Démarrez le programme et faites une requête dans un navigateur web à nouveau. 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` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

Selon votre navigateur, vous pourriez obtenir une sortie légèrement différente. Maintenant que nous affichons les données de la requête, nous pouvons voir pourquoi nous obtenons plusieurs connexions à partir d’une seule requête du navigateur en regardant 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 / de manière répétée parce qu’il ne reçoit pas de réponse de notre programme.

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

Examiner de plus près une requête HTTP

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

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

La première ligne est la ligne de requête qui contient les informations sur ce que le client demande. La première partie de la ligne de requête indique la méthode utilisée, comme GET ou POST, qui décrit comment le client fait 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 : un URI est presque, mais pas tout à fait, la même chose qu’un localisateur de ressource uniforme (URL). La différence entre les URI et les URL 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 que le client utilise, puis la ligne de requête se terminé par une séquence CRLF. (CRLF signifie carriage return (retour chariot) et line feed (saut de ligne), qui sont des termes issus de l’époque des machines à écrire !) La séquence CRLF peut aussi s’écrire \r , où \r est un retour chariot et 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 quand le CRLF est affiché, nous voyons un nouveau retour à la ligne plutôt que \r .

En regardant les données de la ligne de requête que nous avons reçues en exécutant notre programme jusqu’à présent, nous voyons 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 restantes à partir de Host: sont les en-têtes. Les requêtes GET n’ont pas de corps.

Essayez de faire une requête depuis un autre navigateur ou de demander une adresse différente, comme 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, renvoyons-lui quelques données !

Écrire une réponse

Nous allons implémenter l’envoi de données en réponse à une requête client. Les réponses ont le format suivant :

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

La première ligne est une ligne de statut qui contient la version HTTP utilisée dans la réponse, un code de statut numérique qui résume le résultat de la requête, et une phrase de raison qui fournit une description textuelle du code de statut. Après la séquence CRLF se trouvent 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 et à un code de statut 200, une phrase de raison OK, pas d’en-têtes et pas de corps :

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

Le code de statut 200 est la réponse de succès standard. Le texte est une toute petite réponse HTTP réussie. Écrivons cela dans le flux comme notre réponse à une requête réussie ! Depuis la fonction handle_connection, supprimez le println! qui affichait les données de la requête et remplacez-le par le code de l’encart 21-3.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}

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

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

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-3: Writing a tiny successful HTTP response to the stream

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

Avec ces changements, exécutons notre code et faisons une requête. Nous n’affichons plus aucune donnée dans le terminal, donc nous ne verrons aucune sortie autre que celle de Cargo. Quand vous chargez 127.0.0.1:7878 dans un navigateur web, vous devriez obtenir une page vierge au lieu d’une erreur. Vous venez de coder à la main la réception d’une requête HTTP et l’envoi d’une réponse !

Renvoyer du vrai HTML

Implémentons la fonctionnalité pour renvoyer plus qu’une page vierge. Créez le nouveau fichier hello.html à la racine du répertoire de votre projet, pas dans le répertoire src. Vous pouvez saisir le HTML que vous voulez ; l’encart 21-4 montre une possibilité.

Filename: hello.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>
Listing 21-4: A sample HTML file to return in a response

C’est un document HTML5 minimal avec un titre et du texte. Pour le renvoyer depuis le serveur quand une requête est reçue, nous allons modifier handle_connection comme montré dans l’encart 21-5 pour lire le fichier HTML, l’ajouter à la réponse comme corps, et l’envoyer.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};
// --snip--

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

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

        handle_connection(stream);
    }
}

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

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

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

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-5: Sending the contents of hello.html as the body of the response

Nous avons ajouté fs à l’instruction use pour importer le module de système de fichiers de la bibliothèque standard dans la portée. Le code pour lire le contenu d’un fichier dans une chaîne devrait vous sembler familier ; nous l’avons utilisé quand nous avons lu le contenu d’un fichier pour notre projet d’E/S dans l’encart 12-4.

Ensuite, nous utilisons format! pour ajouter le contenu du fichier comme corps de la réponse de succès. Pour garantir une réponse HTTP valide, nous ajoutons l’en-tête Content-Length, qui est défini à la taille du corps de notre 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 renvoyons simplement le contenu du fichier HTML de manière inconditionnelle. Cela signifie que si vous essayez de demander 127.0.0.1:7878/something-else dans votre navigateur, vous obtiendrez toujours la même réponse HTML. Pour le moment, notre serveur est très limité et ne fait pas ce que font la plupart des serveurs web. Nous voulons personnaliser nos réponses en fonction de la requête et ne renvoyer le fichier HTML que pour une requête bien formée vers /.

Valider la requête et répondre de manière sélective

Pour l’instant, notre serveur web renverra le HTML du fichier peu importe ce que le client a demandé. Ajoutons une fonctionnalité pour vérifier que le navigateur demande / avant de renvoyer le fichier HTML et pour renvoyer une erreur si le navigateur demande autre chose. Pour cela, nous devons modifier handle_connection, comme montré dans l’encart 21-6. Ce nouveau code vérifie le contenu de la requête reçue par rapport à ce que nous savons être une requête pour / et ajouté des blocs if et else pour traiter les requêtes différemment.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}
// --snip--

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

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

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

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}
Listing 21-6: Handling requests to / differently from other requests

Nous n’allons examiner que la première ligne de la requête HTTP, donc plutôt que de lire la requête entière dans un vecteur, nous appelons next pour obtenir le premier élément de l’itérateur. Le premier unwrap gère l’Option et arrête le programme si l’itérateur n’a pas d’éléments. Le second unwrap gère le Result et à le même effet que le unwrap qui était dans le map ajouté dans l’encart 21-2.

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

Si la request_line ne correspond pas à 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 dans un instant pour répondre à toutes les autres requêtes.

Exécutez ce code maintenant et demandez 127.0.0.1:7878 ; vous devriez obtenir le HTML de hello.html. Si vous faites une autre requête, comme 127.0.0.1:7878/something-else, vous obtiendrez une erreur de connexion comme celles que vous avez vues en exécutant le code des encarts 21-1 et 21-2.

Maintenant, ajoutons le code de l’encart 21-7 au bloc else pour renvoyer une réponse avec le code de statut 404, qui signale que le contenu de la requête n’a pas été trouvé. Nous renverrons aussi du HTML pour une page à afficher dans le navigateur indiquant la réponse à l’utilisateur final.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}

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

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r
Content-Length: {length}\r
\r
{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
Content-Length: {length}\r
\r
{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    }
}
Listing 21-7: Responding with status code 404 and an error page if anything other than / was requested

Ici, notre réponse à une ligne de statut avec le code de statut 404 et la phrase de raison NOT FOUND. Le corps de la réponse sera le HTML du fichier 404.html. Vous devrez créer un fichier 404.html à côté de hello.html pour la page d’erreur ; encore une fois, n’hésitez pas à utiliser le HTML que vous voulez, ou utilisez l’exemple de HTML de l’encart 21-8.

Filename: 404.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>
Listing 21-8: Sample content for the page to send back with any 404 response

Avec ces changements, relancez votre serveur. Demander 127.0.0.1:7878 devrait renvoyer le contenu de hello.html, et toute autre requête, comme 127.0.0.1:7878/foo, devrait renvoyer le HTML d’erreur de 404.html.

Refactorisation

Pour le 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 de statut et le nom du fichier. Rendons le code plus concis en extrayant ces différences dans des lignes if et else séparées qui assigneront les valeurs de la ligne de statut et du nom de 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. L’encart 21-9 montre le code résultant après le remplacement des gros blocs if et else.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}
// --snip--

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

    let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

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

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-9: Refactoring the if and else blocks to contain only the code that differs between the two cases

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

Le code précédemment dupliqué est maintenant en dehors des blocs if et else et utilise les variables status_line et filename. Cela rend plus facile de voir la différence 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 façon dont la lecture du fichier et l’écriture de la réponse fonctionnent. Le comportement du code de l’encart 21-9 sera le même que celui de l’encart 21-7.

Formidable ! Nous avons maintenant un serveur web simple en environ 40 lignes de code Rust qui répond à une requête avec une page de contenu et répond à toutes les autres requêtes avec une réponse 404.

Actuellement, notre serveur fonctionne dans un seul thread, ce qui signifie qu’il ne peut traiter qu’une seule requête à la fois. Examinons comment cela peut poser problème en simulant des requêtes lentes. Ensuite, nous le corrigerons pour que notre serveur puisse gérer plusieurs requêtes à la fois.