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

  2. 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 utiliser buffer_unordered() pour exécuter les Future en provenance du stream en limitant le nombre de Future non terminées qui s'exécutent en parallèle.
  • À partir d'un itérateur sur des Future, on peut construire un futures::stream::FuturesUnordered qui implémente Stream 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.

  1. 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 appelle tcp_ping_many() sur cette liste.
pub async fn tcp_mping<'a>(targets: &[&'a str], ports: &[u16])
    -> Vec<(&'a str, u16, Result<bool, Error>)>
{
  todo!()
}
  1. Modifiez le programme principal pour qu'il appelle tcp_mping() au lieu de tcp_ping(). Changez la visibilité de tcp_ping() qui n'a plus de raison d'être pub.

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.