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 !"); } }
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[..])); }
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(); }
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>
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(); }
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 } }
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(); } }
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>
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(); }
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.