Créer un backend en Javascript (partie 6) : Comment fonctionne NodeJS sous le capot ?

ericlecodeur

Eric Le Codeur

Posted on October 13, 2021

Créer un backend en Javascript (partie 6) : Comment fonctionne NodeJS sous le capot ?

Voici une série d'articles qui vous permettra créer des applications backend en Javascript.

Node.js est aujourd'hui un incontournable, il est donc essentiel pour un développeur de le maitriser.

Je vais donc publier un nouvel article environ au deux jours et petit à petit vous apprendrez tout ce qu'il y a à savoir sur Node.js

Pour ne rien manquer suivez moi sur twitter : https://twitter.com/EricLeCodeur


Comment fonctionne NodeJS sous le capot ?

Dans cette section nous allons faire un peu de théorie et découvrir comment NodejS exécute son code Javascript.

Comme vous le savez, NodeJS permet d'exécuter du code asynchrone. Ce concept peu semblé simple mais en arrière plan c'est un peu plus compliqué. Qu'est-ce qui détermine quel code est exécuté ? Qu'est-ce qui détermine l'ordre d'exécution ?

Comprendre ces concepts est essentiel pour développer avec NodeJS. Pas besoin de devenir un expert sur le sujet mais au moins comprendre la base.

À noter que certain concepts ont été simplifié afin de permettre de mieux les expliquer.

L'architecture de NodeJS

NodeJS est composé de deux parties principales l'engin V8 et la librairie libuv

Alt Text

L'engin V8

S'occupe de convertir le code Javascript en code machine. Une fois le code convertie en code machine l'exécution sera géré par la librairie libuv

libuv

Est une librairie open-source, écrite en c++ qui se spécialise dans l'exécution asynchrone i/o (ex. File system, Networking et plus)

libuv implémente deux features très important de NodeJS soit le Event Loop et le Thread Pool

Un point important à comprendre c'est que NodeJS fonctionne en mode single thread.

C'est à dire qu'il peux exécuter seulement une tâche à la fois. Si une tâche demande trop de temps/ressource alors elle va bloquer/empêcher les autres tâches de s'exécuter.

Imaginez, par exemple, si il y avait 100 000 usagers sur le site en même temps qui demandait l'accès à la base de donnée, le temps de réponse deviendrait vite inacceptable. C'est pourquoi NodeJS a besoin d'une gestion efficace de l'exécution du code asynchrone.... Ça c'est le travail du Event Loop

Le Event Loop permet de gérer le code asynchrone comme les callbacks, les promesses et requêtes network qui demande peu de resource. Et quand une tâche est trop longue à exécuter, afin de ne pas bloquer le thread, le Event Loop va déléguer ce travail au Thread Pool.

Le Thread Pool peut quand a lui exécuter des tâches en parallèle et s'occupe donc des tâches plus lourdes comme l'accès au file system et les processus très demandant comme par exemple les conversions de vidéo ou la cryptographie.

Ordre d'exécution d'une application NodeJS

Lorsque l'on exécute une application NodeJS, le code d'initialization, les requires et le code "top level" sont exécuté immédiatement un après l'autre.

Les callbacks rencontré dans notre code ne sont pas exécuté immédiatement car potentiellement bloquant, il bloquerait l'application aux autres tâches et aux autres utilisateurs. Ces callbacks sont donc enregistré auprès du Event Loop

Une fois le code "top level" exécuté, NodeJS donnera la main au Event Loop afin qu'il puisse exécuter les tâches qu'il contient.

Le Event Loop décide, selon des critères pré-définit, quel ordre d'exécution devra être respecté. Le Event Loop peut également décidé de déléguer une tâche vraiment longue au Thread Pool. (ex. accès au file system).

Le Thread Pool lui peut exécuter plusieurs tâche en même temps (multi-thread) et retournera le résultat au Event Loop

Tant et aussi longtemps qu'il y a des tâches à exécuter, le Event Loop va garder l'application active.

Une fois toutes les tâches du Event Loop terminé, le contrôle est re-donné au Thread principal de votre application qui terminera le programme.

NodeJS en exemple

C'est bien beau la théorie mais revoyons le tout cette fois ci avec un exemple concret

const fs = require('fs')

console.log('Début de la première tâche')

fs.readFile('./data/products.json', 'utf8', (err, data) => {
    console.log(data)
    console.log('Première tâche terminé')
})

console.log('Début de la deuxième tâche')
Enter fullscreen mode Exit fullscreen mode

Résultat

Début de la première tâche
Début de la deuxième tâche
{
     "name": "iPhone 12",
     "price": 900
}


Première tâche terminé
Enter fullscreen mode Exit fullscreen mode

Basé sur la logique expliquer plus tôt, NodeJS exécutera le code dans l'ordre suivant :

→ const fs = require(fs)

→ console.log('Début de la première tâche')

→ enregistrement du callback readFile avec le Event Loop

→ console.log('Début de la deuxième tâche')

→ Tâches de haut niveau terminé la main est donc passé au Event Loop

 → readFile callback → Déléguer au Thread Pool

 → Quand le readFile est terminé

     → console.log(data) 

     → console.log('Première tâche terminé')

  → Si aucune autre tâche en attente alors termine le Event Loop
Enter fullscreen mode Exit fullscreen mode

→ Fin du programme

Exemple avec SetTimeout zéro

console.log('Premier')

setTimeout(() => {
    console.log('Deuxième')
}, 0)

console.log('Troisième')
Enter fullscreen mode Exit fullscreen mode

Résultat

Premier
Troisième
Deuxième
Enter fullscreen mode Exit fullscreen mode

Ici on aurait pu penser qu'avec un setTimeOut de 0 il serait exécuté immédiatement ? Mais non, comme vu précédemment, NodeJS envoi les callback au Event Loop et exécute le code top level en premier.

Basé sur cette logique, le NodeJS exécutera le code dans l'ordre suivant :

→ console.log('Premier')

→ register setTimeout callback avec le Event Loop

→ console.log('Troisième')

→ Passe la main au Event Loop

→ callback setTimeout 

    → console.log('Deuxième')

→ Si pas d'autre tache alors termine le Event Loop
Enter fullscreen mode Exit fullscreen mode

→ Fin du programme

Exemple serveur

const http = require('http')

const server = http.createServer((req, res) => {
    if (req.url === '/') {
        res.end('<h1>Home page</h1>')
    } else if (req.url === '/about') {
        res.end('<h1>About page</h1>')

        let i = 0
        do {
            i++
        } while (i < 10000000000)

    } else {
        res.end('page not found')
    }    
})

server.listen(5000, 'localhost', () => {
    console.log('Server is listening at localhost on port 5000')
})
Enter fullscreen mode Exit fullscreen mode

Il y a deux enseignement à retirer de cet exemple. Premièrement, l'application NodeJS ne va jamais s'arrêter. Le Event Loop est sans fin puisqu'il attend les events du serveur. La fonction 'listen' garde le Event Loop actif.

Enfin, lorsque un usager va visiter la page about, Node va exécuter le 'do while' et comme ce n'est pas du code asynchrone l'accès au site web sera temporairement bloqué pour tous les usagers jusqu'a temps que le do while se termine. Cela est un bon exemple du fait que NodeJS est single thread et que il faut faire attention comment vous codez votre application.

Par exemple, dans ce cas ci, il serait préférable de placer le do while a l'intérieur d'une fonction async afin de ne pas bloquer le thread.

Conclusion

C'est tout pour aujourd'hui, suivez moi sur twitter : https://twitter.com/EricLeCodeur afin d'être avisé de la parution du prochain article (d'ici deux jours).

💖 💪 🙅 🚩
ericlecodeur
Eric Le Codeur

Posted on October 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related