LIFWEB - CM6

Programmation JavaScript Serveur Node.js

Romuald THION

Semestre printemps 2023-2024 UCBL

Web APIs et fetch côté client

https://save418.com/ https://www.google.com/teapot

Vérifier une réponse HTTP

💡 On a souvent besoin d’un handler pour vérifier la réponse HTTP pour faire la différence entre :

  • Une erreur de réseau
    • pas d’accès internet, nom DNS non résolu, etc.
  • Une réponse HTTP avec un code 4xx ou 5xx
    • une erreur applicative client ou serveur.

Propriété ok (MDN)

function checkOK(response) {
  if (!response.ok) {
    throw new Error(`[${response.status}] ${response.statusText}`);
  }
  return response;
}

🗒️ Remarques :

  • On crée toujours un objet Error (MDN).
  • On laisse remonter l’erreur à l’appelant.
  • Si OK, on renvoie response pour chaînage.

📄 exemple-check-ok-http.js.

Usage Promise

fetch("https://httpbin.org/status/418")
  .then(checkOK)
  .then((resp) => console.log(resp.status))
  .catch(console.error);

Usage async/await

try {
  const resp = checkOK(await fetch("https://httpbin.org/status/418"));
  console.log(resp.status);
} catch (error) {
  console.error(error);
}

TP GitHub Verifier (2/2)

Démonstration du résultat attendu du TP4-b.

Applications Web

  • Des APIs Web comme https://thecatapi.com/ :
    • logique métier, données, etc. ;
    • éventuellement présentation HTML ;
    • en Node.js (mais peut-être Java, Python, Rust).
    • 👉 la suite de LIFWEB
  • Des clients qui les utilisent :
    • un squelette HTML/CSS ;
    • alimenté à partir des APIs via JS.
    • 👉 précédemment dans LIFWEB.

Utiliser une API Web

Exemple sur https://lifweb.univ-lyon1.fr/

Exemple : route health via l’interface

Voir la doc live.

Exemple : avec https://curl.se/

curl https://lifweb.univ-lyon1.fr/health -H 'accept: application/json'

À combiner avec jq pour le rendu.

{
  "postgresInfos": {
    "postgresVersion": "16.2 (Ubuntu 16.2-1.pgdg22.04+1)",
    ...
    "driver": "https://github.com/porsager/postgres"
  },
  "serverId": "lifweb:369857:lsuib8s3",
  "serverStartedAt": "2024-02-20T15:14:54.519Z",
  "serverUri": "http://localhost:8001",
  ...
  "title": "lif-web-challenge-server",
  "version": "1.1.1"
}

Exemple : avec fetch

https://curlconverter.com/ permet de traduire la commande curl.

fetch("https://lifweb.univ-lyon1.fr/health", {
  headers: {
    accept: "application/json",
  },
});

Exemple : avec https://httpie.io/

https lifweb.univ-lyon1.fr/health accept:application/json

🤩 Méthode (non programmable) préférée 🤩

  • Version CLI et GUI ;
  • Plus intuitif que curl ;
  • Complet, voir exemples.
https POST lifweb.univ-lyon1.fr/level/1/check \
  X-API-KEY:"099f1cd0-23a2-4cef-82a3-769fe9ceb2e2" \
  challenge="9f60a610e7e9b5079b4a24d918808370"

# HTTP/1.1 200 OK
# Connection: keep-alive
# Content-Length: 87
# ...
{
  "duration_ms": 15945,
  "id_lvl": 1,
  "success": {
    "challenge": true,
    "level": true,
    "stage": true
  }
}

Environnement d’exécution Node.js

Logo Node.js

Node.js versus navigateur

Serveur/back

  • moteur V8
  • accès système complet
    • sockets, threads…
  • event loop + pool de threads
  • modules
    • CommonJS (CJS)
    • EcmaScript (ESM) 👈

Client/front

  • moteur du browser
  • limitations de sécurité
    • CORS…
  • events loop du navigateur
  • modules EcmaScript

🚧 Node.js s’aligne progressivement sur EcmaScript et les APIs Web, mais les APIs historiques restent. 🚧

Exécution de programme Node.js

S’utilise comme Python ou OCaml (sans compilation) :

  • soit en interpréteur interactif REPL
    • REPL = Read Eval Print Loop
  • soit en interpréteur non interactif
    • node file.js

💡 La majorité des outils de l’écosystème JS utilisent Node.js.

À la différence de Python ou OCaml :

  • 🔁 Node.js intègre nativement une boucle d’événements
    • programmation asynchrone/événementielle native
    • applications orientées serveur
  • 🚀 les applications I/O intensive sont performantes
  • 🐢 Les applications CPU intensive sont lentes
    • JS est un langage interprété et lent
    • On interface JS à du code natif (C/C++/Rust)

Exemple : serveur CTF

Avec AutoCannon : 2 vCPUs, Nginx HTTPS, accès PostgreSQL. Réseau fibre SFR.

autocannon --connections 512 --workers 16\
"https://lifweb.univ-lyon1.fr/health"

# ┌─────────┬────────┬───────────┬───────────┬─────────┐
# │ Stat    │ 97.5%  │ Avg       │ Stdev     │ Max     │
# ├─────────┼────────┼───────────┼───────────┼─────────┤
# │ Latency │ 206 ms │ 100.08 ms │ 279.16 ms │ 6635 ms │
# └─────────┴────────┴───────────┴───────────┴─────────┘
# ┌───────────┬─────────┬────────┬────────┬─────────┐
# │ Stat      │ 97.5%   │ Avg    │ Stdev  │ Min     │
# ├───────────┼─────────┼────────┼────────┼─────────┤
# │ Req/Sec   │ 4,535   │ 3,968  │ 417.74 │ 3,061   │
# ├───────────┼─────────┼────────┼────────┼─────────┤
# │ Bytes/Sec │ 3.77 MB │ 3.3 MB │ 348 kB │ 2.54 MB │
# └───────────┴─────────┴────────┴────────┴─────────┘

📄 perf.json.

Runtimes JS aujourd’hui

De nouvelles alternatives à Node.js

🔮 Standardisation JS backend https://wintercg.org/. 🔮

Architecture de Node.js

Source - RichOnTheWeb

I/O versus CPU

Une requête HTTP : des 100aines de millions de cycles !

🏆 Là où Node.js brille : I/O concurrentes. 🏆

What is the meaning of I/O intensive in Node.js

Gérer la concurrence

Un processus/thread par client

Architecture thread based (source)

🤔 Voir le TP11 de LIF - Système d’Exploitation

Architecture événementielle

Architecture événementielle worker (source)

💡 Processus/threads de travail auquel sont délégués les traitements via un démultiplexeur.

Boucle d’événements Node.js : libuv

https://libuv.org/ composant clef de Node.js (source)

Principe de la libuv

Initialement conçue comme la boucle d’événements de Node.js, la libuv est utilisée comme boucle d’événements en Python (uvloop), Lua (Luvit), …

while there are still events to process:
    e = get the next event
    if there is a callback associated with e:
        call the callback
  • 🔗 Asynchronous TCP/UDP sockets
  • 🌐 Asynchronous DNS resolution
  • 📁 Asynchronous file and file system operations
  • 🧵 Threads, processes
  • 🕥 Timers, …

Ne jamais bloquer la boucle

☣️ Bloquer le thread principal avec du calcul des I/O synchrones, c’est bloquer la boucle d’événements.☣️

Les handlers doivent :

  • Être de complexité minimale (idéalement \mathcal{O}(1));
  • Être de durées équivalentes entre clients ;
  • Ne pas utiliser de fonctions sync.

Don’t Block the Event Loop (or the Worker Pool).

Node.js, lots of ways to block your event-loop (and how to avoid it).

Détails sur les boucles d’événements

En fait, l’histoire est plus complexe. C’t-à-dire que l’mec a dit : … Vald, « Bonjour ».

🔖 Voir Microtasks - javascript.info, The Node.js Event Loop, Timers, and process.nextTick() - Node.js, When to use queueMicrotask() vs. process.nextTick() - Node.js.

Tâches et micro-tâches

🔁 Il y a plusieurs queues d’événements :

  • Les tâches à chaque tour de boucle.
  • Les micro-tâches en fin de chaque tour :
    • On ne passe à la tâche suivante que lorsque la queue des micro-tâches est vide.

⚙️ API queueMicrotask - MDN, queueMicrotask - Node.js, setImmediate - Node.js, timerPromises.setImmediate - Node.js et process.nextTick - Node.js.

Exemple : tâches/micro-tâches

import { setTimeout, setImmediate } from "node:timers";
import { setTimeout as setTimeoutPromise } from "node:timers/promises";
console.info("Start");
setImmediate(() => console.log(0));
queueMicrotask(() => console.log(1));
setTimeout(console.log, 0, 2);
setTimeoutPromise(0, 3).then(console.log);
setTimeout(console.log, 0, 4);
Promise.resolve(5).then(console.log);
console.info("End");

💡 Affichera dans l’ordre Start, End, 1, 5, 0, 2, 3 puis 4.

Les phases de la boucle

The Node.js Event Loop, Timers, and process.nextTick() - Node.js.

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

💡 Ce diagramme est en fait celui de libuv

Event-driven architecture

https://nodejs.org/api/events.html#events

Much of the Node.js core API is built around an idiomatic asynchronous event-driven architecture in which certain kinds of objects (called “emitters”) emit named events that cause Function objects (“listeners”) to be called.

Toute l’architecture Node.js est asynchrone, orientée événements :

  • Concurrence coopérative native (et non préemptive)
  • Performant pour les applications I/O intensive
    • Dont les serveurs Web
  • Valable pour toute l’API Node.js
    • fichiers, streams, socket, crypto, process
    • 💡 tout est asynchrone

💡 A la différence de LIF - Programmation Concurrente et Administration :

  • On choisit quand suspendre les exécutions
    • Programmatiquement, ce n’est pas l’OS
  • Programmation multithread limitée :
    • Le moteur de la boucle est mono-thread
    • Les worker threads sont cachés par la libuv
    • Threads possibles via Web Workers (MDN)
      • Hors périmètre LIFWEB

Exemple ping/pong

import { EventEmitter } from "node:events";
/* ... */
const emitter1 = new EventEmitter();
emitter1.last = hrtime.bigint();

emitter1.on("ping", async (value, time) => {
  console.info(`[1] received ${value}@+${time - emitter1.last}`);
  emitter1.last = time;
  emitter2.emit("ping", value, hrtime.bigint());
});
/* ... idem emitter1 */
emitter1.emit("ping", 0, hrtime.bigint());
emitter1.emit("ping", 0, hrtime.bigint());
// [1] received 0@+18809
// [2] received 0@+295990871
// ...

📄 exemple-ping-pong.js.

Design Pattern Observer

The Observer pattern defines an object (called subject) that can notify a set of observers (or listeners) when a change in its state occurs. (Node.js Design Patterns)

📖 Un des 23 designs patterns du Gang of Four, classé behavioural pattern, central de la conception des événements Node.js (et navigateur).

Diagramme de classes UML (Wikipedia)

EventEmitter / EventTarget

  • Node.js implémente nativement le patron Observer avec les classes
    • EventEmitter (doc)
    • EventTarget (doc)
  • Correspondance avec l’UML
    • On enregistre update() via un callback
    • registerObserver = addEventListener
    • notifyObservers = dispatchEvent

EventEmitter

  • natif Node.js
  • v0.1.26
  • e.on(...)
  • e.emit(...)
  • ❎ héritage
  • bubbling
  • event : string

EventTarget

  • Web API (DOM/navigateurs)
  • v14.5.0
  • t.addEventListener(...)
  • t.dispatchEvent(...)
  • ✅ héritage
  • bubbling
  • Event : classe

🔖 Node.js EventTarget vs. DOM EventTarget.

const ee = new EventEmitter();
const listenEmit = (id) => (value) => console.log(`[${id}]: ${value}`);
ee.on("ping", listenEmit("A"));
ee.emit("ping", 42);

const et = new EventTarget();
const listenTarget = (id) => (event) => console.log(`[${id}]: ${event.detail.value}`);
et.addEventListener("ping", listenTarget("A"));
// ici avec un CustomEvent
et.dispatchEvent(new CustomEvent("ping", { detail: { value: 42 } }));

📄 node-target-vs-emitter.js.

👉 EventTarget s’aligne sur le navigateur et le standard. Voir Prefer EventTarget over EventEmitter.

CPS ou EventEmitter ?

// callback style CPS
function helloCallback(callback) {
  setTimeout(() => callback(undefined, "hello world"), 100);
}
helloCallback((error, message) => console.log(message));

// callback style event
import { EventEmitter } from "node:events";
function helloEvents() {
  const eventEmitter = new EventEmitter();
  setTimeout(() => eventEmitter.emit("complete", "hello world"), 100);
  return eventEmitter;
}
helloEvents().on("complete", (message) => console.log(message));

📄 node-callback-vs-emitter.js

💡 helloCallback() et helloEvents() sont fonctionnellement équivalents :

  • CB moins élégant si différents événements ;
  • CB naturellement appelé une seule fois ;
  • Plusieurs handlers possibles sur le même événement.

👉 EventEmitter apporte une abstraction sur Continuation Passing Style (CPS).

Exemple : serveur TCP echo

import net from "node:net";
function handleConnection(socket) {
  socket.on("error", (error) => console.error(`error...`));
  socket.on("end", () => console.debug(`closed...`));
  socket.on("data", (chunk) => socket.write(`server ${chunk}`));
  socket.write("Bonjour !\n");
}

const server = net.createServer();
server
  .on("connection", handleConnection)
  .on("listening", () => console.debug(`listening...`))
  .listen(1337);

📄 node-tcp-echo.js.

🤔 TP11 de LIF - Système d’Exploitation.

L’environnement de développement

NPM

npm, pNpM ou Yarn

npm : Node Packet Manager. pNpM et Yarn sont des successeurs.

npm est à Node.js ce que pip est à Python, avec un support natif des environnements type venv .

npm --version
# 8.16.0
npm init
# question interactives
npm install slugify
# added 1 package in 2s
cat main.js
# import slugify from "slugify"
# console.log(slugify("C'est un test ♥ !"));
node main.js
# C'est-un-test-love-!

le fichier package.json

{
  "name": "cm4_exemples",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "DEBUG=app nodemon server.js"
  },
  "dependencies": {
    "dotenv": "^10.0.0"
  },
  "devDependencies": {
    "eslint": "^7.32.0",
    "nodemon": "^2.0.12"
  }
}

La définition du projet et dépendances, voir docs.npmjs.com dépendances.

Exemple : lancement de package

JSON = JavaScript Object Notation : une représentation textuelle (une serialization) des objets JavaScript

Avec le fichier package.json précédent :

npm install
# added 296 packages in 2s

npm run dev
# > cm4_exemples@1.0.0 dev
# > cross-env DEBUG=app nodemon server.mjs

# [nodemon] 2.0.19
# [nodemon] to restart at any time, enter `rs`
# [nodemon] watching path(s): *.*
# [nodemon] watching extensions: js,mjs,json
# [nodemon] starting `node server.mjs`
#   app Server listening at http://127.0.0.1:5000/ +0ms

npx the package runner

npx permet d’exécuter des commandes locales au dossier (dans node_modules/) sans les installer globalement

https://www.npmjs.com/package/npx

Executes command either from a local node_modules/.bin, or from a central cache, installing any packages needed in order for command to run.

npx est implicitement utilisé par les scripts du package.json.

Exemple

/tmp/npx❯ npx cowsay "Hello world"
Need to install the following packages:
  cowsay@1.5.0
Ok to proceed? (y) y
 _____________
< Hello world >
 -------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
/tmp/npx❯ ll
/tmp/npx❯

Systèmes de modules

🤯 Plusieurs systèmes d’import co-existent : 🤯

Common JS (CJS)

  • historique
  • const maLib = require('laLib');
  • module.exports = { ... };
  • par défaut pour .js

EcmaScript (ESM) :

  • standard JS
  • import maLib from 'laLib';
  • export default { ... };
  • par défaut pour .mjs

💡 Préférer les modules ESM 💡

Conclusion

Premier projet Node.js

Voir http://lifweb.pages.univ-lyon1.fr/package.json.

💯 Servira désormais de référence. 💯

Voir CM5 sur https://eslint.org/, https://prettier.io/, etc.

📐 On choisit une convention et on s’y tient. 📐

Bonnes pratiques / DevOPS

https://12factor.net/

Futur TPs

Architecture n-tiers classique (source)
  • Web API en Node.js ;
  • BD PostgreSQL ;
  • Nginx en reverse proxy ;
  • VM Ubuntu 22.04 ;
  • Client AJAX qui utilise l’API.