mio pour surveiller des descripteurs
de fichiersmio et les appels système
epoll (Linux) / kqueue (macOS)Dans le cours, nous avons implémenté une file d’évènements en
utilisant directement les appels système epoll_create1,
epoll_ctl, et epoll_wait. En pratique, la
crate mio (Metal I/O) expose exactement la même
abstraction mais de façon portable (Linux, macOS, Windows) et
idiomatique en Rust. Elle est notamment la brique de base sur laquelle
tokio est construite.
L’objectif de ce TP est de réimplémenter le programme vu en cours:
plusieurs connexions TCP vers un serveur, avec traitement des réponses
par file d’évènements, en utilisant mio à la place de nos
appels epoll_create1/ctl/wait.
delayserverPour tester votre implémentation, vous avez besoin du serveur vu en
cours. Créez un projet Cargo séparé delayserver avec les
dépendances suivantes dans Cargo.toml:
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }Et le code source dans src/main.rs:
use axum::{Router, extract::Path, http::StatusCode,
response::IntoResponse, routing::get};
use std::time::Duration;
use tokio::time::sleep;
async fn delay_handler(Path((delay, message)):
Path<(u64, String)>) -> impl IntoResponse {
sleep(Duration::from_millis(delay)).await;
(StatusCode::OK, message)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/{delay}/{message}", get(delay_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
println!("Serveur démarré sur 127.0.0.1:8080");
axum::serve(listener, app).await.unwrap();
}Lancez-le avec cargo run dans un terminal séparé. Il
accepte des requêtes de la forme:
GET /500/bonjour HTTP/1.1\r\nHost: localhost\r\n\r\n
et répond après 500 ms avec le corps
bonjour.
Créez un nouveau projet Cargo event_queue avec les
dépendances suivantes:
[dependencies]
mio = { version = "1", features = ["net", "os-poll"] }Écrivez une fonction
get_request(id: usize, n: usize) -> String qui construit
une requête HTTP/1.1 vers le serveur delayserver. Le délai
de la requête numéro id doit être proportionnel à
n - id (en millisecondes, multiplié par 1000 par exemple),
de façon à ce que la dernière requête envoyée soit la première à
recevoir une réponse. Le message peut être
"message_{id}".
Format attendu (requête HTTP brute):
GET /{delay}/{message} HTTP/1.1\r\nHost: localhost\r\n\r\n
Dans main, ouvrez n = 5 connexions TCP vers
127.0.0.1:8080 à l’aide de
mio::net::TcpStream::connect. Envoyez immédiatement la
requête sur chaque connexion avec write_all en ignorant les
erreurs éventuelles: sur loopback, le handshake TCP est quasi-instantané
et le noyau accepte l’écriture avant même que la connexion soit
pleinement établie.
Ajoutez l’en-tête Connection: close à la requête: cela
demande au serveur de fermer la connexion après avoir envoyé sa réponse,
ce qui produira Ok(0) lors de la lecture côté client.
Créez une instance mio::Poll et son
mio::Events. Enregistrez chaque stream TCP dans le registre
de la file avec:
mio::Token) correspondant à l’indice
i du streammio::Interest::READABLECorrespondance avec le cours:
mio |
epoll (cf. cours) |
|---|---|
Poll::new() |
epoll_create1(0) |
registry.register(...) |
epoll_ctl(EPOLL_CTL_ADD) |
poll.poll(...) |
epoll_wait(...) |
mio::Token(i) |
epoll_data = i |
Interest::READABLE |
EPOLLIN |
Implémentez la boucle principale qui:
poll.poll(&mut events, None) pour bloquer
jusqu’au prochain évènementOk(n)), s’arrête sur WouldBlockOk(0) et incrémente le
compteur de réponses traitéesn réponses ont été
reçuesObservez l’ordre d’arrivée des réponses. Vérifiez que:
4 (délai le plus court) arrivent
en premier0 (délai le plus long) arrivent en
dernierToken) correspondent bien aux streams
enregistrésAjoutez un affichage préfixé par le token pour chaque chunk reçu:
[token=4] HTTP/1.1 200 OK ...
[token=3] HTTP/1.1 200 OK ...
...
Tous les évènements traités.
WouldBlockAvec des sockets non-bloquants, read() peut retourner
ErrorKind::WouldBlock à tout moment. Cela signifie
simplement qu’il n’y a plus de données disponibles pour
l’instant. Ce n’est pas une erreur fatale: il faut sortir de la
boucle de lecture interne et attendre la prochaine notification de
poll.
use std::io::{self, Read};
match stream.read(&mut buf) {
Ok(0) => { /* connexion fermée → réponse terminée */ break; }
Ok(n) => { /* afficher buf[..n] */ }
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
/* pas de données → attendre */ break;
}
Err(e) => return Err(e),
}WouldBlockAvec mio, il n’est pas nécessaire de
re-enregistrer un stream après avoir reçu un WouldBlock.
Cependant, il est obligatoire de lire en boucle
jusqu’au WouldBlock avant de rappeler
poll.
En effet, mio utilise EPOLLET
(edge-triggered) en interne sur Linux: exactement comme dans
l’implémentation du cours. En mode edge-triggered, le noyau n’envoie une
notification que lors d’un changement d’état (p. ex.
passage de “pas de données” à “des données sont disponibles”). Si on
s’arrête de lire avant d’avoir vidé le buffer, le noyau ne renverra pas
de notification pour les données restantes.