node:http et les streams : traiter les données en flux
Le module http vu de près et la lecture des données morceau par morceau.
Objectifs
À la fin de cette leçon, vous saurez :
- importer et utiliser
node:httppour un serveur minimal ; - expliquer pourquoi on lit certaines données en flux (streams) ;
- consommer un stream lisible avec les événements
dataetend; - relier deux flux avec
pipe().
🔗 Pour vous rafraîchir la mémoire : en-têtes HTTP et JSON · redirections et pipes
node:http : le module derrière vos serveurs
Le chapitre précédent a listé node:http parmi les modules intégrés ; voici sa forme minimale :
import http from "node:http";
const serveur = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("bonjour");
});
serveur.listen(3000, () => console.log("port 3000"));
Trois objets suffisent : le serveur (écoute un port), la requête req (ce que le client envoie) et la réponse res (ce que vous renvoyez). Le chapitre 19 construit dessus pas à pas — routage, corps POST, API complète. Ici, retenons l'essentiel : req et res sont déjà des streams.
Le problème : tout charger ou lire en flux
Lire un fichier de 2 Go avec readFile oblige Node à tenir tout le fichier en RAM avant d'en faire quoi que ce soit. Dix utilisateurs simultanés ? Vingt Go. La solution consiste à traiter les données par morceaux (chunks), au fur et à mesure :
fichier ──morceau──> traitement ──morceau──> destination
──morceau──> (mémoire constante)
Un stream est cette canalisation. Trois familles : lisibles (fs.createReadStream, req), écrivables (fs.createWriteStream, res) et duplex.
Consommer un flux : data et end
Tout flux lisible émet les mêmes événements :
import fs from "node:fs";
const lecture = fs.createReadStream("gros.log", "utf8");
let octets = 0;
lecture.on("data", (morceau) => {
octets += morceau.length;
});
lecture.on("end", () => {
console.log("fini :", octets, "caractères, sans jamais tout charger");
});
C'est exactement le mécanisme que le chapitre 19 utilisera pour lire le corps d'une requête POST : req.on("data", ...) accumule, req.on("end", ...) finalise.
Relier deux flux : pipe
Relier une source à une destination tient en un mot :
import fs from "node:fs";
import http from "node:http";
const serveur = http.createServer((req, res) => {
if (req.url === "/gros") {
res.writeHead(200, { "Content-Type": "application/octet-stream" });
fs.createReadStream("gros.log").pipe(res); // source → réponse
return;
}
res.writeHead(404);
res.end("introuvable");
});
serveur.listen(3000);
pipe s'occupe de tout : lire un morceau, l'écrire, attendre que la destination avale (la contre-pression), recommencer, puis terminer. Refaire la même chose « à la main » avec data/write demande de gérer soi-même ce rythme — c'est l'erreur classique qui fait exploser la mémoire.
Mesurer plutôt que croire
console.log(process.memoryUsage().heapUsed / 1024 / 1024, "Mo heap");
Comparez readFile sur un gros fichier puis createReadStream + pipe : la première fait grimper la consommation avec la taille du fichier, la seconde reste plate. C'est toute la justification du concept.
Exercice
- Lancez le serveur minimal ci-dessus, testez au navigateur puis au curl.
- Créez un fichier texte de quelques Mo. Comptez ses caractères avec
createReadStream+ événementsdata/end. - Servez ce fichier via
createReadStream().pipe(res)sur une route/fichier. - Prédisez puis vérifiez : que voit curl si vous appelez
res.write("a")dix mille fois avantres.end()? - En une phrase : pourquoi
readFileserait-il dangereux dans un vrai serveur de fichiers ?
Résumé
node:httpexpose un serveur ;reqest un flux lisible,resun flux écrivable.- Un stream traite les données morceau par morceau : mémoire constante quelle que soit la taille.
datareçoit chaque chunk,endsignale la fin.piperelie source et destination en gérant le rythme.- Fichiers volumineux et corps de requêtes sont les deux usages canoniques — vous y revenez dès le chapitre suivant.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 1. curl http://localhost:3000 affiche bonjour. Le navigateur montre la même chose ; le header Content-Type: text/plain évite toute interprétation HTML.
Question 2.
for i in $(seq 1 20); do cat gros.log >> gros2.log; done # grossir le fichier
let total = 0;
fs.createReadStream("gros2.log", "utf8")
.on("data", (c) => { total += c.length; })
.on("end", () => console.log(total));
Le compteur s'incrémente pendant la lecture ; le heap reste plat.
Question 3. Ajouter dans le callback du serveur :
if (req.url === "/fichier") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
fs.createReadStream("gros2.log").pipe(res);
return;
}
Question 4. curl affiche dix mille « a » d'un coup à la fin (Node bufferise ce qui part trop vite), puis termine à res.end(). La réponse est valide mais démontre l'intérêt de pipe, qui respecte le rythme du réseau.
Question 5. Un seul client demandant un gros fichier suffirait à saturer la RAM du serveur, pour tous les autres clients — alors qu'un flux sert chacun avec quelques Mo constants.