Explorer les internes de Node.js
Depuis l'introduction de Node.js par Ryan Dahl au JSConf européen le 8 novembre 2009, il a été largement utilisé dans l'industrie technologique. Des entreprises telles que Netflix, Uber et LinkedIn donnent de la crédibilité à l'affirmation selon laquelle Node.js peut supporter un volume élevé de trafic et de simultanéité. C'est juste un runtime! " "Il a des boucles d'événements!" "Node.js est un thread unique comme JavaScript!"
Bien que certaines de ces affirmations soient vraies, nous allons approfondir le runtime Node.js, comprendre comment il exécute JavaScript, voir s'il est réellement un thread unique, et , enfin, une meilleure compréhension de l'interconnexion entre ses dépendances principales, V8 et libuv.
Prérequis
- Connaissances de base de JavaScript
- Familiarité avec la sémantique de Node.js (
nécessite
fs
)
Qu'est-ce que Node.js?
Il peut être tentant de supposer ce que beaucoup de gens pensent de Node.js, la définition la plus courante étant qu'il s'agit d'un runtime pour le langage JavaScript . Pour considérer cela, nous devons comprendre ce qui a conduit à cette conclusion.
Node.js est souvent décrit comme une combinaison de C ++ et de JavaScript. La partie C ++ est constituée de liaisons exécutant du code de bas niveau qui permettent d'accéder au matériel connecté à l'ordinateur. La partie JavaScript prend JavaScript comme code source et l'exécute dans un interpréteur populaire du langage, appelé moteur V8 .
Avec cette compréhension, nous pourrions décrire Node.js comme un outil unique qui combine JavaScript et C ++ pour exécuter des programmes en dehors de l'environnement du navigateur.
Mais pourrions-nous réellement appeler cela un runtime? Pour le déterminer, définissons ce qu'est un runtime.
Qu'est-ce qu'un runtime? https://t.co/eaF4CoWecX
– Christian Nwamba (@codebeast) 5 mars 2020
Dans l'un de ses réponses sur StackOverflow, DJNA définit un environnement d'exécution comme «tout ce dont vous avez besoin pour exécuter un programme, mais aucun outil pour le changer». Selon cette définition, nous pouvons affirmer avec certitude que tout ce qui se passe pendant que nous exécutons notre code (dans n'importe quel langage) s'exécute dans un environnement d'exécution.
D'autres langues ont leur propre environnement d'exécution. Pour Java, il s'agit de l'environnement d'exécution Java (JRE). Pour .NET, il s'agit du Common Language Runtime (CLR). Pour Erlang, c'est BEAM.
Néanmoins, certains de ces runtimes ont d'autres langages qui en dépendent. Par exemple, Java a Kotlin, un langage de programmation qui se compile en code qu'un JRE peut comprendre. Erlang a Elixir. Et nous savons qu'il existe de nombreuses variantes pour le développement .NET, qui s'exécutent toutes dans le CLR, connu sous le nom de .NET Framework.
Nous comprenons maintenant qu'un runtime est un environnement fourni pour qu'un programme puisse s'exécuter avec succès, et nous savons que V8 et une multitude de bibliothèques C ++ permettent à une application Node.js de s'exécuter. Node.js lui-même est le véritable runtime qui lie tout ensemble pour faire de ces bibliothèques une entité, et il ne comprend qu'un seul langage – JavaScript – indépendamment de ce avec quoi Node.js est construit.
Structure interne de Node.js
Lorsque nous tentons d'exécuter un programme Node.js (tel que index.js
) à partir de notre ligne de commande à l'aide de la commande node index.js
nous appelons le runtime Node.js. Ce runtime, comme mentionné, se compose de deux dépendances indépendantes, V8 et libuv.

V8 est un projet créé et maintenu par Google. Il prend le code source JavaScript et l'exécute en dehors de l'environnement du navigateur. Lorsque nous exécutons un programme via une commande de nœud
le code source est transmis par le runtime Node.js à V8 pour exécution.
La bibliothèque libuv contient du code C ++ qui permet un accès de bas niveau au fonctionnement système. Les fonctionnalités telles que la mise en réseau, l'écriture dans le système de fichiers et la concurrence ne sont pas livrées par défaut dans V8, qui est la partie de Node.js qui exécute notre code JavaScript. Avec son ensemble de bibliothèques, libuv fournit ces utilitaires et bien plus dans un environnement Node.js.
Node.js est la colle qui maintient les deux bibliothèques ensemble, devenant ainsi une solution unique. Tout au long de l'exécution d'un script, Node.js comprend à quel projet passer le contrôle et quand.
API intéressantes pour les programmes côté serveur
Si nous étudions un peu l'histoire de JavaScript, nous saurions qu'il est destiné à ajouter certaines fonctionnalités et interaction avec une page du navigateur. Et dans le navigateur, nous interagirions avec les éléments du modèle objet de document (DOM) qui composent la page. Pour cela, un ensemble d'API existe, appelé collectivement l'API DOM.
Le DOM n'existe que dans le navigateur; c'est ce qui est analysé pour rendre une page, et il est essentiellement écrit dans le langage de balisage appelé HTML. De plus, le navigateur existe dans une fenêtre, d'où l'objet window
qui sert de racine à tous les objets de la page dans un contexte JavaScript. Cet environnement est appelé environnement de navigateur et c'est un environnement d'exécution pour JavaScript.

fs
chemin
tampon
événements
HTTP
etc.) ), comme nous les avons, n'existent que pour Node.js, et ils sont fournis par Node.js (lui-même un runtime) afin que nous puissions exécuter des programmes écrits pour Node.js. Experiment: How fs. writeFile
crée un nouveau fichier
Si V8 a été créé pour exécuter JavaScript en dehors du navigateur, et si un environnement Node.js n'a pas le même contexte ou environnement qu'un navigateur, alors comment ferions-nous quelque chose comme l'accès le système de fichiers ou créer un serveur HTTP?
Prenons par exemple une application Node.js simple qui écrit un fichier dans le système de fichiers dans le répertoire courant:
const fs = require ("fs")
fs.writeFile ("./ test.txt", "text");
Comme indiqué, nous essayons d'écrire un nouveau fichier dans le système de fichiers. Cette fonctionnalité n'est pas disponible dans le langage JavaScript; il est disponible uniquement dans un environnement Node.js. Comment cela s'exécute-t-il?
Pour comprendre cela, faisons un tour de la base de code Node.js.
En se dirigeant vers le référentiel GitHub pour Node.js nous voyons deux dossiers principaux , src
et lib
. Le dossier lib
contient le code JavaScript qui fournit le bel ensemble de modules inclus par défaut avec chaque installation de Node.js. Le dossier src
contient les bibliothèques C ++ pour libuv.
Si nous regardons dans le dossier src
et parcourons le fichier fs.js
nous verrons qu'il est plein de code JavaScript impressionnant. Sur la ligne 1880 nous remarquerons une déclaration d'exportation
. Cette instruction exporte tout ce à quoi nous pouvons accéder en important le module fs
et nous pouvons voir qu'elle exporte une fonction nommée writeFile
.
Recherche de function writeFile (
] (où la fonction est définie) nous conduit à ligne 1303 où nous voyons que la fonction est définie avec quatre paramètres:
fonction writeFile (chemin, données, options, rappel) {
callback = peut-êtreCallback (options de rappel ||);
options = getOptions (options, {encodage: 'utf8', mode: 0o666, indicateur: 'w'});
const flag = options.flag || «w»;
if (! isArrayBufferView (data)) {
validateStringAfterArrayBufferView (données, «données»);
data = Buffer.from (data, options.encoding || 'utf8');
}
if (isFd (chemin)) {
const isUserFd = true;
writeAll (chemin, isUserFd, données, 0, data.byteLength, rappel);
revenir;
}
fs.open (chemin, indicateur, options.mode, (openErr, fd) => {
if (openErr) {
rappel (openErr);
} autre {
const isUserFd = false;
writeAll (fd, isUserFd, data, 0, data.byteLength, callback);
}
});
}
Sur les lignes 1315 et 1324 nous voyons qu'une seule fonction, writeAll
est appelée après quelques vérifications de validation. Nous trouvons cette fonction sur ligne 1278 dans le même fichier fs.js
.
fonction writeAll (fd, isUserFd, buffer, offset, length, callback) {
// écriture (fd, tampon, décalage, longueur, position, rappel)
fs.write (fd, buffer, offset, length, null, (writeErr, écrite) => {
if (writeErr) {
if (isUserFd) {
rappel (writeErr);
} autre {
fs.close (fd, fonction close () {
rappel (writeErr);
});
}
} else if (écrit === longueur) {
if (isUserFd) {
rappel (null);
} autre {
fs.close (fd, rappel);
}
} autre {
offset + = écrit;
longueur - = écrit;
writeAll (fd, isUserFd, tampon, décalage, longueur, rappel);
}
});
}
Il est également intéressant de noter que ce module tente de s'appeler. Nous voyons cela sur ligne 1280 où il appelle fs.write
. À la recherche de la fonction écriture
nous découvrirons quelques informations.
La fonction écriture
commence sur ligne 571 et s'exécute sur 42 lignes . Nous voyons un motif récurrent dans cette fonction: la façon dont elle appelle une fonction sur le module de liaison
comme on le voit sur les lignes 594 et 612 . Une fonction du module de liaison
est appelée non seulement dans cette fonction, mais dans pratiquement toutes les fonctions exportées dans le fichier fs.js
.
La variable de liaison
est déclarée sur ligne 58 tout en haut du fichier, et un clic sur cet appel de fonction révèle des informations , avec l'aide de GitHub.

Cette fonction internalBinding
se trouve dans le module nommés chargeurs . La fonction principale du module chargeurs est de charger toutes les bibliothèques libuv et de les connecter via le projet V8 avec Node.js. La façon dont cela fonctionne est plutôt magique, mais pour en savoir plus, nous pouvons regarder de près la fonction writeBuffer
qui est appelée par le module fs
.
Nous devons regarder où cela se connecte avec libuv, et où V8 entre en jeu. En haut du module des chargeurs, une bonne documentation y indique ceci:
// Ce fichier est compilé et exécuté par node.cc avant bootstrap / node.js
// a été appelé, donc les chargeurs sont bootstrapés avant de commencer
// en fait bootstrap Node.js. Il crée les objets suivants:
//
// Chargeurs de liaison C ++:
// - process.binding (): le chargeur de liaison C ++ hérité, accessible à partir de l'espace utilisateur
// car il s'agit d'un objet attaché à l'objet de processus global.
// Ces liaisons C ++ sont créées à l'aide de NODE_BUILTIN_MODULE_CONTEXT_AWARE ()
// et ont leurs nm_flags définis sur NM_F_BUILTIN. Nous ne faisons aucune garantie
// à propos de la stabilité de ces fixations, mais il faut quand même faire attention
// problèmes de compatibilité causés par eux de temps en temps.
// - process._linkedBinding (): destiné à être utilisé par les plongeurs pour ajouter
// liaisons C ++ supplémentaires dans leurs applications. Ces liaisons C ++
// peut être créé en utilisant NODE_MODULE_CONTEXT_AWARE_CPP () avec l'indicateur
// NM_F_LINKED.
// - internalBinding (): le chargeur de liaison C ++ interne privé, inaccessible
// depuis le pays de l'utilisateur, sauf via `require ('internal / test / binding')`.
// Ces liaisons C ++ sont créées à l'aide de NODE_MODULE_CONTEXT_AWARE_INTERNAL ()
// et leurs nm_flags définis sur NM_F_INTERNAL.
//
// Chargeur de module JavaScript interne:
// - NativeModule: un système de modules minimal utilisé pour charger le noyau JavaScript
// modules trouvés dans lib / ** / *. js et deps / ** / *. js. Tous les modules de base sont
// compilé dans le nœud binaire via node_javascript.cc généré par js2c.py,
// afin qu'ils puissent être chargés plus rapidement sans coût d'E / S. Cette classe fait
// lib / internal / *, modules deps / internal / * et internalBinding () disponibles par
// par défaut sur les modules de base, et laisse les modules de base se requérir via
// require ('internal / bootstrap / loaders') même lorsque ce fichier n'est pas écrit
// Style CommonJS.
Ce que nous apprenons ici, c'est que pour chaque module appelé depuis l'objet de liaison
dans la section JavaScript du projet Node.js, il en existe un équivalent dans la section C ++, dans le dossier src
.
De notre tournée fs
nous voyons que le module qui fait cela se trouve dans node_file.cc
. Chaque fonction accessible via le module est définie dans le fichier; par exemple, nous avons le writeBuffer
sur ligne 2258 . La définition réelle de cette méthode dans le fichier C ++ se trouve sur la ligne 1785 . De plus, l'appel à la partie de libuv qui écrit réellement le fichier se trouve sur les lignes 1809 et 1815 où la fonction libuv uv_fs_write
est appelé de façon asynchrone.
Que retirons-nous de cette compréhension?
Tout comme de nombreux autres runtimes de langage interprétés, le runtime de Node.js peut être piraté. Avec une meilleure compréhension, nous pourrions faire des choses qui sont impossibles avec la distribution standard simplement en regardant à travers la source. Nous pourrions ajouter des bibliothèques pour modifier la façon dont certaines fonctions sont appelées. Mais surtout, cette compréhension est le fondement d'une exploration plus approfondie.
Node.js est-il unitaire?
Assis sur libuv et V8, Node.js a accès à certaines fonctionnalités supplémentaires qu'un moteur JavaScript typique fonctionnant dans le
Tout JavaScript exécuté dans un navigateur s'exécutera dans un seul thread. Un thread dans l'exécution d'un programme est comme une boîte noire située au-dessus du CPU dans lequel le programme est exécuté. Dans un contexte Node.js, du code peut être exécuté dans autant de threads que nos machines peuvent en transporter.
Pour vérifier cette revendication particulière, explorons un simple extrait de code.
const fs = require ("fs") ;
// Un petit benchmarking
const startTime = Date.now ()
fs.writeFile ("./ test.txt", "test", (err) => {
Si (erreur) {
console.log (err)
}
console.log ("1 Terminé:", Date.now () - startTime)
});
Dans l'extrait ci-dessus, nous essayons de créer un nouveau fichier sur le disque dans le répertoire actuel. Pour voir combien de temps cela pourrait prendre, nous avons ajouté une petite référence pour surveiller l'heure de début du script, ce qui nous donne la durée en millisecondes du script qui crée le fichier.
Si nous exécutons le code ci-dessus, nous obtiendrons un résultat comme celui-ci:

$ node ./test.js
-> 1 Terminé: 0,003 s
C'est très impressionnant: seulement 0,003 seconde.
Mais faisons quelque chose de vraiment intéressant. Commençons par dupliquer le code qui génère le nouveau fichier et mettons à jour le nombre dans l'instruction log pour refléter leurs positions:
const fs = require ("fs");
// Un petit benchmarking
const startTime = Date.now ()
fs.writeFile ("./ test1.txt", "test", fonction (err) {
si (err) {
console.log (err)
}
console.log ("1 Terminé:% ss", (Date.now () - startTime) / 1000)
});
fs.writeFile ("./ test2.txt", "test", fonction (err) {
si (err) {
console.log (err)
}
console.log ("2 Terminé:% ss", (Date.now () - startTime) / 1000)
});
fs.writeFile ("./ test3.txt", "test", fonction (err) {
si (err) {
console.log (err)
}
console.log ("3 Terminé:% ss", (Date.now () - startTime) / 1000)
});
fs.writeFile ("./ test4.txt", "test", fonction (err) {
si (err) {
console.log (err)
}
console.log ("4 Terminé:% ss", (Date.now () - startTime) / 1000)
});
Si nous tentons d'exécuter ce code, nous obtiendrons quelque chose qui nous épatera. Voici mon résultat:

Tout d'abord, nous remarquerons que les résultats ne sont pas cohérents . Deuxièmement, nous constatons que le temps a augmenté.
Les tâches de bas niveau sont déléguées
Node.js est monothread, comme nous le savons maintenant. Des parties de Node.js sont écrites en JavaScript et d'autres en C ++. Node.js utilise les mêmes concepts de boucle d'événements et de pile d'appels que nous connaissons dans l'environnement du navigateur, ce qui signifie que les parties JavaScript de Node.js sont monothread. Mais la tâche de bas niveau qui nécessite de parler avec un système d'exploitation n'est pas monothread.

Lorsqu'un appel est reconnu par Node.js comme étant destiné à libuv, il délègue cette tâche à libuv. Dans son fonctionnement, libuv requiert des threads pour certaines de ses bibliothèques, d'où l'utilisation du pool de threads dans l'exécution des programmes Node.js quand ils sont nécessaires.
Par défaut, le pool de threads Node.js fourni par libuv a quatre threads dans il. Nous pourrions augmenter ou réduire ce pool de threads en appelant process.env.UV_THREADPOOL_SIZE
en haut de notre script.
// script.js
process.env.UV_THREADPOOL_SIZE = 6;
//…
//…
Que se passe-t-il avec notre programme de création de fichiers
Il semble qu'une fois que nous invoquons le code pour créer notre fichier, Node.js frappe la partie libuv de son code, qui consacre un thread à cette tâche. Cette section de libuv obtient des informations statistiques sur le disque avant de travailler sur le fichier.
Cette vérification statistique peut prendre un certain temps; par conséquent, le thread est libéré pour certaines autres tâches jusqu'à ce que la vérification statistique soit terminée. Une fois la vérification terminée, la section libuv occupe tout thread disponible ou attend qu'un thread devienne disponible.
Nous n'avons que quatre appels et quatre threads, il y a donc suffisamment de threads pour faire le tour. La seule question est de savoir à quelle vitesse chaque thread va traiter sa tâche. Nous remarquerons que le premier code à entrer dans le pool de threads retournera d'abord son résultat, et il bloque tous les autres threads lors de l'exécution de son code.
Conclusion
Nous comprenons maintenant ce qu'est Node.js. Nous savons que c'est un runtime. Nous avons défini ce qu'est un runtime. Et nous avons approfondi ce qui compose le runtime fourni par Node.js.
Nous avons parcouru un long chemin. Et à partir de notre petite visite du référentiel Node.js sur GitHub nous pouvons explorer toutes les API qui pourraient nous intéresser, en suivant le même processus que nous avons suivi ici. Node.js est open source, donc nous pouvons sûrement plonger dans la source, n'est-ce pas?
Même si nous avons abordé plusieurs des bas niveaux de ce qui se passe dans le runtime Node.js, nous ne devons pas supposer que nous savons tout. Les ressources ci-dessous indiquent quelques informations sur lesquelles nous pouvons développer nos connaissances:
- Introduction à Node.js
En tant que site officiel, Node.dev explique ce qu'est Node.js, ainsi que ses gestionnaires de packages et répertorie - « JavaScript & Node.js », The Node Beginner Book
Ce livre de Manuel Kiessling fait un travail fantastique. d'expliquer Node.js, après avoir averti que JavaScript dans le navigateur n'est pas le même que celui de Node.js, même si les deux sont écrits dans le même langage. - Beginning Node.js
Ce livre pour débutant va au-delà d'une explication de l'exécution. Il enseigne les packages et les flux et la création d'un serveur Web avec le framework Express. - LibUV
Il s'agit de la documentation officielle du code C ++ de prise en charge du runtime Node.js. - V8
Il s'agit de la documentation officielle du moteur JavaScript qui permet d'écrire Node.js avec JavaScript.

Source link