20 Mar 2017

Ne casse pas ta pipe, viens maîtriser les streams !

De part la nature asynchrone et événementielle de NodeJS, il est essentiel de bien comprendre le fonctionnement des streams. En effet, NodeJS est très efficace quand il s’agit de traiter des problématiques d’I/O et c’est pour cela que nous vous proposons un article sur ce concept afin que vous puissiez en profiter lors de vos développements. Explorons donc le concept des streams…

nodejs_logo

Essayons nous à en écrire une définition…

“Un stream est une interface abstraite qui permet de transférer en continu de la donnée d’un point A à un point B.”

Bon, nous avons là une définition mais est-ce “sérieusement” suffisant pour avoir une meilleur compréhension des streams ? Certainement pas…. Continuons…

Nous avons dit au préalable que de nombreux composants de NodeJS utilisent les streams pour leur fonctionnement :

  • Lecture / Écriture dans des fichiers,
  • Lecture / Écriture dans la console,
  • Requêtes / Réponses HTTP,
  • Etc.,

… et à travers ces quelques exemples, nous devinons déjà qu’il y a des streams qui ne transfèrent pas les données dans le même sens ou pour certaines qui agissent sur les données.

Par exemple, une requête HTTP fonctionne donc sur la base d’un stream de type readable tandis qu’une réponse est un stream de type writable. Vous pouvez aussi avoir utilisé à travers vos développements la librairie fs qui permet de lire et écrire dans des fichiers qui utilise donc des streams readable ET writable.

Autre exemple (d’un autre univers), si vous avez déjà utilisé les pipe dans Unix ( | ) alors il faut savoir que les streams en NodeJS fonctionnent sur le même principe.

En effet, la commande suivante : $ ls | grep *.js >> js_files.txt va lister tous les fichiers, ne retenir que ceux avec l’extension .js et écrire le fruit de cette sélection dans le fichier js_files.txt.

Il existe donc plusieurs types de streams :

  • Readable : Streams depuis qui de la donnée peut être lue (fs.createReadStream()),
  • Writable : Streams dans lequel de la donnée peut être écrite (fs.createWriteStream()),
  • Duplex : Streams depuis qui de la données peut être lue et écrite (net.Socket),
  • Transform : Streams qui permettent de modifier de la donnée,
  • Passthrough  : Streams qui permettent d’observer sans modifier de la donnée,

Transform

Nous le verrons par la suite (et avec des exemples) mais NodeJS v6 met à disposition des API de haut niveau qui permettent de travailler simplement avec les streams.

Qu’est-ce qui rend les streams si puissants et utiles ?

Comme il est facile de transférer de la données via les streams, il est possible de développer plein de petits modules indépendants qui vont échanger de la donnée entre eux (à la manière de ce qui existe déjà dans Unix) et  c’est ça qui rend les streams si utiles. Le fait de développer une librairie ou un module selon ce principe permet de se concentrer sur le mantra suivant : “Do one thing and do it well”. Et c’est l’assemblage de ces petits modules qui permettra de résoudre des tâches plus compliquées. Gulp est un bon exemple de librairie permettant d’assembler via les streams plusieurs petits traitements.

En résumé, les streams sont et permettent :

  • Produire ou consommer de la données via des chunks de données (dans des buffers),
  • Sur la base d’événements et sans bloquer le système,
  • Avec une faible empreint mémoire,
  • Et en automatisant les problématiques de gestion de mémoire,
  • Et merci de noter que beaucoup de modules de node sont déjà stream ready.

Comment fonctionne un stream ?

Comment on s’en doute, si l’objectif d’un stream est de transférer de la donnée petit à petit d’un point A à un point B, alors son fonctionnement repose notamment sur les événements. En effet, les deux points A et B vont via des événements s’informer sur les paquets transférés et sur les états de ces transferts. Dans NodeJS, tous les streams sont des instances de EventEmitter qui ont des propriétés et méthodes spéciales.

Un stream est donc un flux (“un tuyau”) où plusieurs événements se déclenchent successivement. Ceux sont les événements qui permettent l’orchestration des transferts des blocs de données (chunks). Comme cela, au lieu de bloquer A et B le temps du transfert, ils peuvent faire autre chose en “parallèle”.

Dans NodeJS, les streams échangent entre eux principalement des strings et des buffers (il est possible de transférer d’autres types de données également, c’est ce que l’on appelle l’object mode). Afin d’envoyer les données par paquet, il existe une mécanique interne qui va découper les données à transférer. Ce mécanisme s’appelle le chunking. Cela permet de transférer de manière plus simple et plus sur des plus petits bouts de données via les buffers. À titre informatif, les buffers de NodeJS sont implémentés pour utiliser des données génériques. En effet, toutes les données vont être converties dans des tableaux fixes d’entiers (en fonction de l’encodage choisi, UTF-8 par défaut). Ces mêmes entiers qui représentent des bytes de nos buffers  sont stockés dans un espace mémoire de la heap du moteur V8. NodeJS fait le choix de transférer des données binaires car cela est plus sûr, plus universel et plus rapide.

Car un schéma peut aider à mettre ces explications en perspectives :

chunk

Ensuite, vous devez savoir que NodeJS va automatiquement gérer les problématiques de temporisation s’il y a trop de données dans les buffers et va reprendre le transfert quand cela sera possible. Cela s’appelle le back pressure.

Le back pressure permet donc de faciliter la gestion des flux afin d’éviter les surcharges mémoires. Concrètement, l’envoi des données va être arrêté le temps que NodeJS puisse en traiter d’autres.

Jouons avec les streams

Dans notre exemple, nous allons lire les données depuis ce site mock afin de récupérer les noms des villes de chaque état afin de les mettre en minuscule et dans le même temps de les écrire un fichier local en sortie.

Importer la classe Transform :

Implémentons notre classe étendant Transform :

 

Appelons là :

L’ensemble du code est consultable ici.

Conclusion

Voilà, j’espère que cet article vous aide à y voir plus clair sur les streams.

Sinon en résumé, voici quand utiliser chaque type de stream :

  • READABLE stream : à utiliser quand vous voulez fournir de la donnée,
  • WRITABLE stream : à utiliser quand vous voulez collecter de la donnée,
  • DUPLEX stream : à utiliser quand vous voulez fournir ou collecter de la donnée mais sur des “streams” distincts,
  • TRANSFORM stream : à utiliser quand vous voulez faire des modifications sur les données circulant dans le stream,
  • PASSTHROUGH stream : à utiliser quand vous voulez observer les données qui circulent (comme le Transform stream mais sans modification)
Share