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).