TP 10 - Socket

Vous devez accepter le assignment d'ici et travailler avec ce repository: Lab10

Objectifs

Le but de ce TP est d'apprendre à utiliser

  • Comment utiliser les sockets

Création d'un serveur Web à un seul thread

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.

Écouter la connexion TCP

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.

Lecture de la request

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.

Ecrire d'une réponse

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.

Renvoyer du vrai HTML

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();
}

Transformer notre serveur monothread en un serveur multithread

À 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.

Thread pool

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.

Création d'un nombre fini de threads

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);
        });
    }
}

Voici à quoi devrait ressembler le main après l'implémentation du trait ThreadPool. Pour plus de détails sur cette implémentation, veuillez lire les chapitres restants de la documentation, en continuant à partir d'ici.

Bibliographie

Exercices

Executer la commande sudo apt install netcat avant de resoudre les exercices.

  1. Exécutez le programme dans ex1.
  2. Résolvez les lignes TODO (dans l'ordre) à partir de ex1.
  3. Résolvez les lignes TODO (dans l'ordre) de ex2.
  4. Résolvez les lignes TODO (dans l'ordre) de ex3.
  5. Implémentez ex1 et ex2 à l'aide de sockets UDP.
sde2/laboratoare/10_rust.txt · Last modified: 2023/05/15 23:50 by cristiana.andrei
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0