Développer un serveur web monotâche

Nous allons commencer par faire fonctionner un serveur web monotâche. Avant de commencer, faisons un survol rapide des protocoles utilisés dans les serveurs web. Les détails de ces protocoles ne sont pas le sujet de ce livre, mais un rapide aperçu vous donnera les informations dont vous avez besoin.

Les deux principaux protocoles utilisé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 tandis que le serveur écoute les requêtes et fournit une réponse au client. Le contenu de ces requêtes et de ces réponses est défini par les protocoles.

TCP est le protocole le plus bas-niveau qui décrit les détails de comment une information passe d'un serveur à un autre mais ne précise pas ce qu'est cette information. HTTP est construit 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 des réponses de TCP et HTTP.

Ecouter les connexions TCP

Notre serveur web a besoin d'écouter les connexions TCP, donc cela sera la première partie sur laquelle nous travaillerons. La bibliothèque standard offre un module std::net qui nous permet de faire ceci. Créons un nouveau projet de manière habituelle :

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

Maintenant, saisissez le code de l'encart 20-1 dans src/main.rs pour commencer. Ce code va écouter les flux TCP entrants à l'adresse 127.0.0.1:7878. Lorsqu'il obtiendra un flux entrant, il va afficher Connexion établie !.

Fichier : src/main.rs

use std::net::TcpListener;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        println!("Connexion établie !");
    }
}

Encart 20-1 : écoute des flux entrants et affichage d'un message lorsque nous recevons un flux

En utilisant TcpListener, nous pouvons écouter les connexions TCP à l'adresse 127.0.0.1:7878. Dans cette adresse, la partie avant les double-points est une adresse IP qui représente votre ordinateur (c'est la même sur chaque ordinateur et ne représente pas spécifiquement l'ordinateur de l'auteur), et 7878 est le port. Nous avons choisi ce port pour deux raisons : HTTP n'est pas habituellement accepté sur ce port et 7878 correspond aux touches utilisées sur un clavier de téléphone pour écrire Rust.

La fonction bind dans ce scénario fonctionne comme la fonction new dans le sens où elle retourne une nouvelle instance de TcpListener. La raison pour laquelle cette fonction s'appelle bind (NdT : signifie "lier") est que dans le domaine des réseaux, se connecter à un port se dit se “lier à un port”.

La fonction bind retourne un Result<T, E>, ce qui signifie que la création de lien peut échouer. Par exemple, la connexion au port 80 nécessite d'être administrateur (les utilisateurs non-administrateur ne peuvent écouter que sur les ports supérieurs à 1023), donc si nous essayons de connecter un port 80 sans être administrateur, le lien ne va pas fonctionner. Pour donner un autre exemple, le lien ne va pas fonctionner si nous exécutons deux instances de notre programme et que nous avons deux programmes qui écoutent sur le même port. Comme nous écrivons un serveur basique uniquement à but pédagogique, nous n'avons pas à nous soucier de la gestion de ce genre d'erreur ; c'est pourquoi nous utilisons unwrap pour arrêter l'exécution du programme si des erreurs surviennent.

La méthode incoming d'un TcpListener retourne l'itérateur qui nous donne une séquence de flux (plus précisément, des flux de type TcpStream). Un seul flux représente une connexion entre le client et le serveur. Une connexion est le nom qui désigne le processus complet de requête et de réponse, durant lequel le client se connecte au serveur, le serveur génère une réponse puis le serveur ferme la connexion. Ainsi, TcpStream va se lire lui-même pour voir ce que le client a envoyé et nous permettre ensuite d'écrire notre réponse dans le flux. De manière générale, cette boucle for traitera l'une après l'autre chaque connexion dans l'ordre et produira une série de flux que nous devrons gérer.

Pour l'instant, notre gestion des flux consiste à appeler unwrap pour arrêter notre programme si le flux rencontre une erreur ; s'il n'y a pas d'erreurs, le programme affiche un message. Nous ajouterons davantage de fonctionnalités en cas de succès dans le prochain encart. La raison pour laquelle nous pourrions recevoir des erreurs de la méthode incoming lorsqu'un client se connecte au serveur est qu'en réalité nous n'itérons pas sur les connexions. En effet, nous itérons sur des tentatives de connexion. La connexion peut échouer pour de nombreuses raisons, beaucoup d'entre elles sont spécifiques au système d'exploitation. Par exemple, de nombreux systèmes d'exploitation ont une limite sur le nombre de connexions ouvertes simultanément qu'ils peuvent supporter ; les tentatives de nouvelles connexions une fois ce nombre dépassé produiront une erreur jusqu'à ce que certaines des connexions soient fermées.

Essayons d'exécuter ce code ! Saisissez cargo run dans le terminal et ensuite ouvrez 127.0.0.1:7878 dans un navigateur web. Le navigateur devrait afficher un message d'erreur tel que “La connexion a été réinitialisée”, car le serveur ne renvoie pas de données pour le moment. Mais si vous regardez le terminal, vous devriez voir quelques messages qui se sont affichés lorsque le navigateur s'est connecté au serveur !

     Running `target/debug/salutations`
Connexion établie !
Connexion établie !
Connexion établie !

Des fois, vous pourriez voir plusieurs messages s'afficher pour une seule requête du navigateur ; la raison à cela est peut-être que le navigateur fait une requête pour la page ainsi que des requêtes pour d'autres ressources, comme l'icone favicon.ico qui s'affiche dans l'onglet du navigateur.

Peut-être que le navigateur essaie aussi de se connecter plusieurs fois au serveur car le serveur ne renvoie aucune donnée dans sa réponse. Lorsque flux sort de la portée et est nettoyé à la fin de la boucle, la connexion est fermée car cela est implémenté dans le drop. Les navigateurs réagissent à ces connexions fermées en ré-essayant, car le problème peut être temporaire. La partie importante est que nous avons obtenu avec succès un manipulateur de connexion TCP !

Pensez à arrêter le programme en appuyant sur ctrl-c lorsque vous avez fini d'exécuter une version donnée du code. Relancez ensuite cargo run après avoir appliqué une série de modifications afin d'être sûr que vous exécutez bien la toute dernière version du code.

Lire la requête

Commençons à implémenter la fonctionnalité permettant de lire la requête du navigateur ! Pour séparer les parties où nous obtenons une connexion de celle où nous agissons avec la connexion, nous allons créer une nouvelle fonction pour traiter les connexions. Dans cette nouvelle fonction gestion_connexion, nous allons lire des données provenant du flux TCP et les afficher afin que nous puissions voir les données envoyées par le navigateur. Changez le code pour qu'il ressemble à l'encart 20-2.

Fichier : src/main.rs

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        gestion_connexion(flux);
    }
}

fn gestion_connexion(mut flux: TcpStream) {
    let mut tampon = [0; 1024];

    flux.read(&mut tampon).unwrap();

    println!("Requête : {}", String::from_utf8_lossy(&tampon[..]));
}

Encart 20-2 : lecture du TcpStream et affichage des données

Nous avons importé std::io::prelude dans la portée pour accéder à certains traits 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 qui dit que nous avons établi une connexion, nous faisons maintenant appel à gestion_connexion et nous lui passons le flux.

Dans la fonction gestion_connexion, nous avons fait en sorte que le paramètre flux soit mutable. La raison à cela est que l'instance de TcpStream garde en mémoire interne le suivi des données qu'il nous a retournées. Il peut lire plus de données que nous en avons demandées et les conserver pour la prochaine fois que nous en redemanderons. Il doit donc être mut car son état interne doit pouvoir changer ; d'habitude, nous n'avons pas besoin que la “lecture” nécessite d'être mutable, mais dans ce cas nous avons besoin du mot-clé mut.

Ensuite, nous devons lire les données du flux. Nous faisons cela en deux temps : d'abord, nous déclarons un tampon sur la pile pour y stocker les données qui seront lues. Nous avons fait en sorte que le tampon fasse 1024 octets, ce qui est suffisamment grand pour stocker les données d'une requête basique, ce qui est suffisant pour nos besoins dans ce chapitre. Si nous avions voulu gérer des requêtes de taille arbitraire, cette gestion du tampon aurait été plus complexe ; nous allons la garder simpliste pour l'instant. Nous envoyons le tampon dans flux.read qui va lire les octets provenant du TcpStream et les ajouter dans le tampon.

Ensuite, nous convertissons les octets présents dans le tampon en chaînes de caractères et nous affichons cette chaîne de caractères. La fonction String::from_utf8_lossy prend en paramètre un &[u8] et le transforme en une String. La partie “lossy” du nom indique le comportement de cette fonction lorsqu'elle rencontre une séquence UTF-8 invalide : elle va remplacer la séquence invalide par , le caractère U+FFFD REPLACEMENT CHARACTER. Vous devriez voir ces caractères de remplacement à la place des caractères du tampon qui n'ont pas été renseignés par des données de requête.

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

$ cargo run
   Compiling salutations v0.1.0 (file:///projects/salutations)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/salutations`
Requête : GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101
Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
������������������������������������

En fonction de votre navigateur, vous pourriez voir une sortie légèrement différente. Maintenant que nous affichons les données des requêtes, nous pouvons constater pourquoi nous obtenons plusieurs connexions pour un seul chargement de page dans le navigateur web en analysant le chemin après le Requête : GET. Si les connexions répétées sont toutes vers /, nous pouvons constater que le navigateur essaye d'obtenir / à répétition car il n'obtient pas de réponse de la part de notre programme.

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

Une analyse plus poussée d'une requête HTTP

HTTP est un protocole basé sur du texte, et une requête doit suivre cette forme :

Méthode URI-Demandée Version-HTTP CRLF
entêtes CRLF
corps-du-message

La première ligne est la ligne de requête qui contient les informations sur ce que demande le client. 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 sa requête. Notre client a utilisé une requête GET.

La partie suivante de la ligne de requête est /, qui indique l'URI (Uniform Resource Identifier) que demande le client : une URI est presque, mais pas complètement, la même chose qu'une URL (Uniform Resource Locator). La différence entre les URI et les URL n'est pas très importante pour nous dans ce chapitre, mais la spécification de HTTP utilise le terme URI, donc, ici, nous pouvons simplement lire URL là où URI est écrit.

La dernière partie est la version HTTP que le client utilise, puis la ligne de requête termine avec une séquence CRLF (CRLF signifie Carriage Return, retour chariot, et Line Feed, saut de ligne qui sont des termes qui remontent à l'époque des machines à écrire !). La séquence CRLF peut aussi être écrite \r\n, dans laquelle \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 toutefois que lorsqu'un CRLF est affiché, nous voyons une nouvelle ligne plutôt qu'un \r\n.

D'après la ligne de requête que nous avons reçue après avoir exécuté notre programme précédemment, nous constatons que la méthode est GET, / est l'URI demandée et HTTP/1.1 est la version.

Après la ligne de requête, les lignes suivant celle où nous avons Host: sont des entêtes. Les requêtes GET n'ont pas de corps.

Essayez de faire une requête dans un navigateur différent ou de demander une adresse différente, telle que 127.0.0.1:7878/test, afin d'observer comment les données de requête changent.

Maintenant que nous savons ce que demande le navigateur, envoyons-lui quelques données !

Ecrire une réponse

Maintenant, nous allons implémenter l'envoi d'une réponse à une requête client. Les réponses suivent le format suivant :

Version-HTTP Code-Statut Phrase-De-Raison CRLF
entêtes CRLF
corps-message

La première ligne est une ligne de statut qui contient la version HTTP utilisée dans la réponse, un code numérique de statut 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 viennent tous les entêtes, une autre séquence CRLF et enfin le corps de la réponse.

Voici un exemple de réponse qui utilise HTTP version 1.1, a un code de statut de 200, une phrase de raison à OK, pas d'entêtes, et pas de corps :

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

Le code de statut 200 est la réponse standard de succès. Le texte est une toute petite réponse HTTP de succès. Ecrivons ceci dans le flux de notre réponse à une requête avec succès ! Dans la fonction gestion_connexion, enlevez le println! qui affiche les données de requête et remplacez-le par le code de l'encart 20-3.

Fichier : src/main.rs

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        gestion_connexion(flux);
    }
}

fn gestion_connexion(mut flux: TcpStream) {
    let mut tampon = [0; 1024];

    flux.read(&mut tampon).unwrap();

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

    flux.write(reponse.as_bytes()).unwrap();
    flux.flush().unwrap();
}

Encart 20-3 : écriture d'une toute petite réponse HTTP de réussite dans le flux

La première ligne définit la variable reponse qui contient les données du message de réussite. Ensuite, nous faisons appel à as_bytes sur notre reponse pour convertir la chaîne de caractères en octets. La méthode write sur le flux prend en argument un &[u8] et envoie ces octets directement dans la connexion.

Comme l'opération write peut échouer, nous utilisons unwrap sur toutes les erreurs, comme précédemment. Encore une fois, dans un véritable application, vous devriez gérer les cas d'erreur ici. Enfin, flush va attendre et empêcher le programme de continuer à s'exécuter jusqu'à ce que tous les octets soient écrits dans la connexion ; TcpStream contient un tampon interne pour réduire les appels au système d'exploitation concerné.

Avec ces modifications, exécutons à nouveau notre code et lançons une requête dans le navigateur. Nous n'affichons plus les données dans le terminal, donc nous ne voyons plus aucune sortie autre que celle de Cargo. Lorsque vous chargez 127.0.0.1:7878 dans un navigateur web, vous devriez obtenir une page blanche plutôt qu'une erreur. Vous venez de coder en dur une réponse à une requête HTTP !

Retourner du vrai HTML

Implémentons la fonctionnalité permettant de retourner plus qu'une simple page blanche. Créez un nouveau fichier, hello.html, à la racine de votre dossier de projet, et pas dans le dossier src. Vous pouvez ajouter le HTML que vous souhaitez ; l'encart 20-4 vous montre une possibilité.

Fichier : hello.html

<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <title>Salutations !</title>
  </head>
  <body>
    <h1>Salut !</h1>
    <p>Bonjour de la part de Rust</p>
  </body>
</html>

Encart 20-4 : un exemple de fichier HTML à retourner dans une réponse

Ceci est un document HTML5 minimal avec des entêtes et un peu de texte. Pour retourner ceci à partir d'un serveur lorsqu'une requête est reçue, nous allons modifier gestion_connexion comme proposé dans l'encart 20-5 pour lire le fichier HTML, l'ajouter dans la réponse comme faisant partie de son corps, et l'envoyer.

Fichier : src/main.rs

use std::fs;
// -- partie masquée ici --

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        gestion_connexion(flux);
    }
}

fn gestion_connexion(mut flux: TcpStream) {
    let mut tampon = [0; 1024];
    flux.read(&mut tampon).unwrap();

    let contenu = fs::read_to_string("hello.html").unwrap();

    let reponse = format!(
        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
        contenu.len(),
        contenu
    );

    flux.write(reponse.as_bytes()).unwrap();
    flux.flush().unwrap();
}

Encart 20-5 : envoi du contenu de hello.html dans le corps de la réponse

Nous avons ajouté une ligne en haut pour importer le module de système de fichiers de la bibliothèque standard. Le code pour lire le contenu d'un fichier dans une String devrait vous être familier ; nous l'avons utilisé dans le chapitre 12 lorsque nous lisions le contenu d'un fichier pour notre projet d'entrée/sortie, dans l'encart 12-4.

Ensuite, nous avons utilisé format! pour ajouter le contenu du fichier comme étant le corps de la réponse avec succès. Pour garantir que ce soit une réponse HTTP valide, nous avons ajouté l'entête Content-Length qui définit la taille du corps de notre réponse, qui dans ce cas est la taille de hello.html.

Exécutez ce code avec cargo run et ouvrez 127.0.0.1:7878 dans votre navigateur web ; vous devriez voir le résultat de votre HTML !

Pour le moment, nous ignorons les données de la requête présentes dans tampon et nous renvoyons sans conditions le contenu du fichier HTML. Cela signifie que si vous essayez de demander 127.0.0.1:7878/autre-chose dans votre navigateur web, vous obtiendrez la même réponse HTML. Notre serveur est très limité, et ne correspond pas à ce que font la plupart des serveurs web. Nous souhaitons désormais personnaliser nos réponses en fonction de la requête et ne renvoyer le fichier HTML que pour une requête bien formatée faite à /.

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

Jusqu'à présent, notre serveur web retourne le HTML du fichier peu importe ce que demande le client. Ajoutons une fonctionnalité pour vérifier que le navigateur demande bien / avant de retourner le fichier HTML et retournons une erreur si le navigateur demande autre chose. Pour cela, nous devons modifier gestion_connexion comme dans l'encart 20-6. Ce nouveau code compare le contenu de la requête que nous recevons à la requête que nous attendrions pour / et ajoute des blocs if et else pour traiter les requêtes de manière différenciée.

Fichier : src/main.rs

use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        gestion_connexion(flux);
    }
}

// -- partie masquée ici --

fn gestion_connexion(mut flux: TcpStream) {
    let mut tampon = [0; 1024];
    flux.read(&mut tampon).unwrap();

    let get = b"GET / HTTP/1.1\r\n";

    if tampon.starts_with(get) {
        let contenu = fs::read_to_string("hello.html").unwrap();

        let reponse = format!(
            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
            contenu.len(),
            contenu
        );

        flux.write(reponse.as_bytes()).unwrap();
        flux.flush().unwrap();
    } else {
        // autres requêtes
    }
}

Encart 20-6 : détection et gestion des requêtes vers / de manière différenciée des autres requêtes

D'abord, nous codons en dur les données correspondant à la requête / dans la variable get. Comme nous lisons des octets bruts provenant du tampon, nous transformons get en une chaîne d'octets en ajoutant la syntaxe de chaîne d'octets b"" au début des données du contenu. Ensuite, nous vérifions que le tampon commence par les mêmes octets que ceux présents dans get. Si c'est le cas, cela signifie que nous avons reçu une requête vers / correctement formatée, qui est le cas de succès que nous allons gérer dans le bloc if qui retourne le contenu de notre fichier HTML.

Si tampon ne commence pas avec les octets présents dans get, cela signifie que nous avons reçu une autre requête. Nous allons bientôt ajouter du code au bloc else pour répondre à toutes ces 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 n'importe quelle autre requête, comme 127.0.0.1:7878/autre-chose, vous allez obtenir une erreur de connexion comme celle que vous avez vue lorsque vous exécutiez le code l'encart 20-1 et de l'encart 20-2.

Maintenant ajoutons le code de l'encart 20-7 au bloc else pour retourner une réponse avec le code de statut 404, qui signale que le contenu demandé par cette requête n'a pas été trouvé. Nous allons aussi retourner du HTML pour qu'une page s'affiche dans le navigateur, indiquant la réponse à l'utilisateur final.

Fichier : src/main.rs

use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        gestion_connexion(flux);
    }
}

fn gestion_connexion(mut flux: TcpStream) {
    let mut tampon = [0; 1024];
    flux.read(&mut tampon).unwrap();

    let get = b"GET / HTTP/1.1\r\n";

    if tampon.starts_with(get) {
        let contenu = fs::read_to_string("hello.html").unwrap();

        let reponse = format!(
            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
            contenu.len(),
            contenu
        );

        flux.write(reponse.as_bytes()).unwrap();
        flux.flush().unwrap();
    // -- partie masquée ici --
    } else {
        let ligne_statut = "HTTP/1.1 404 NOT FOUND";
        let contenu = fs::read_to_string("404.html").unwrap();

        let reponse = format!(
            "{}\r\nContent-Length: {}\r\n\r\n{}",
            ligne_statut,
            contenu.len(),
            contenu
        );

        flux.write(reponse.as_bytes()).unwrap();
        flux.flush().unwrap();
    }
}

Encart 20-7 : répondre un code de statut 404 et une page d'erreur lorsqu'autre chose que / a été demandé

Ici notre réponse possède 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 présent dans le fichier 404.html. Nous aurons besoin de créer un fichier 404.html au même endroit que hello.html pour la page d'erreur; de nouveau, n'hésitez pas à utiliser le HTML que vous souhaitez ou, à défaut, utilisez le HTML d'exemple présent dans l'encart 20-8.

Fichier : 404.html

<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <title>Salutations !</title>
  </head>
  <body>
    <h1>Oups !</h1>
    <p>Désolé, je ne connaît pas ce que vous demandez.</p>
  </body>
</html>

Encart 20-8 : contenu d'exemple pour la page à renvoyer avec les réponses 404

Une fois ces modifications appliquées, exécutez à nouveau votre serveur. Les requêtes vers 127.0.0.1:7878 devraient retourner le contenu de hello.html et toutes les autres requêtes, telle que 127.0.0.1:7878/autre-chose, devraient retourner le HTML d'erreur présent dans 404.html.

Un peu de remaniement

Pour l'instant, les blocs if et else contiennent beaucoup de code répété : ils lisent tous les deux des fichiers et écrivent le contenu de ces fichiers dans le flux. La seule différence entre eux sont la ligne de statut et le nom du fichier. Rendons le code plus concis en isolant ces différences dans des lignes if et else qui vont assigner les valeurs de la ligne de statut et du nom de fichier à des variables ; nous pourrons ensuite utiliser ces variables sans avoir à nous préoccuper du contexte dans le code qui va lire le fichier et écrire la réponse. L'encart 20-9 montre le code résultant après remplacement des gros blocs if et else.

Fichier : src/main.rs

use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

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

    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();

        gestion_connexion(flux);
    }
}

// -- partie masquée ici--

fn gestion_connexion(mut flux: TcpStream) {
    // -- partie masquée ici--

    let mut tampon = [0; 1024];
    flux.read(&mut tampon).unwrap();

    let get = b"GET / HTTP/1.1\r\n";

    let (ligne_statut, nom_fichier) = if tampon.starts_with(get) {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contenu = fs::read_to_string(nom_fichier).unwrap();

    let reponse = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        ligne_statut,
        contenu.len(),
        contenu
    );

    flux.write(reponse.as_bytes()).unwrap();
    flux.flush().unwrap();
}

Encart 20-9 : remaniement des blocs if et else pour qu'ils contiennent uniquement le code qui différencie les deux cas

Maintenant que les blocs if et else retournent uniquement les valeurs correctes pour la ligne de statut et le nom du fichier dans un tuple, nous pouvons utiliser la déstructuration pour assigner ces deux valeurs à ligne_statut et nom_fichier en utilisant un motif dans l'instruction let, comme nous l'avons vu dans le chapitre 18.

Le code précédent qui était en double se trouve maintenant à l'extérieur des blocs if et else et utilise les variables ligne_statut et nom_fichier. Cela permet de mettre en évidence plus facilement les différences entre les deux cas, et cela signifie que nous n'avons qu'un seul endroit du code à modifier si nous souhaitons changer le fonctionnement de lecture du fichier et d'écriture de la réponse. Le comportement du code de l'encart 20-9 devrait être identique à celui de l'encart 20-8.

Super ! Nous avons maintenant un serveur web simple qui tient dans environ 40 lignes de code, qui répond à une requête précise par une page de contenu et répond à toutes les autres avec une réponse 404.

Actuellement, notre serveur fonctionne dans une seule tâche, ce qui signifie qu'il ne peut répondre qu'à une seule requête à la fois. Examinons maintenant à quel point cela peut être un problème en simulant des réponses lentes à des requêtes. Ensuite, nous corrigerons notre serveur pour qu'il puisse gérer plusieurs requêtes à la fois.