🚧 Attention, peinture fraüche !

Cette page a Ă©tĂ© traduite par une seule personne et n'a pas Ă©tĂ© relue et vĂ©rifiĂ©e par quelqu'un d'autre ! Les informations peuvent par exemple ĂȘtre erronĂ©es, ĂȘtre formulĂ©es maladroitement, ou contenir d'autres types de fautes.

Exécuter du code asynchrone

Un serveur HTTP doit ĂȘtre capable de servir plusieurs clients en concurrence, et par consĂ©quent, il ne doit pas attendre que les requĂȘtes prĂ©cĂ©dentes soient terminĂ©es pour s'occuper de la requĂȘte en cours. Le livre Rust rĂ©sout ce problĂšme en crĂ©ant un groupe de tĂąches oĂč chaque connexion est gĂ©rĂ©e sur son propre processus. Nous allons obtenir le mĂȘme effet en utilisant du code asynchrone, au lieu d'amĂ©liorer le dĂ©bit en ajoutant des processus.

Modifions le gestion_connexion pour retourner une future en la déclarant comme étant une fonction asynchrone :

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

L'ajout de async à la déclaration de la fonction change son type de retour de () à un type qui implémente Future<Output=()>.

Si nous essayons de compiler cela, le compilateur va nous avertir que cela ne fonctionnera pas :

$ cargo check
    Checking async-rust v0.1.0 (file:///projects/async-rust)
warning: unused implementer of `std::future::Future` that must be used
  -- > src/main.rs:12:9
   |
12 |         gestion_connexion(flux);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: futures do nothing unless you `.await` or poll them

Comme nous n'avons pas utilisĂ© await ou poll sur le rĂ©sultat de gestion_connexion, cela ne va jamais s'exĂ©cuter. Si vous lancez le serveur et visitez 127.0.0.1:7878 dans un navigateur web, vous constaterez que la connexion est refusĂ©e, notre serveur ne prend pas en charge les requĂȘtes.

Nous ne pouvons pas utiliser await ou poll sur des futures dans du code synchrone tout seul. Nous allons avoir besoin d'un environnement d'exécution asynchrone pour gérer la planification et l'exécution des futures jusqu'à ce qu'elles se terminent. Vous pouvez consulter la section pour choisir un environnement d'exécution pour plus d'information sur les environnements d'exécution, exécuteurs et réacteurs asynchrones. Tous les environnements d'exécution listés vont fonctionner pour ce projet, mais pour ces exemples, nous avons choisi d'utiliser la crate async-std.

Ajouter un environnement d'exécution asynchrone

L'exemple suivant va monter le remaniement du code synchrone pour utiliser un environnement d'exécution asynchrone, dans ce cas async-std. L'attribut #[async_std::main] de async-std nous permet d'écrire une fonction main asynchrone. Pour l'utiliser, il faut activer la fonctionnalité attributes de async-std dans Cargo.toml :

[dependencies.async-std]
version = "1.6"
features = ["attributes"]

Pour commencer, nous allons changer pour une fonction main asynchrone, et utiliser await sur la future retournée par la version asynchrone de gestion_connexion. Ensuite, nous testerons comment le serveur répond. Voici à quoi cela ressemblerait :

#[async_std::main]
async fn main() {
    let ecouteur = TcpListener::bind("127.0.0.1:7878").unwrap();
    for flux in ecouteur.incoming() {
        let flux = flux.unwrap();
        // Attention : cela n'est pas concurrent !
        gestion_connexion(flux).await;
    }
}

Maintenant, testons pour voir si notre serveur peut gĂ©rer les connexions en concurrence. Transformer simplement gestion_connexion en asynchrone ne signifie pas que le serveur puisse gĂ©rer plusieurs connexions en mĂȘme temps, et nous allons bientĂŽt voir pourquoi.

Pour illustrer cela, simulons une rĂ©ponse lente. Lorsqu'un client fait une requĂȘte vers 127.0.0.1:7878/pause, notre serveur va attendre 5 secondes :

use async_std::task;

async 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";
    let pause = b"GET /pause HTTP/1.1\r\n";

    let (ligne_statut, nom_fichier) = if tampon.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if tampon.starts_with(pause) {
        task::sleep(Duration::from_secs(5)).await;
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };
    let contenu = fs::read_to_string(nom_fichier).unwrap();

    let reponse = format!("{ligne_statut}{contenu}");
    flux.write(reponse.as_bytes()).unwrap();
    flux.flush().unwrap();
}

C'est trĂšs ressemblant Ă  la simulation d'une requĂȘte lente dans le livre Rust, mais avec une diffĂ©rence importante : nous utilisons la fonction non bloquante async_std::task::sleep au lieu de la fonction bloquante std::thread::sleep. Il est important de se rappeler que mĂȘme si un code est exĂ©cutĂ© dans une fonction asynchrone et qu'on utilise await sur elle, elle peut toujours bloquer. Pour tester si notre serveur puisse gĂ©rer les connexions en concurrence, nous avons besoin de nous assurer que gestion_connexion n'est pas bloquante.

Si vous exĂ©cutez le serveur, vous constaterez qu'une requĂȘte vers 127.0.0.1:7878/pause devrait bloquer toutes les autres requĂȘtes entrantes pendant 5 secondes ! C'est parce qu'il n'y a pas d'autres tĂąches concurrentes qui peuvent progresser pendant que nous utilisons await sur le rĂ©sultat de gestion_connexion. Dans la prochaine section, nous allons voir comment utiliser du code asynchrone pour gĂ©rer en concurrence les connexions.