Node.js can HTTP/2 Push! . When I see the word push, I think of WebSocket message push. Can HTTP/2 also be used to push messages proactively, like WebSocket? Oh, that’s cool. I just got interested.

However, after reading the article, I found that there is a slight gap between ideal and reality. Simply put, HTTP/2’s so-called server push is the ability to respond to multiple resources when a server receives a request. For example: the browser asks the server for index.html, and the server not only returns the index.html, it can also push index.js, index.css, and so on to the client. The most obvious benefit is that the browser saves page load time by not having to parse the page and then make a request to retrieve the data.

It’s a bit different, but it looks interesting enough to give it a try. After all, the paper come Zhongjue shallow, must know this to practice!

HTTP/2

I haven’t used HTTP/2 before, so I always want to know about it before trying it out. There is also a lot of information available on the web about HTTP/2, but I will briefly mention one of its biggest advantages: speed! . This is faster than HTTP 1.x, so why is it faster?

The head of compression

The headers here refer to HTTP request headers. You might wonder how big the request header can be, but it’s nothing compared to the resource. In fact, with the development of the Internet, more and more data are carried in the request head, along with a random “user-agent” on a long string. Cookies also store more and more information. To make matters worse, all requests on a page will carry these repeated headers.

Therefore, HTTP/2 adopts the HPACK algorithm, which can greatly compress the header data and reduce the total resource request size. The general idea is to maintain two dictionaries, a static dictionary, and the more common header names. A dynamic dictionary that maintains common header data for different requests.

multiplexing

We know that in HTTP 1.x, we can request in parallel. However, browsers have a limit on the number of concurrent requests for the same domain (FireFox, Chrome, up to six). So many sites may have more than one static resource site. The TCP connection must be re-established each time a request is made. Most Web engineers are familiar with the TCP three-way handshake, which is also expensive.

While keep-alive in http1.x can avoid the TCP three-way handshake, keep-alive is serial. So either multiple handshakes in parallel or no handshakes in serial is not the best result, what we want is no handshakes in parallel.

Fortunately HTTP/2 solved this problem. After the connection between the client and server is established, a bidirectional channel is established between the two sides. This flow channel can contain multiple messages (HTTP requests) at the same time. The data frames of different messages can be sent in parallel out of order without mutual influence and blockage, thus realizing a TCP connection and concurrent execution of N HTTP requests. By increasing concurrency and reducing TCP connection overhead, HTTP/2 speeds are greatly improved, especially when network latency is high.

Here are two network request time waterfall flow comparison diagrams:

The HTTP 1.1

HTTP/2

Server Push

Above, we described that HTTP/2 connections establish a two-way channel. A Server Push is a stream in which the client can return data that was not requested by the client.

The above header compression and multiplexing does not require the developer to do anything, as long as HTTP/2 is enabled and the browser supports it. But Server Push requires developers to write code to do it. Let’s go ahead and play on Node.

Node HTTP/2 Server Push operations

Node support for HTTP/2

Experimental HTTP/2 support has been available since Node 8.4.0. On the evening of April 24, 2018, Node V10 was finally released, however, with experimental support for HTTP/2… However, the community is already discussing the HTTP/2 removal experiment, and we should expect to see better SUPPORT for HTTP/2 from Node in the near future. So before that, we can first master this knowledge and do some practice.

Follow the gourd

Let’s start by creating an HTTP/2 service based on the Node documentation. One thing to mention here is that none of the current popular browsers support unencrypted, insecure HTTP/2. So we must generate the certificate and key, and then create a secure HTTP/2 link via http2.createsecureServer.

If you want to practice it yourself and generate a local certificate, you can refer to here: Portal.

// server.js
const http2 = require('http2')
const fs = require('fs')
const streamHandle = require('./streamHandle/sample')
const options = {
  key: fs.readFileSync('./ryans-key.pem'),
  cert: fs.readFileSync('./ryans-cert.pem'),}const server = http2.createSecureServer(options)
server.on('stream', streamHandle)
server.listen(8125)
Copy the code

We then write the stream processing according to the document and push data with a URL path of ‘/’.

// streamHandle/sample.js
module.exports = stream= > {
  stream.respond({ ':status': 200 })
  stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => {
    if (err) throw err
    pushStream.respond({ ':status': 200 })
    pushStream.end('some pushed data')
    pushStream.on('close', () = >console.log('close'))
  })
  stream.end('some data')}Copy the code

Then we opened Chrome and went to https://127.0.0.1:8125 and found that the page kept showing some data. I don’t know where the pushed data is. The Network request panel opens, and there are no other requests.

Puzzled, but I don’t want to stop here, how to do?

From the cradle

I decided to start by writing a normal HTTP/2 business request as follows:

module.exports = (stream, headers) = > {
  const path = headers[':path']
  if (path.indexOf('api') > =0) {
    / / request API
    stream.respond({ 'content-type': 'application/json'.':status': 200 })
    stream.end(JSON.stringify({ success: true}}))else if (path.indexOf('static') > =0) {
    // Request a static resource
    const fileType = path.split('. ').pop()
    const contentType = fileType === 'js' ? 'application/javascript' : 'text/css'
    stream.respondWithFile(`./src${path}`, {
      'content-Type': contentType
    })
  } else {
    / / requests for HTML
    stream.respondWithFile('./src/index.html')}}Copy the code

If the request address is static, return a static resource for the path. Otherwise an HTML file is returned.

The contents of the HTML file are as follows:


      
<html>
<head>
  <meta charset=utf-8>
  <title>HTTP/2 Server Push</title>
  <link rel="shortcut icon" type=image/ico href=/static/favorite.ico>
  <link href=/static/css/app.css rel=stylesheet>
</head>
<body>
  <h1>HTTP/2 Server Push</h1>
  <script type=text/javascript src=/static/js/test.js></script>
</body>
</html>
Copy the code

After running, open Chrome again, visit https://127.0.0.1:8125, we can see that the page is properly rendered, check the web panel, and find that the protocol is already HTTP/2.

So we’ve developed a very simple HTTP/2 application. Next, we add the server push function, which actively returns the JS resource when accessing the index.html request to see how the browser responds.

Grab a gourd baby

module.exports = (stream, headers) = > {
 const path = headers[':path']
 if (path.indexOf('api') > =0) {
   // Request API part code - omitted
 } else if (path.indexOf('static') > =0) {
   // Request static resource part of the code - omitted
 } else {
   // Actively push js files when requesting HTML
   stream.pushStream(
 	  { ':path': '/static/js/test.js' },
     (err, pushStream, headers) => {
   	if (err) throw err
   	pushStream.respondWithFile('./src/static/js/test2.js', {
   	  'content-type:': 'application/javascript'
   	})
   	pushStream.on('error'.console.error)
     }
   )
   stream.respondWithFile('./src/index.html')}}Copy the code

Code is, when a client requests the index. The HTML, in addition to return to the index of a service. The HTML files, by the way the test2. Js this file on the server and the client if request https://127.0.0.1:8125/static/js/test.js again, Test2.js will be retrieved directly.

The purpose of using test2.js is to conveniently know whether the client requests the test2.js file pushed by the server or directly requests the test.js file obtained by the server.

Test2. js will print This is normal js.test2. js will print This is server push JS.

One would expect the latter. Then we open Chrome, visit https://127.0.0.1:8125, and display the following results:

!!!!!!!!!! Lift the table !!!!

The result of This display is not expected to print This is Server push JS, the JS file requested by the page is normal network request, not the test2.js that I actively push. I scour the country and saw a similar problem in a Node issue: http2 pushStream not Providing Files for: Path Entries (CHROME 65, works in FF).

Works in FireFox ??????? Chrome’s bug????????

You write the code according to the document, but the result is not as shown in the document, various checks are useless, and finally found to be some non-subjective reasons, the biggest pain for programmers is this…. Then, with a mixture of pain and change of heart, I opened my Firefox, visited the page, and saw the following results:

Finally! As you can see, the page prints the output of the test2.js file.

It didn’t work at first because of a bug in Chrome. Nevertheless, we have taken a huge step forward.

Ps: my Chrome version 66.0.3359.117, still have this bug

Chicken ribs

Although we took a big step forward, we faced an awkward problem: more of our static resources were hosted on CDN. Then our actual scene will encounter the following situation:

  1. All website resources, including HTML, CSS, JS, and image, are stored on one service server. Sorry students, your business server bandwidth is low, I am afraid that can not withstand so many static resources of concurrent requests, you have been hopelessly slow.
  2. Network routing goes through the back end, that is, HTML goes through the back end, and other static resources host CDN. Sorry, students, static resources are all on THE CDN, how to push your business server?
  3. Complete front-end and back-end separation, HTML and other static resources are on the CDN. In this case, it’s useful, but it’s not great. Because HTTP/2 inherently supports multiplexing, it has reduced the network overhead associated with TCP three-way handshakes. Server push simply reduces the time it takes for a browser to parse HTML, which is negligible for modern browsers. (PS: As I write, I happen to see a cloud service that supports Server Push.)

In that case, it’s a chicken rib! All for nothing?

I was born to be useful

People still can’t easily give up treatment. If you think about it a little bit more, there are some application scenarios – initialization API requests.

Many single-page applications now have a lot of initialization requests to get user information, page data, and so on. And these are required HTML loading, and then js loading, and then to execute. And most of the time, the page is blank until the data is loaded. However, the JS resources of a single page application are often large. It is also common for a vendor package to be several megabytes. By the time a browser loads and parses such a large package, it can take a lot of time. Then request some initialization API, if these API is time-consuming, the page will be blank for a long time.

However, if we could push the API data initialized to the client when the HTML request is made, we could get the data immediately when the JS request is finished, which would save valuable white screen time. Let’s do it again!

module.exports = (stream, headers) = > {
  const path = headers[':path']
  if (path.indexOf('api') > =0) {
    / / request API
    stream.respond({ 'content-type': 'application/json'.':status': 200 })
    stream.end(JSON.stringify({ apiType: 'normal'}}))else if (path.indexOf('static') > =0) {
    // Request static resource code - omitted
  } else {
    / / requests for HTML
    stream.pushStream({ ':path': '/api/getData' }, (err, pushStream, headers) => {
      if (err) throw err
      pushStream.respond({ ':status': 200 , 'content-type': 'application/json'});
      pushStream.end(JSON.stringify({ apiType: 'server push'}}))); stream.respondWithFile('./src/index.html')}}Copy the code

Similarly, I make some differences between the normal request API and the data pushed by the server, so that it is more intuitive to determine whether the data pushed by the server is obtained. Then write the following request in the front-end js file and print the result:

window.fetch('/api/getData').then(result= > result.json()).then(rs= > {
  console.log('fetch:', rs)
})
Copy the code

Unfortunately, here’s what we got:

The result of the request indicates that this is not server push data. Could it be a bug in the browser? Or does fetch not support fetching data from server push? I immediately wrote another version with XMLHttpRequest:

window.fetch('/api/getData').then(result= > result.json()).then(rs= > {
  console.log('fetch:', rs)
})

const request = new XMLHttpRequest();
request.open('GET'.'/api/getData'.true)
request.onload = function(result) {
  console.log('ajax:'.JSON.parse(this.responseText))
};
request.send();
Copy the code

The results are as follows:

!!!!!!!!!! Lift the table !!!!

Fetch does not support HTTP2 server push!

Or chicken ribs

In fact, in addition to fetch does not support, there is another fatal problem, that is, the server push, on the current Node server, the url of the server push resource cannot be fuzzy matching. That is, if a request has url dynamic parameters, it will not match. As in my example stream. pushHandle ({‘:path’: ‘/ API /getData’}, pushHandle), if the interface requested by the front end is/API /getData? Param =1, then you don’t get server push data.

In addition, it only supports GET and HEAD requests. POST and PUT are also not supported.

In view of the fetch issue, I searched both inside and outside China, but failed to come to a conclusion. This also shows that the server push feature is rarely used in the community at present, and it is difficult to quickly locate and solve problems when encountering problems.

Therefore, it seems that in the push API, its application scenario is again limited, only suitable for push fixed URL initialization GET requests.

The sea of pain is endless

To sum up, I have come to the conclusion that using Server push on Node is currently inappropriate in extreme cases and the costs outweigh the benefits. Mainly due to the following reasons:

  1. As of Node V10.0.0, HTTP/2 is still an experimental module;
  2. Poor browser support; As mentioned above, Fetch does not support server push.
  3. The actual scenarios for pushing static resources are few and far between, and the speed increase is not significant in theory;
  4. The push API only supports fixed urls and does not carry any dynamic parameters.

Note: The above content is only limited to Node service, other servers have not been studied, do not necessarily have the above problems

Although SERVER Push is currently not working for me, HTTP/2 is a good thing. In addition to the benefits I mentioned at the beginning of this article, HTTP/2 has a lot of new and useful features, such as stream priority and flow control, which are not covered in this article. We can go to understand, to our future development of high-performance Web should certainly have a lot of help!

This paper involved the source code: https://github.com/wuomzfx/http2-test

The original link: https://yuque.com/wuomzfx/article/eh551s