In questa lezione vedremo come creare un semplice server HTTP usando il modulo omonimo in Node.js. Realizzeremo poi un server HTTPS usando un certificato da noi generato col comando openssl. Illustreremo, inoltre, come sia possibile inviare al client un file JSON e utilizzare gli stream per migliorare le prestazioni del server. Infine vedremo come mostrare pagine differenti al variare dell’URL.
Un primo server HTTP e HTTPS
Per prima cosa implementiamo un semplice server HTTP che si limiti a mostrare nel browser del semplice testo. Per questo primo esempio creiamo quindi un file http_server.js in cui inseriamo il seguente frammento di codice.
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Esempio server HTTPn');
});
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo http://${address}:${port}
`);
}
server
.listen(
8000,
'127.0.0.1',
callback
)
Dopo aver importato il modulo http, usiamo la funzione http.createServer() per creare un’istanza di http.Server. Si tratta di un EventEmitter che deriva da net.Server (nella precedente lezione, se ricordate, abbiamo creato un server TCP usando il modulo net). Usiamo quindi la funzione httpServer.listen() per avviare il server all’indirizzo e porta specificati come argomenti. Una volta avviato il server, verrà stampato un messaggio nel terminale per confermare che il server funziona correttamente. Al metodo http.createServer() passiamo come argomento una funzione che a sua volta riceve come argomenti req (request) e res (response). Request è un oggetto di tipo http.IncomingMessage. Si tratta di un Readable Stream che come abbiamo visto implementa la ‘classe’ EventEmitter. Response è invece un oggetto http.ServerResponse (Writable Stream) che utilizziamo per inviare dei dati al client. Facciamo infine uso del metodo response.writeHead() per indicare che il contenuto inviato al client, con il metodo response.end(), è del semplice testo.
Un server HTTPS
In maniera analoga possiamo creare un server HTTPS. L’unica differenza consiste nella necessità di usare una chiave privata e un certificato SSL/TLS. Dal momento che si tratta soltanto di un esempio, possiamo generarli noi grazie al comando openssl.
openssl req
-x509 -nodes -days 365 -newkey rsa:2048
-keyout "chiave_privata.pem" -out "certificato.pem"
Abbiamo creato nella cartella corrente due file chiave_privata.pem e certificato.pem (auto-firmato) usando l’algoritmo RSA-2048bit con validità di un anno. Abbiamo indicato di non crittografare la chiave privata (-nodes).
All’interno della stessa cartella in cui sono contenuti il certificato e la chiave privata, possiamo creare un nuovo file https_server.js con il seguente frammento di codice.
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('chiave_privata.pem'),
cert: fs.readFileSync('certificato.pem')
};
server = https.createServer(options, (req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Esempio server HTTPSn');
})
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo https://${address}:${port}
`);
}
server
.listen(
4444,
'127.0.0.1',
callback
)
Se proviamo ad aprire il browser all’indirizzo https://127.0.0.1: 4444, visualizziamo una schermata che ci segnala che il certificato usato non è riconosciuto. In questo caso, avendo noi stessi generato il certificato, possiamo tranquillamente procedere e visualizzare il risultato mostrato nell’immagine sottostante.
Lavorare con file html e template
Abbiamo visto nei precedenti articoli che in Node.js possiamo leggere dei file in maniera estremamente semplice. Possiamo modificare il nostro esempio e inviare al client il contenuto di un file index.html.
const http = require('http');
const fs = require('fs');
const fileHTML = fs.readFileSync('index.html');
const server = http.createServer((req, res) => {
// cambiamo il valore di Content-type
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(fileHTML);
});
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo http://${address}:${port}
`);
}
server
.listen(
8000,
'127.0.0.1',
callback
)
L’esempio è simile a quello già visto. In questo caso, leggiamo il contenuto del file e lo inviamo al client. Notate che abbiamo cambiato il valore dell’header HTTP 'Content-Type': 'text/html' al fine di segnalare al browser che abbiamo inviato un file HTML.
Finora abbiamo visto delle soluzione che usavano del contenuto statico. Spesso è però necessario generare il contenuto della pagina in maniera dinamica. Per far ciò possiamo affidarci a dei liguaggi che permettono di adoperare dei template in combinazione con dei dati per creare il contenuto HTML finale. Nell’esempio che segue vedremo EJS (Effective JavaScript templating) anche se esistono numerose alternative.
All’interno di una nuova cartella, creiamo un file app.js e index.ejs (notate l’estensione.ejs). Con l’aiuto di NPM installiamo il package ejs (npm install ejs –save) dopo aver lanciato il comando npm init -y. Nel file index.ejs inseriamo il seguente codice.
// index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Esempio Node.js Server</title>
</head>
<body>
<h1><%= utente.nome %></h1>
<p>
<%= utente.bio %>
</p>
</body>
</html>
Non tratteremo la sintassi specifica di EJS di cui potete comunque trovare le informazioni necessarie sul sito ufficiale. Nel nostro esempio, tuttavia, chiediamo di inserire due valori utente.nome e utente.bio. Per capire da dove arrivano queste due proprietà, analizziamo il codice di app.js
// app.js
const http = require('http');
const fs = require('fs');
const ejs = require('ejs');
const template = fs.readFileSync('index.ejs').toString();
const utente = {
nome: 'Mario Rossi',
bio: `
Maecenas sed diam eget risus varius blandit sit amet non magna.
Maecenas sed diam eget risus varius blandit sit amet non magna.
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
Cum sociis natoque penatibus et magnis dis parturient montes,
nascetur ridiculus mus. Praesent commodo cursus magna,
vel scelerisque nisl consectetur et.
`
}
const server = http.createServer((req, res) => {
// cambiamo il valore di Content-type
res.writeHead(200, {'Content-Type': 'text/html'});
// {utente} è equivalente a {utente: utente}
// il valore fa riferimento alla costante definita sopra
const output = ejs.render(template, {utente});
res.end(output);
});
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo http://${address}:${port}
`);
}
server
.listen(
8000,
'127.0.0.1',
callback
)
Dopo aver letto il contenuto del file index.ejs, attraverso il metodo fs.readFileSync(), generiamo, ad ogni richiesta, il codice HTML finale. Per far ciò ci affidiamo alla funzione ejs.render() a cui passiamo come argomenti il template contenuto nel file index.ejs (di tipo String) e i dati che devono essere usati all’interno di tale file al posto di utente.nome e utente.bio. (Abbiamo passato un oggetto avente la sola proprietà ‘utente’)
Lavorare con i file JSON
Node.js dà la possibilità di inviare al client dei dati in formato JSON. Partiamo dall’esempio visto inizialmente e modifichiamolo leggermente.
const http = require('http');
const giocatori = require('./giocatori.json');
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify(giocatori));
});
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo http://${address}:${port}
`);
}
server
.listen(
8000,
'127.0.0.1',
callback
)
Non abbiamo fatto altro che leggere il contenuto del file JSON e inviarlo al browser. Anche in questo caso abbiamo modificato il valore dell’header HTTP 'Content-Type': 'application/json' per segnalare che i dati che stiamo inviando sono di tipo JSON.
?
Usare gli stream per migliorare le performance del server
Negli esempi precedenti abbiamo spesso letto un intero file in memoria e abbiamo poi inviato il contenuto al client. Questo tipo di operazioni sono dispendiose dal punto di vista dell’uso della memoria. Possiamo allora ottimizzare i server facendo ricorso al meccanismo degli Stream e delle Pipe. Vediamo anche in questo caso un semplice esempio.
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/html'});
// usiamo uno Stream e il metodo pipe
fs.createReadStream('index.html').pipe(res);
});
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo http://${address}:${port}
`);
}
server
.listen(
8000,
'127.0.0.1',
callback
)
Il codice appena visto è quasi identico a quello usato negli esempi precedenti. L’unica differenza è che ora creiamo uno Stream di tipo Readable per leggere il contenuto di un file index.html che inviamo direttamente al client usando il metodo pipe()
Definire diversi endpoint dell’applicazione
Per concludere questa lezione vediamo un ultimo esempio in cui definiamo diveri comportamenti a seconda del percorso visitato dall’utente. Se infatti riprendiamo gli esempi appena visti, il comportamento del server è sempre lo stesso. Il contenuto servito rimane identico sia se si visita http://127.0.0.1: 8000/ o http://127.0.0.1: 8000/qualsiasi-altro-percorso. In questo semplice esempio useremo direttamente la proprietà req.url per determinare il percorso visitato. In situazioni più complesse può essere però comodo effettuare il parsing di req.url attraverso il metodo parse() del modulo url.
Nel nostro caso ci limitiamo a creare un file app.js e i file HTML index.html, contatti.html, portfolio.html e 404.html. All’interno del file app.js copiamo il seguente frammento di codice.
// app.js
const http = require('http');
const fs = require('fs');
const header = {'Content-Type': 'text/html'};
const send = (source, destination, status = 200, headers = header) => {
destination.writeHead(status, headers);
fs.createReadStream(source).pipe(destination);
};
const server = http.createServer((req, res) => {
const url = req.url;
if (url === '/') {
return send('index.html', res);
} else if (/^/contatti/?$/.test(url)) {
return send('contatti.html', res);
} else if (/^/portfolio/?$/.test(url)) {
return send('portfolio.html', res);
} else {
send('404.html', res, 404, {});
}
});
const callback = () => {
const address = server.address().address;
const port = server.address().port;
console.log(`
Server avviato all'indirizzo http://${address}:${port}
`);
}
server
.listen(
8000,
'127.0.0.1',
callback
)
Riportiamo, inoltre, il contenuto del solo file index.html dal momento che gli altri file sono molto simili.
<!-- file index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Esempio Routing Node.js - Homepage</title>
</head>
<body>
<nav>
<a href="/">Home</a> |
<a href="/contatti">Contatti</a> |
<a href="/portfolio">portfolio</a>
</nav>
<h1>Home</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam.</p>
</body>
</html>
Nell’esempio illustrato, usiamo una serie di if per verificare quale sia il percorso richiesto e se questo non compare fra quelli accettati, mostriamo una pagina di errore.
Potete vedere il risultato nelle due immagini sottostanti.
Nel caso il percorso non sia fra quelli accettati viene mostrato il contenuto della pagina 404.html.
Come potete immaginare dall’esempio appena visto, usare un semplice if o switch per i diversi percorsi, non permette di costruire un’applicazione che scala facilmente. Bisognerebbe al contrario creare una soluzione più efficiente con un oggetto che si occupi di definire il diverso comportamento dell’applicazione al variare dell’URL. Per ora non vedremo un simile esempio, ma, in una prossima lezione, introdurremo Express, un framework molto diffuso che permette di costruire applicazioni anche più complesse in maniera veloce e intuitiva.
Conclusioni
In questo esempio abbiamo visto come usare il modulo http e https per creare un semplice server. Nella prossima lezione vedremo come usare l’API di Node.js per generare nuovi processi figli. Per far ciò faremo uso del modulo child_process.