Node.js - Server users.dimi.uniud.it
Transcript
Node.js - Server users.dimi.uniud.it
Node.js Concetti fondamentali by Stefano Burigat Esecuzione asincrona Quasi tutte le funzioni in node vengono eseguite in modo asincrono (a tal punto che molte funzioni hanno una versione sincrona che contiene esplicitamente la parola chiave "sync" nel nome). Nel paradigma di node, uno dei (possibili) parametri delle funzioni asincrone è una funzione di callback che verrà chiamata al termine dell'esecuzione del codice della funzione asincrona. Esempio: funzioni timer La funzione setIntervalserve ad eseguire una funzione di callback in modo ripetuto. Il primo argomento è la funzione di callback, il secondo argomento indica ogni quanti millisecondi deve essere eseguita la funzione. Eventuali ulteriori argomenti vengono passati alla funzione di callback. setInterval(function() { console.log("callback"); }, 1000); Tenete presente che non c'è garanzia che il tempo impostato sia perfettamente rispettato. Esercizi Provate a scrivere un messaggio dopo la chiamata della funzione setInterval(). Fate in modo che il messaggio da stampare all'interno della funzione venga passato come argomento. La funzione setTimeoutè simile a setInterval ma chiama la funzione di callback una sola volta. Provatela. Le funzioni clearTimeoute clearIntervalbloccano i rispettivi tipi di timer. Entrambe hanno come unico argomento l'oggetto ritornato dalla funzione che definisce il timer. Provate ad utilizzare clearInterval per simulare setTimeout. Esempio: lettura file La funzione readFiledel modulo fsserve a leggere un file dal file system e chiamare una funzione di callback quando il file è stato letto. Il primo argomento è il file da leggere, il secondo (opzionale) la codifica, il terzo la funzione di callback (alla quale vengono passati due argomenti, eventuale errore e dati letti). var fs = require("fs"); fs.readFile("test.txt", "utf8", function(error, data) { console.log(data); }); console.log("Lettura file..."); Esercizi Provate a passare un nome di file che non esiste come primo parametro. Perchè otteniamo quell'output? Modificate il codice per stampare un messaggio nel caso in cui si sia verificato un errore. NOTA: le condizioni d'errore andrebbero sempre gestite. Eliminate il secondo parametro e verificate cosa succede. Convenzioni L'esempio precedente mostra due convenzioni importanti nella scrittura di codice node (non sono regole del linguaggio e non sono sempre rispettate). Se un metodo prevede una funzione di callback come argomento allora tale funzione viene passata per ultima. Se una funzione prevede un errore come argomento, questo viene passato per primo. Esempio: web server Il modulo httpfornisce le funzionalità necessarie alla creazione di un web server. var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'content-type': 'text/plain'}); res.end("Hello Node!"); }).listen(8124); console.log('Server creato'); Accedete al server via browser. Esempio: web server La funzione createServercrea un server http che resta in attesa di connessioni dai client sulla porta specificata tramite la funzione listen. A createServer viene passata una funzione di callback che viene chiamata nel momento in cui si verifica una connessione al server. Alla funzione di callback vengono passati due argomenti che rappresentano la richiesta di connessione e la risposta del server. All'interno della funzione di callback vengono utilizzate la funzione writeHeadper scrivere l'header del messaggio di risposta del server e la funzione endper scrivere il corpo del messaggio e segnalare che il messaggio è stato inviato (entrambe le funzioni sono obbligatorie). Esercizi Al posto di scrivere il corpo del messaggio direttamente tramite funzione end, è possibile farlo tramite funzione write sull'oggetto res. Provatela. Provate ad aggiungere una funzione di callback alla funzione listen che stampa un messaggio "Server in ascolto sulla porta 8124". Combinate la creazione di un server http con la lettura di un file (programma precedente) per tornare il contenuto del file all'utente. Informate anche il client in caso di eventuali errori di lettura. Creare un client http La funzione requestdel modulo http permette di effettuare una richiesta http ad un server. var http = require('http'); var options = { host: 'localhost', port: 8124, path: '/', method: 'GET' }; http.request(options, function(res) { console.log('risposta arrivata'); }).end(); console.log('richiesta effettuata'); Esercizio: creare una versione del client che effetta una richiesta ogni 8 secondi. Esercizio Fate in modo che il server http ritorni al client della slide precedente i primi 100 interi, utilizzando la funzione seguente: function writeNumbers(res) { counter = 0; for (var i = 0; i<100; i++) { counter++; res.write(counter.toString() + "\n"); } } Provate a far partire server e client. Perchè otteniamo quel risultato? Gestione degli eventi Node mantiene una coda degli eventi che si verificano durante l'esecuzione di un'applicazione. Ad ogni iterazione del ciclo di gestione degli eventi, un evento viene estratto e processato. Se tale evento ne genera altri, questi vengono inseriti in coda. Al termine della gestione di un evento, si passa all'evento successivo. Negli esempi che abbiamo visto finora, le funzioni di callback venivano chiamate al verificarsi di particolari eventi ma tali eventi non erano visibili (ad esempio, la funzione di callback passata a createServerviene chiamata al verificarsi dell'evento request). Event emitter In node, gli oggetti che generano eventi vengono chiamati event emitter. Creare un event emitter richiede semplicemente di importare il modulo eventse creare un'istanza dell'oggetto EventEmitter. Tale istanza può creare nuovi eventi tramite la funzione emit(). var events = require("events"); var emitter = new events.EventEmitter(); emitter.emit("nuovoEvento"); Eventi Il nome di un evento può essere una qualunque stringa valida. A volte è necessario fornire informazione aggiuntiva rispetto al solo nome dell'evento. In tal caso, è possibile passare ulteriori parametri alla funzione emit(). var events = require("events"); var emitter = new events.EventEmitter(); var dato = "datoAggiuntivo"; emitter.emit("nuovoEvento", dato); Ascoltatori Per poter gestire gli eventi emessi da un event emitter, è necessario creare degli ascoltatori (listener) tramite le funzioni on()oppure addListener()dell'emitter stesso. Entrambi i metodi prendono come parametri il nome dell'evento ed una funzione che verrà chiamata quando si verificherà l'evento. var events = require("events"); var emitter = new events.EventEmitter(); var dato = "datoAggiuntivo"; emitter.on("nuovoEvento", function(dato) { console.log("Dato passato: " + dato); }); emitter.emit("nuovoEvento", dato); Esercizi Partendo dal codice di un web server visto in precedenza, creare un ascoltatore dell'evento request, passandogli la funzione di callback attualmente inserita in createServer. Aggiungere un ascoltatore per l'evento listeningche viene emesso quando un server viene associato ad una specifica porta, stampando il messaggio "Server in ascolto sulla porta 8124" tramite tale ascoltatore. Nell'esempio del client http, creare un ascoltatore per l'evento responseche viene emesso quando viene ricevuta una risposta ad una richiesta http. La funzione once()può essere utilizzata al posto di on()per fare in modo che un evento venga gestito una sola volta. Provatela modificando l'esempio della slide "Ascoltatori". Node è single-threaded Di base, le applicazioni node possono svolgere una sola operazione alla volta. L'utilizzo degli eventi e dell'esecuzione asincrona può dare l'impressione che le applicazioni siano multithreaded ma viene sempre processato un solo evento alla volta. Esercizio: provate a inserire un ciclo infinito nel corpo della funzione di callback del web server visto in precedenza. Chiamate il server da due tab diverse del browser. Esercizio: scrivete un'applicazione con due funzioni setInterval che stampano due messaggi diversi. Eseguite l'applicazione. Inserite un ciclo infinito in una delle due funzioni. Rieseguite l'applicazione. Eventi imprevisti Prendete il codice del web server visto in precedenza ed inserite un messaggio di log sul server successivamente alla funzione end. Eseguite l'applicazione e accedete al server da browser. Come potete vedere, risulta che ci sono stati due accessi al server al posto dell'unico accesso che si attendeva. Questo perchè il browser può mandare richieste per diverse risorse. Provate a stampare un messaggio di log che mostri la proprietà url dell'oggetto req per verificare quale risorsa è stata richiesta. Bisogna sempre verificare che le richieste che arrivano siano effettivamente per i dati che ci si aspetta. Gestione delle eccezioni Le eccezioni in un ambiente JavaScript sincrono vengono gestite normalmente tramite istruzioni trye catch. Il codice seguente in node non funziona: var fs = require("fs"); try { fs.readFile("", "utf8", function(error, data) { if (error) { throw error; } console.log(data); }); } catch (exception) { console.log("Eccezione catturata!") } Gestione delle eccezioni Nell'esempio precedente, quando l'eccezione viene lanciata non siamo più dentro il blocco try/catch. Ci sono diversi modi per gestire queste situazioni in node: Controllare sempre il parametro error delle funzioni e gestirlo in modo appropriato. Utilizzare un gestore globale delle eccezioni che cattura tutto ciò che "scappa". process.on("uncaughtException", function(error) { console.log("Eccezione catturata!") }); Utilizzare il concetto di "dominio" tramite il modulo domain. Domini I domini sono il meccanismo preferito per la gestione delle eccezioni. Quando un timer, un event emitter o una funzione di callback registrati con un dominio creano un errore, questo viene notificato al dominio e può essere gestito. La funzione run()può essere utilizzata per eseguire una funzione e registrare implicitamente con il dominio qualunque timer, event emitter o callback presente all'interno della funzione. Domini var fs = require("fs"); var domain = require("domain").create(); domain.run(function() { fs.readFile("", "utf8", function(error, data) { if (error) { throw error; } console.log(data); domain.dispose(); }); }); domain.on("error", function(error) { console.log("Eccezione catturata!") }); Domini Al posto della funzione run()è possibile utilizzare le funzioni bind()e intercept()che accettano come parametro una funzione di callback che si vuole associare al dominio. Al contrario di run(), la funzione passata a bind()o intercept()non viene eseguita immediatamente. Esercizio: provate ad utilizzare bind()per racchiudere la funzione di callback che viene passata a readFile nell'esempio precedente. Esercizio: provate ad utilizzare intercept()al posto di bind(). In tal caso, non è necessario inserire il parametro error nella funzione di callback, nè gestire tale parametro all'interno del corpo della funzione. Callback hell Un problema che si può verificare nella scrittura di codice node è il cosiddetto "callback hell" cioè il fatto che si possano avere diversi livelli di callback innestate. Il callback hell porta a codice difficile da leggere e mantenere. L'esempio di lettura di file visto in precedenza va in crash se il file da leggere non esiste. Si può ovviare al problema controllando che il file esista e che sia effettivamente un file, sfruttando le funzioni fs.exists()e fs.stat(), come nell'esempio seguente. Callback hell var fs = require("fs"); var fileName = "test.txt"; fs.exists(fileName, function(exists) { if (exists) { fs.stat(fileName, function(error, stats) { if (error) { throw error; } if (stats.isFile()) { fs.readFile(fileName, "utf8", function(error, data) { if (error) { throw error; } console.log(data); }); } }); } }); Callback hell Un modo per risolvere il problema del callback hell (a spese della comprensione del flusso del controllo) è quello di utilizzare funzioni con nome al posto di funzioni anonime. Ad esempio, si può chiamare la funzione exists() nel modo seguente: function cbExists(exists) { if (exists) { fs.stat(fileName, cbStat); } } fs.exists(fileName, cbExists); Provate a completare il resto del codice implementando le due funzioni mancanti (in base al codice della slide precedente).