Scanner en parallèle
Nous souhaitons effectuer plusieurs scans en parallèle. Pour cela, nous allons modifier notre sous-commande ping
pour qu'elle accepte une liste d'hôtes ainsi qu'une liste de ports, pour pouvoir faire par exemple :
$ cargo run -- ping www.enst.fr,google.com,localhost 80,443,22
google.com:22 timed out
google.com:80 is open
google.com:443 is open
localhost:22 is open
localhost:80 is closed
localhost:443 is closed
www.enst.fr:22 timed out
www.enst.fr:80 is open
www.enst.fr:443 is open
-
Modifiez la sous-commande
ping
pour qu'elle récupère une liste d'hôtes séparés par des virgules ainsi qu'une liste de ports séparés par des virgules. -
Implémentez une fonction
scanner::net::tcp_ping_many()
qui accepte une liste d'hôtes et une liste de ports et effectue en parallèle un appel àscanner::net::tcp_ping()
.
async fn tcp_ping_many<'a>(targets: &[(&'a str, u16)])
-> Vec<(&'a str, u16, Result<bool, Error>)>
{
todo!()
}
Pour chaque couple (host, port)
, cette fonction renverra le résultat de tcp_ping()
dans un triplet rappelant également l'hôte et le port.
Vous remarquerez qu'on a dû nommer la lifetime des chaînes de caractères avec un paramètre générique 'a
. Sans cela, le compilateur n'aurait pas pu choisir entre la lifetime de la slice ou celle des données contenues dans la slice pour affecter une lifetime aux &str
dans le type de retour.
Ci-dessous, on trouvera des conseils pour implémenter l'exécution parallèle des différentes connexions.
Outils disponibles pour l'exécution parallèle
Pour l'exécution parallèle, on peut opter (entre autre) pour deux mécanismes différents :
- À partir d'un stream contenant des
Future
, on peut utiliserbuffer_unordered()
pour exécuter lesFuture
en provenance du stream en limitant le nombre deFuture
non terminées qui s'exécutent en parallèle. - À partir d'un itérateur sur des
Future
, on peut construire unfutures::stream::FuturesUnordered
qui implémenteStream
et fournit les résultats des futures au fur et à mesure qu'ils sont disponibles.
Ces deux mécanismes sont détaillés ci-dessous.
FuturesUnordered
Ce type permet de regrouper des Future
et de les exécuter en parallèle mais renvoie les résultats sous la forme d'un Stream
qu'on lira de manière asynchrone. Ce FuturesUnordered
peut être construit grâce à collect()
sur un itérateur contenant des Future
.
Une fois ce type FuturesUnordered
construit, on peut appeler collect()
(disponible dans le trait futures::stream::StreamExt
) pour collecter le contenu de ce stream dans une collection qui sera disponible de manière asynchrone. On fera donc la succession d'opérations :
itérateur sur (host, port)
-> itérateur sur des futures contenant chacune (host, port, résultat)
-> FuturesUnordered qui un est stream produisant des (host, port, résultat)
-> future contenant un vecteur avec tous les (host, port, résultat)
On s'aperçoit que le dernier type est exactement ce qu'on veut renvoyer depuis la fonction asynchrone tcp_ping_many()
.
Attention toutefois : il y a un risque de dépasser le nombre de descripteurs de fichiers pouvant être ouvert par le programme courant. Dans ce cas, il ne sera pas possible de créer d'autres TcpStream
.
buffer_unordered()
Cette fonction qui s'applique à un stream de Future
prend en paramètre le nombre de Future
non terminées qui s'exécutent en parallèle. Cela permet donc de limiter le nombre d'exécutions parallèle, par exemple pour ne pas dépasser le nombre de descripteurs de fichier disponibles pour notre programme. Les opérations peuvent être schématisées par :
stream sur (host, port)
-> stream sur des futures contenant chacune (host, port, résultat)
-> .buffer_unordered(N) qui produit un stream avec (host, port, résultat)
-> .collect() : future contenant un vecteur avec tous les (host, port, résultat)
Là aussi on obtient le type attendu. C'est le mécanisme conseillé pour implémenter le parallélisme dans le cas présent.
- Implémentez une fonction
scanner::net::tcp_mping()
qui prend une liste d'hôtes et liste de ports, qui construit une liste de(host, port)
cible et qui appelletcp_ping_many()
sur cette liste.
pub async fn tcp_mping<'a>(targets: &[&'a str], ports: &[u16])
-> Vec<(&'a str, u16, Result<bool, Error>)>
{
todo!()
}
- Modifiez le programme principal pour qu'il appelle
tcp_mping()
au lieu detcp_ping()
. Changez la visibilité detcp_ping()
qui n'a plus de raison d'êtrepub
.
Vous remarquerez en exécutant un exemple similaire à celui se trouvant en haut de cette page que le temps d'attente maximal est de 3 secondes. En effet, chaque tentative de connexion s'exécute en parallèle et génère un timeout au bout de 3 secondes.