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