Javascript
Programmation asynchrone

Se méfier de son intuition …

  setTimeout(function(){console.log('b')},0)
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
function a(){
  console.log('a')
  b()
}

                        
function b(){
  setTimeout(function(){console.log('b')},0)
  c()
}

                        
function c(){
  console.log('c')
}

                        
a()
  • setTimeout(cb, milliseconds) : exécute la fonction cb après que le délai soit écoulé
  • Quel est le résultat affiché par node ?

Environnement d'exécution javascript

L'environnement d'exécution javascript (node ou navigateur) fournit:

  • un seul thread d'exécution pour votre code !
  • queue de message pour différer des actions
  • une boucle d'évaluation globale qui dépile la queue de message et exécute les actions différées

EventLoop : boucle d'exécution principale

while (atLeastOneEventIsQueued) {
1
2
3
4
 
runYourScript();
while (atLeastOneEventIsQueued) {
  fireNextQueuedEvent();
};
  1. votre code est exécuté, et diffère des actions
  2. après l'exécution de votre code, l'EventLoop sélectionne une des actions différées et l'exécute
  3. à chaque fin du code courant, on reprend : trouver quoi faire dans la queue, exécuter

Remarque : chaque code exécuté peut à son tour différer des actions …

Programmation asynchrone

Les actions différées sont définies de la manière suivante:

  • l'évènement qui enclenchera le début de la résolution
  • le code à appeler : en javascript, ce sera une fonction

Les évènements signalés dépendent de l'environnement d'exécution :

  • navigateur : input utilisateur (souris, clavier), flux réseau (ajax)
  • node.js : évènements liés à des flux I/O (fichiers, socket réseau) début de flux, données disponibles, fin de flux

Certains évèrements se retrouvent sur les 2 environnements :

  • timer qui arrive à échéance setTimeout, setInterval
  • évènement positionné par le développeur

La gestion des évènements est entièrement sous le contrôle de l'environnement.

Exemple node.js

request('http://gamba.enseeiht.fr/cours_javascript', callback);
1
2
3
4
5
6
7
8
 
var request = require('request')
var callback = function(error, response, body) {
  console.log(body);
}
// rajout d'un message (event, callback) 
request('http://gamba.enseeiht.fr/cours_javascript', callback);

                        
console.log('Done!');
  • request est une fonction asynchrone : non exécutée dans le thread courant
  • request empile dans la queue de message : évènement : fin de réception flux réseau (exécution de la requête http, réception réponse), action à exécuter: fonction callback

Résultat : «Done!» s'affiche d'abord, puis l'EventLoop attend un évènement. Quand l'évènement arrive (ici résultat de request), la fonction callback est exécutée.

Programmation évènementielle

Manière classique de gérer plusieurs clients

  • un thread en écoute pour gérer les connexions
  • un thread est utilisé pour la gestion des clients
  • - couteux en mémoire : un thread = 2Mo mémoire
  • + facile de gérer une tâche couteuse en CPU : on lui dédie un thread
  • -- synchronisation des threads

Modèle évènementiel

  • un thread principal : une seule boucle d'exécution
  • node.js : un seul process ! Pour la communication système, utilise les versions non bloquantes (epoll linux, kqueue *BSD/OS X, /dev/poll Solaris, select POSIX).
  • node.js : les entrées/sorties (réseau, fichier, base de données) peuvent être déléguées à des threads annexes
  • communication threads annexes <-> thread principal par une file de message
  • ++ économe en mémoire -> gère de nombreuses connexions simultanément
  • + synchronisation par message : pas de partage d'état, pas de soucis
  • - un seul process : tout est bloqué si le process principal calcule

node.js est donc à conseiller sur des programmes passant du temps sur les I/O et peu sur le calcul. Par exemple : une application web, mais pas seulement.

Exemple

fs.readFile('/tmp/test', consomme_fichier)
1
2
3
4
5
6
7
 
function consomme_fichier(err, data){
  if (err) { throw err }
  console.log(data)
}
fs.readFile('/tmp/test', consomme_fichier)

                        
console.log("contenu du fichier: ")
  • le thread principal (EventLoop) commence l'exécution du code fourni
  • un thread est crée pour la lecture du fichier
  • dans la file des évènements, on note : (si fin de lecture de /tmp/test, appeler consomme_fichier)
  • l'EventLoop arrive à la fin du code courant : on passe à la boucle suivante (Tick)
  • l'EventLoop cherche dans la file des évènements : la file n'est pas vide, mais l'évènement ne s'est pas réalisé
  • le thread de lecture du fichier note dans la file que la lecture est finie, et fournit le résultat
  • l'EventLoop voit un évènement réalisé, et exécute le code associé
  • plus rien dans la file des évènement, programme fini

Event et node.js : EventEmitter

  • Interface pour des objets réagissant à des évènements
  • API simple

Extrait :

  • dans le module events : require('events').EventEmitter
  • emitter.addListener(event, listener) (ou emitter.on) :

C'est la fonctionnalité principale : on ajoute un écouteur à un évènement.

  • event : une chaîne de caractères
  • listener : une fonction appelée handler ou callback. On peut donner la signature que l'on veut.
server.on('connection', function(stream) {
1
2
3
 
server.on('connection', function(stream) {
  console.log('new connection');
}
  • emitter.emit(event, [arg1], [arg2], […] : exécute tous les écouteurs de l'évènement event.

Attention : si vous appeler emitter.emit, l'appel sera synchrone et réalisé immédiatement. Pour une gestion asynchrone, emit est utilisé par du code géré par l'environnement d'exécution pour signaler un évènement externe (fin de lecture d'un fichier par exemple)

La documentation d'un classe implémentant EventEmitter est donc essentiellement une suite de :

  • event: handler(arg1, arg2, …) : le nom de l'évènement, et la signature de la fonction qui le gère.

Node.js : Stream

L'interface Stream permet à node.js de traiter de manière uniforme tous les flux comme des EventEmitter.

Les flux peuvent être en lecture, écriture, ou les 2 (duplex).

stream.Readable

  • flowing mode: les données arrivent toute seules
  • non-flowing mode: demander les données avec stream.read()

Évènements gérés :

  • readable : soulevé quand des données peuvent être lues
  • 'data' : function(chunk) {…} : passe en flowing mode.
  console.log('got %d bytes of data', chunk.length);
1
2
3
4
 
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
  console.log('got %d bytes of data', chunk.length);
});
  • 'end' : signale la fin des données du flux

Méthodes:

  • readable.setEncoding(encoding):

    readable.setEncoding('utf8');

  • readable.read([size])

stream.Writable

  • writable.write(chunk, [encoding], [callback])

    • retourne un booléen. true signifie que les données écrites ont été traitées.
    • callback : appelé après que les données aient été lu du buffer chunk.

  • event drain : si writable.write(chunk) retourne false, drain signale quand l'écriture est de nouveau possible.

  • writable.end() : signale que l'écriture est finie, émet l'évènement finish.

Application pour un serveur http :

Mise en application

// server is an [EventEmmiter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 
var http = require('http');
var server = http.createServer();
var util = require('util');

                        
// server is an [EventEmmiter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
// Events :
// request : when a request is received : function(request, response) {}
// connection: function(socket){}
// close : function(){}

                        
server.addListener('request', handle);

                        
function handle(req, res) {
  var message = "request received:\n";
  var url = require('url').parse(req.url, true);
  var body = '';
  message += "method: " + req.method + "\n";
  message += "path: " + url.pathname + "\n";
  message += "query: " + util.inspect(url.query, {color: true}) + "\n";
  message += "headers: " + util.inspect(req.headers, {color: true}) + "\n";
  message += "body: \n";
  req.on('data', function(chunk){
    body += chunk;
    console.log("---\n" + chunk +"---\n");
  });
  req.on('end', function(){
    message += body
    console.log(message);
    res.writeHead(200, {
      'Content-Length': Buffer.byteLength(message),
      'Content-Type': 'text/plain'
    });
    
    res.write(message, 'utf8'); // utf8 is the default
    res.end();
  });
  
}

                        
server.listen(8000);
console.log("listening on port 8000");

Faire tourner le serveur

node http.js

Tester ce mini serveur avec curl

curl -X GET -H 'MyHeader: space/and_beyond' http://localhost:8000/toto
curl -X PUT -H 'Content-Type:application/json' --data "{toto:titi, a: 42}" http://localhost:8000/putit

Modifier le serveur pour renvoyer une page html contenant Hello Word quand une requête de GET /hello est demandée.

  1. Le document html que vous inclurez dans le corps de la réponse est stocké dans une variable javascript.
  2. Le document html est lu à partir d'un fichier. Pour cela, utiliser fs.readFile
  3. Lire d'un fichier : Readable Stream. Écrire dans la réponse http : Writable Stream. Combinez fs.createReadStream avec res.write/res.end en utilisant readable.pipe

12/12

#