Sun Oct 15 2017
Paul Shan
HTTP/2 or HTTP/2.0, the upgraded version of the HTTP network protocol is derived from an experimental protocol, named SPDY, developed by Google. It’s the first revision of HTTP protocol since HTTP1.1 and contains many useful features in it. The most interesting and promising feature of HTTP/2 is server push
. This article will give you a low level view of how server push
works and how to implement it using node.js in the server. You can pickup any other server side language as well; we’re choosing node as majority of our users are from JavaScript ecosystem.
At a high level server push is as simple as giving you a soda bottle and a glass when you asked for a whiskey in a wine retail shop.
When the browser hits a particular route of your server and it serves an html file; you know it very well that in a few moment the browser will again ask for some css or JavaScript files (if not cached). So why not send those files along with that html file itself somehow?
The entire idea of server push is based on this. A server should be able to send or push extra files/streams even if there was no request from the client/browser.
style.css
and script.js
soon, so he should forecast for these.index.html
) notifying the client that he is willing to send two more data streams.index.html
.style.css
and script.js
.index.html
, when it will feel the need of style.css
and script.js
, it will check if anything received by server push is similar to the requirement, and gets the response from there.At the protocol level, it’s all about frames (group of bytes), which are part of streams. There are different kinds of frames available in HTTP/2, whereas to understand the server push
, we should be good by knowing only four of them. HEADERS
, PUSH_PROMISE
, RST_STREAM
and DATA
.
The HEADERS
frames carries the http headers and works like a notifier or messenger. Client can send these frames to server which makes the server understand that a request has been made and if sent by the server to the client; it means a response to the previous request (or push) is being sent.
PUSH_PROMISE
, as the name suggests, is the most important frame of server push
mechanism. It works like a pre-header frame for pushed contents. It is used to notify the client in advance of streams the server intends to initiate (the new streams for style.css
and script.js
as per our previous example). It is sent along with the header information of the about to be pushed contents. PUSH_PROMISE
s are sent before the content of any file (even the initiater, which is index.html
as per our example).
There are two big benefits of this approach. First, it will avoid the race condition of the same resource getting requested again by the client (cause if the client don’t have a info on what the server is going to push, it may request for style.css
after index.html is rendered and push is not fully finished). Secondly, if the about to be pushed files are already there on the client due to previous cache, the client can refuse to accept the pushed streams (or any other stream) by sending RST_STREAM
frame.
After all these settlements, DATA
frame comes into the picture to send the actual content of those files.
Node is always in active development and one of the fastest growing tools out there. Previously there used to be an npm package spdy which was generally used to implement server push
; but since node 8.4.0
onwards started supporting http/2 natively (in experimental mode with --expose-http2
flag), we will do it in the native way. So, you should have node.js 8.4.0
or above installed.
Server push doesn’t mandate a secure server; but majority of the browsers will not support server push unless done from a secured server.
You can refer our node.js ssl server or just run the following command (if you have openssl) in your terminal or command prompt, navigating to your project’s root directory. You will find two new files privateKey.key
and certificate.crt
has been created.
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout privateKey.key -out certificate.crt
First of all scaffold your node project (may be by using npm init
) and create a server.js
file with the code below to make a secure server.
'use strict'
const fs = require('fs');
const path = require('path');
const http2 = require('http2');
const PORT = process.env.PORT || 3000;
// Request handler
function onRequest(req, res) {
res.statusCode = 200;
res.end("Hello VoidCanvas");
}
// creating an http2 server
const server = http2.createSecureServer({
cert: fs.readFileSync(path.join(__dirname, '/certificate.crt')),
key: fs.readFileSync(path.join(__dirname, '/privateKey.key'))
}, onRequest);
// start listening
server.listen(PORT, (err) => {
if (err) {
console.error(err);
return -1;
}
console.log(`Server listening to port ${PORT}`);
});
After completing this step, to check whether the experimental http2 is working or not you should start your node server using the command node --expose-http2 server.js
. You should be able to visit https://localhost:3000
now. If you find something like the image below in your browser; click on advanced and then proceed.
You can refer our git repo to get the project structure. It’s very simple. Just have added a public folder containing the files to be served. Rest you already know about the certificate, private key etc. Once you’ve set the same in your end, just modify your server.js
like the following to make your server, push the stylesheet and javascript files whenever client requests for index.html
.
'use strict'
const fs = require('fs');
const path = require('path');
const http2 = require('http2');
const utils = require('./utils');
const { HTTP2_HEADER_PATH } = http2.constants;
const PORT = process.env.PORT || 3000;
// The files are pushed to stream here
function push(stream, path) {
const file = utils.getFile(path);
if (!file) {
return;
}
stream.pushStream({ [HTTP2_HEADER_PATH]: path }, (pushStream) => {
pushStream.respondWithFD(file.content, file.headers)
});
}
// Request handler
function onRequest(req, res) {
const reqPath = req.path === '/' ? '/index.html' : req.path;
const file = utils.getFile(reqPath);
// 404 - File not found
if (!file) {
res.statusCode = 404;
res.end();
return;
}
// Push with index.html
if (reqPath === '/index.html') {
push(res.stream, '/assets/main.js');
push(res.stream, '/assets/style.css');
} else {
console.log("requiring non index.html")
}
// Serve file
res.stream.respondWithFD(file.content, file.headers);
}
// creating an http2 server
const server = http2.createSecureServer({
cert: fs.readFileSync(path.join(__dirname, '/certificate.crt')),
key: fs.readFileSync(path.join(__dirname, '/privateKey.key'))
}, onRequest);
// start listening
server.listen(PORT, (err) => {
if (err) {
console.error(err);
return -1;
}
console.log(`Server listening to port ${PORT}`);
});
Now again run your node server just like you did previously with node --expose-http2 server.js
and go to localhost’s post 3000 keeping your developer’s tool open. You will find something like the following there.
A very common question comes in people’s mind is, what will happen when the client again make a request for another html page of the same website (considering the entire website uses same css and js files)?
Will it again push those files? Cause server doesn’t know if it’s a new request or old. So will it hamper the bandwidth?
The answer is NO!
While describing the http/2 push communication, I’ve said that before sending any DATA
frame (which has the content of a file) to the client the server sends the PUSH_PROMISE
frame. And PUSH_PROMISE
also contains headers.
Once the PUSH_PROMISE
is received in the clients end; the client checks the headers to determine in the file about to be pushed is already cached to his side. If yes, the client sends a RST_STREAM
to confirm the rejection of the to be pushed file/files.
This way, the cached files will not be re-pushed and your bandwidth will not be killed.
As I’ve just described that cached files will not be re-pushed; but all these decisions are being handled by the server and client internally with no intervention from the developer. If I consider our demo, if the client again asks for index.html
, the server’s code of push()
function will run; even though the files will not be actually transferred.
I think this is also a performance hit. For this small example it’s a small push()
function, but for someone else there could be more complicated and time consuming operations. So for him, even though the bandwidth problem won’t occur, but the CPU consumption will still increase.
But that’s how it is for now. You can probably drop a cookie in your client to determine whether he is a returning user or new and do your complex operations basing on that. If you have another way to bypass this scenario, kindly add that to comment section.
So keep pushing and stay happy :)
SHARE THIS ARTICLE
Thu Mar 10 2016
OAuth authentications are pretty popular now a days and another thing which is popular is JavaScript. This article shows how to plugin google’s oAuth api for authentication in your own node application.Sat Mar 01 2014
This is a continuation of my CSS3 loader snippet collection series. I've provided spinning css3 animation loader in the part 1 of this series and here in part 2, I'm providing various square type loading