HTTP/2 has several advantages over HTTP/1 that I've mention in my earlier post. In this post, I want to show how push-request can be performed using Node.js to create an HTTP/2 server. Push request is used to push static files such as scripts and styles so that the client can consume those static files as soon as possible without the need to request them first.
In this example, several built-in Node modules are required and an external module for ease of content-type setting named mime
. Let's install it first.
npm init
npm i --save mime
HTTP/2 encodes all headers of a request and it presents several new headers for identifying a request such as :method
and :path
. For more clarity, I call some constants related to the HTTP/2 header from the http2.constants
property. Let's create the server.js
file.
const http2 = require('http2');
const {
HTTP2_HEADER_PATH,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_CONTENT_LENGTH,
HTTP2_HEADER_LAST_MODIFIED,
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_STATUS,
HTTP_STATUS_INTERNAL_SERVER_ERROR
} = http2.constants;
const mime = require('mime');
const path = require('path');
const fs = require('fs');
const fsp = require('fs/promises');
const { O_RDONLY } = fs.constants;
Currently, most browsers require TLS encrypted communication for HTTP/2 so that for this demo, we need to generate a self-signed certificate and include the certificate as the server parameter.
const serverPort = 3000;
const publicLocation = 'public'; // directory to store static files
const serverOptions = {
key: fs.readFileSync('./your-selfsigned-key.pem'),
cert: fs.readFileSync('./your-selfsigned-cert.pem')
}
We need to create a public
directory. Then, create several static files including index.html
, app.js
, and style.css
inside the directory. We can write any methods or declarations inside those files for demo purposes. The index.html
file should include app.js
and style.css
on the head or body.
In server.js
, we create a function that will handle file sending through the HTTP/2 stream. In Node.js, the stream is an instance of the http2.ServerHttp2Stream
object.
function sendFile(stream, fileLocation) {
let fileHandle;
fsp.open(fileLocation, O_RDONLY)
.then((fh) => {
fileHandle = fh;
return fileHandle.stat();
})
.then((stats) => {
// setup file sending header
const headers = {
[HTTP2_HEADER_CONTENT_LENGTH]: stats.size,
[HTTP2_HEADER_LAST_MODIFIED]: stats.mtime.toUTCString(),
[HTTP2_HEADER_CONTENT_TYPE]: mime.getType(fileLocation)
};
// close the file in 'close' event of the stream
stream.on('close', () => {
fileHandle.close();
});
// send response with file descriptor
stream.respondWithFD(fileHandle.fd, headers);
})
.catch((reason) => {
stream.respond({
[HTTP2_HEADER_STATUS]: HTTP_STATUS_INTERNAL_SERVER_ERROR
});
stream.end();
});
}
Last, we define an HTTP/2 server object that will handle file requests. For this demo, the server only accepts any request to index.html
file. Other requests will be responded with a plain text message.
const server = http2.createSecureServer(serverOptions);
server.on('stream', (stream, headers) => {
// get some headers
const method = headers[HTTP2_HEADER_METHOD].toLowerCase();
const url = new URL(headers[HTTP2_HEADER_PATH], 'https://' + headers[HTTP2_HEADER_AUTHORITY]);
const pathname = url.pathname.replace(/^\/+|\/+$/g, '');
// handle root or index.html file request
if (pathname==='' || pathname==='index.html') {
if (stream.pushAllowed) {
// push app.js
stream.pushStream({
[HTTP2_HEADER_PATH]: '/app.js'
}, (err, pushStream) => {
if (!err) {
sendFile(pushStream, path.join(__dirname, publicLocation, 'app.js'));
}
});
// push style.css
stream.pushStream({
[HTTP2_HEADER_PATH]: '/style.css'
}, (err, pushStream) => {
if (!err) {
sendFile(pushStream, path.join(__dirname, publicLocation, 'style.css'));
}
});
}
// send index.html
let indexFileLocation = path.join(__dirname, publicLocation, 'index.html');
sendFile(stream, indexFileLocation);
} else { // handle other requests
stream.respond({
'content-type': 'text/plain; charset=utf-8',
':status': 200
});
stream.end('hello world');
}
});
server.listen(serverPort, () => {
console.log('HTTP2 server listen to port ' + serverPort);
});
Now we can start the server and open the website in a browser with the address https://localhost:3000/index.html
. If we open the browser inspection tool, we can see on the network panel that the initiator of app.js
and style.css
requests are called "Push". In other words, those files have already been cached by the browser, and the browser isn't required to make additional HTTP requests to the server.
Comments
Post a Comment