Le but de ce TP est d'apprendre à utiliser
Les deux principaux protocoles impliqués dans les serveurs Web sont le Hypertext Transfer Protocol (HTTP) et Transmission Control Protocol (TCP). Les deux protocoles sont des protocoles de requête-réponse, ce qui signifie qu'un client lance des requêtes et qu'un serveur écoute les requêtes et fournit une réponse au client. Le contenu de ces demandes 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 façon dont les informations passent d'un serveur à un autre, mais ne précise pas quelles sont ces informations. 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 travaillerons avec les octets bruts des requêtes et réponses TCP et HTTP.
Ce code écoutera à l'adresse locale 127.0.0.1:7878 les flux TCP entrants. Lorsqu'il reçoit un flux entrant, il affiche Connexion établie !
.
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!"); } }
En utilisant TcpListener
, nous pouvons écouter les connexions TCP à l'adresse 127.0.0.1:7878
. Dans l'adresse, la section 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, il est donc peu probable que notre serveur entre en conflit avec un autre serveur Web que vous pourriez avoir en cours d'exécution sur votre machine, et 7878 est rouillé sur un téléphone.
La fonction bind
dans ce scénario fonctionne comme la nouvelle fonction en ce sens qu'elle renverra une nouvelle instance de TcpListener. La fonction est appelée bind car, en réseau, la connexion à un port pour écouter est connue sous le nom de liaison à un port.
La méthode incoming
sur TcpListener renvoie un itérateur qui nous donne une séquence de flux (plus précisément, des flux de type TcpStream). Un flux unique représente une connexion ouverte entre le client et le serveur. Une connexion est le nom du 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. En tant que tel, nous lirons à partir du TcpStream pour voir ce que le client a envoyé, puis nous écrirons notre réponse au flux pour renvoyer les données au client. Dans l'ensemble, cette boucle for traitera chaque connexion à tour de rôle et produira une série de flux que nous pourrons gérer.
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(); handle_connection(stream); } } 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(); println!("Request: {:#?}", http_request); }
Nous apportons std::io::prelude
et std::io::BufReader
dans la portée pour accéder aux traits et aux 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 indiquant que nous avons établi une connexion, nous appelons maintenant la nouvelle fonction handle_connection et lui transmettons le flux.
Dans la fonction 'handle_connection
, nous créons une nouvelle instance de BufReader
qui encapsule une référence mutable au flux. BufReader ajoute la mise en mémoire tampon en gérant les appels aux méthodes de 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
renvoie un itérateur de Result<String, std::io::Error>
en divisant le flux de données chaque fois qu'il voit un octet de retour à la ligne. Pour obtenir chaque chaîne, nous mappons et déballons chaque résultat. Le résultat peut être une erreur si les données ne sont pas UTF-8 valides ou s'il y a eu un problème de lecture à partir du flux. Encore une fois, un programme de production devrait gérer ces erreurs avec plus de grâce, mais nous choisissons d'arrêter le programme dans le cas d'erreur pour plus de simplicité.
Le navigateur signale la fin d'une requête HTTP en envoyant deux caractères de nouvelle ligne d'affilée, donc pour obtenir une requête du flux, nous prenons des lignes jusqu'à ce que nous obtenions une ligne qui est la chaîne vide. Une fois que nous avons collecté les lignes dans le vecteur, nous les imprimons en utilisant un joli format de débogage afin que nous puissions jeter un œil aux instructions que le navigateur Web envoie à notre serveur.
Nous allons mettre en place l'envoi de données en réponse à une demande 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 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 motif qui fournit une description textuelle du code d'état. Après la séquence CRLF se trouvent 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 HTTP version 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
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 response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); }
La première nouvelle ligne définit la variable de réponse qui contient les données du message de réussite. Ensuite, nous appelons as_bytes
sur notre réponse pour convertir les données de chaîne en octets. La méthode write_all
sur le flux prend un &[u8]
et envoie ces octets directement sur la connexion. Comme l'opération write_all
peut échouer, nous utilisons unwrap sur tout résultat d'erreur comme auparavant. Encore une fois, dans une application réelle, vous ajouteriez ici la gestion des erreurs.
Il s'agit d'un document HTML5 minimal avec un titre et du texte. Pour le renvoyer du serveur lorsqu'une requête est reçue, nous modifierons handle_connection comme indiqué pour lire le fichier HTML, l'ajouter à la réponse en tant que corps et l'envoyer.
use std::{ 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(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); }
À l'heure actuelle, le serveur traitera chaque demande à tour de rôle, ce qui signifie qu'il ne traitera pas une deuxième connexion tant que la première n'aura pas terminé le traitement. Si le serveur recevait de plus en plus de requêtes, cette exécution en série serait de moins en moins optimale. Si le serveur reçoit une requête dont le traitement est long, les requêtes suivantes devront attendre que la requête longue soit terminée, même si les nouvelles requêtes peuvent être traitées rapidement.
Un pool de threads est un groupe de threads générés qui attendent et sont prêts à gérer une tâche. Lorsque le programme reçoit une nouvelle tâche, il affecte l'un des threads du pool à la tâche, et ce thread traitera la tâche. Les threads restants dans le pool sont disponibles pour gérer toutes les autres tâches qui arrivent pendant le traitement du premier thread. Lorsque le premier thread a terminé de traiter sa tâche, il est renvoyé dans le pool de threads inactifs, prêt à gérer une nouvelle tâche. Un pool de threads vous permet de traiter les connexions simultanément, augmentant ainsi le débit de votre serveur.
Nous limiterons le nombre de threads dans le pool à un petit nombre pour nous protéger des attaques par Denial of Service (DoS) ; si notre programme créait un nouveau fil pour chaque requête au fur et à mesure qu'elle arrivait, quelqu'un faisant 10 millions de requêtes à notre serveur pourrait créer des ravages en utilisant toutes les ressources de notre serveur et en interrompant le traitement des requêtes.
Plutôt que de générer des threads illimités, nous aurons alors un nombre fixe de threads en attente dans le pool. Les demandes qui arrivent sont envoyées au pool pour traitement. Le pool maintiendra une file d'attente des demandes entrantes. Chacun des threads du pool supprimera une demande de cette file d'attente, traitera la demande, puis demandera à la file d'attente une autre demande. Avec cette conception, nous pouvons traiter jusqu'à N requêtes simultanément, où N est le nombre de threads. Si chaque thread répond à une requête de longue durée, les requêtes suivantes peuvent toujours être sauvegardées dans la file d'attente, mais nous avons augmenté le nombre de requêtes de longue durée que nous pouvons gérer avant d'atteindre ce point.
Nous voulons que notre pool de threads fonctionne de manière similaire et familière, de sorte que le passage de threads à un pool de threads ne nécessite pas de modifications importantes du code qui utilise notre API.
fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming() { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } }
sudo apt install netcat
avant de resoudre les exercices.