Architettura di NodeJS I: Un'introduzione generale
Fin dall’inizio degli anni 2020, i progressi dell'Intelligenza Artificiale e il modo in cui gli sviluppatori lavorano sono cambiati con grande frequenza. Oggi si parla molto di programmazione in linguaggio naturale, senza toccare direttamente le linee di codice come si faceva in passato. Questo rappresenta un grande cambiamento nel modo in cui guardiamo i computer, ma cosa è effettivamente cambiato nel modo in cui i computer guardano noi?
Per un processore, programmare in Assembly, Python o in linguaggio naturale sono soltanto astrazioni per gli zeri e gli uno che verranno interpretati dalle sue porte logiche. Il grande valore delle astrazioni si distingue quando un essere umano deve interpretare una determinata istruzione scritta nel codice o creare una nuova funzionalità, permettendogli così di lavorare in modo più rapido ed efficace, mentre i motori del linguaggio utilizzato si occupano del lavoro pesante che avviene negli strati più vicini all’hardware della macchina.
Tra i diversi linguaggi di programmazione, JavaScript è di sicuro tra i più popolari. Un linguaggio che inizialmente veniva interpretato soltanto dai browser, ma che oggi possiede superpoteri ed è presente in quasi tutti i tipi di dispositivi. Con lo scopo di portare le sue funzionalità allo sviluppo backend, nel 2009 nasce NodeJS, un nuovo runtime capace di interpretare il codice JavaScript sul server, che da allora ha consolidato un numero enorme di sostenitori attorno alla sua comunità.
Con quasi due decenni di storia, molte cose sono evolute nell'architettura di NodeJS, dando origine a un ecosistema robusto per applicazioni di grandi dimensioni. Nonostante la sua importanza nello sviluppo del software moderno, molti sviluppatori che utilizzano questo strumento non hanno mai dedicato del tempo a capire come funzioni questo enorme ingranaggio, ed è esattamente su questo punto che ci addentreremo in questa serie di articoli.
LE SFIDE DEL SERVER-SIDE
Quando passiamo dalle applicazioni web alle applicazioni eseguite sui server, praticamente tutte le funzionalità devono essere ripensate:
- Compilazione: non abbiamo più un browser ad aiutarci, ora dobbiamo fare in modo che il linguaggio generi codice macchina nativo per i processori;
- Operazioni di I/O: come effettueremo la lettura o la scrittura su rete o su disco, il traffico dati, i timer, ecc.;
- Utilizzo della memoria: come manipoleremo i dati, come verranno archiviati e come gestiremo le diverse strutture dati native del linguaggio di programmazione;
- Concorrenza e parallelismo: come eseguire le istruzioni su più thread o dividere la capacità di elaborazione tra le diverse operazioni del sistema operativo o dell'applicazione stessa.
I COMPONENTI DELL’INGRANAGGIO
Per gestire tutti questi punti, NodeJS utilizza diversi componenti integrati e risorse del sistema operativo stesso.
Inizialmente, JavaScript era soltanto un linguaggio interpretato e privo di ottimizzazioni, il che lo rendeva lento.Tuttavia, nel 2008, insieme alla prima release di Chrome, Google ha rilasciato V8, un motore che cercava di risolvere il problema di lentezza delle sue complesse applicazioni web, come Gmail. Questo meccanismo ha influenzato la struttura dei browser, passando da un’esecuzione “riga per riga” a una struttura di compilazione in codice nativo in tempo reale, processo chiamato JIT (Just-in-Time) compilation. Questa fase avviene attraverso diversi passaggi, come il Parsing per una rappresentazione ad albero della struttura del codice chiamata AST (Abstract Syntax Tree), l’interpretazione di questa struttura effettuata da Ignition e la conversione finale eseguita da TurboFan.
Nonostante V8 non sia stato creato per NodeJS, è diventato uno dei suoi componenti principali. Tramite esso, possiamo eseguire il codice che operava nel browser direttamente sul server. Ma questo non è l'unica risorsa necessaria, abbiamo bisogno anche delle operazioni di I/O.
Quando sviluppiamo applicazioni server-side, gestiamo chiamate al sistema operativo, come la lettura di file, la comunicazione di rete, i timer, ecc. Queste operazioni sono note come operazioni di I/O. Diversi linguaggi riescono a stabilire una comunicazione diretta con il kernel, tuttavia NodeJS esegue il codice JavaScript attraverso V8 in un ambiente isolato, ovvero non è in grado di utilizzare direttamente le risorse della macchina.
Immagina di stare trasportando una scatola: tu sei il sistema operativo che trasporta le informazioni e controlla l'hardware. La scatola che porti è il runtime di NodeJS e, al suo interno, viene eseguito il codice JavaScript. In questo modo, il codice è completamente isolato e, per accedere a qualsiasi risorsa esterna, sarebbe necessario che qualcuno "aprisse la scatola”, ovvero aiutasse la comunicazione con il kernel. Da questa necessità nasce Libuv, una libreria scritta in C che permette la comunicazione con le operazioni di I/O, la gestione dei thread e l’elaborazione asincrona.

Tuttavia, poiché Libuv è scritta in C, per arrivare a NodeJS abbiamo bisogno di qualcosa in grado di integrarla con JavaScript. Nell'ecosistema NodeJS, il “collante” tra ciò che accade all'interno e all’esterno della scatola sono i Binding.
In generale, abbiamo due grandi componenti completamente differenti e indipendenti che, fino a quel momento, operano in modo del tutto separato senza sapere cosa stia succedendo nell’altro. Quando la nostra applicazione si avvia, V8 viene preparato attraverso un processo di bootstrapping. In questo momento, NodeJS utilizza l’API di V8 per registrare puntatori di memoria che collegano le funzioni JavaScript alle funzioni native in C++. La funzione internalBinding di NodeJS è responsabile del mantenimento di questa mappatura. Quindi, quando programmiamo la lettura di un file, i Binding sanno indirizzarci verso la funzione di Libuv che, di fatto, svolgerà il lavoro pesante.

In questo modo, abbiamo una piattaforma completa per interpretare JavaScript e permettere che, attraverso di esso, gestiamo le chiamate al sistema operativo.
Tutta l'orchestrazione di questo flusso, tuttavia, non è così lineare quanto sembra. Questo processo dipende da meccanismi come l’Event Loop, le Event Queue, ecc., argomenti che affronteremo nei prossimi post.