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