Wednesday, April 18, 2012

SPDY Server Push in Edge Node-SPDY

‹prev | My Chain | next›

As of last night, I know how to SPDY-ize express.js applications with only node-spdy—no express-spdy required. This requires a bit of futzing (switching down to express.js 2.x because node-spdy won't work with 3.x and modifying express to run on unstable node.js 0.7). I will worry about compatibility another day. Today, I would like to get SPDY server push working with this configuration.

The node-spdy documentation suggests that a push stream can be initiated with (this was corrected):
spdy.createServer(options, function(req, res) {
  var headers = { 'content-type': 'application/javascript' };
  res.send('/main.js', headers, function(err, stream) {
    if (err) return;

    stream.end('alert("hello from push stream!");');
  });

  res.end('<script src="/main.js"></script>');
}).listen(443);
That will not work for my case because this is a dumb server that responds to all requests with nothing more than a document containing: <script src="/main.js"></script> (the res.end() line at the end). This will not give express.js a chance to kick in and do its thing. Also, I think that the res.send() line is meant to be res.push().

This is easy enough to try out, I save the dumb server, fire it up, and load the page in Chrome. This promptly results in a crash:
➜  express-spdy-test  node app

node.js:247
        throw e; // process.nextTick error, or 'error' event on first tick
              ^
TypeError: Cannot read property 'method' of undefined
    at ServerResponse.send (/home/chris/tmp/express-spdy-test/node_modules/express/lib/response.js:108:30)
    at Server.<anonymous> (/home/chris/tmp/express-spdy-test/app.js:21:7)
    at Server.emit (events.js:70:17)
    at Connection.<anonymous> (/home/chris/tmp/express-spdy-test/node_modules/spdy/lib/spdy/server.js:61:14)
    at Connection.emit (events.js:70:17)
    at HTTPParser.onIncoming (http.js:1665:12)
    at HTTPParser.onHeadersComplete (http.js:115:25)
    at Stream.ondata (http.js:1562:22)
    at Array.0 (/home/chris/tmp/express-spdy-test/node_modules/spdy/lib/spdy/server.js:544:27)
    at EventEmitter._tickCallback (node.js:238:41)
Yup. Line 21 is the res.send(). So I convert that to res.push():
var app = spdy.createServer(options, function(req, res) {
  var headers = { 'content-type': 'application/javascript' };
  res.push('/main.js', headers, function(err, stream) {
    if (err) return;

    stream.end('alert("hello from push stream!");');
  });

  res.end('<script src="/main.js"></script>');
});
Now, I get my expected alert():


Man, I love SPDY server push. Chrome makes a request, to which the dumb server always replies "<script src="/main.js"></script>". Without SPDY server push, this would result in the browser making a second request of the server for the /main.js resource. But, using SPDY server push, we have already pushed the contents of /main.js into browser cache—in this case the alert() dialog. By the time the browser realizes it needs to request /main.js, it is already in browser cache so no request is actually made and the content is served directly from cache.

Checking things over in the SPDY tab of chrome://net-internals, I see that there is, indeed, a SPDY push stream taking place:
SPDY_SESSION_SYN_STREAM
--> flags = 1
--> host: localhost:3000
    method: GET
    url: /
    version: HTTP/1.1
    ....
--> id = 1
SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: application/javascript
    status: 200
    url: https://localhost:3000/main.js
    version: HTTP/1.1
--> id = 2
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 33
--> stream_id = 2
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 2
SPDY_SESSION_SYN_REPLY
--> flags = 0
--> status: 200 OK
    version: HTTP/1.1
--> id = 1
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 32
--> stream_id = 1
SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 1
The only problem with the above SPDY server push is that the entire contents of the push are sent before the page response. In SPDY, data and headers are separate—even in push. It would likely be more proper to send just the headers in the form of SPDY_SESSION_PUSHED_SYN_STREAM, then complete the SYN_REPLY and send all data associated with the initial request along on stream #1 before sending the pushed data. That is a minor quibble (and one that can be addressed another day). For now, it is pretty cool that SPDY push works so well.

With that, I can return to my SPDY-ized express site. To perform a SPDY push in express, I need to modify my index route:
exports.index = function(req, res){
  res.render('index', { title: 'Express' });
};
Just as in the dumb server, I add a res.push() statement:
exports.index = function(req, res){
  var headers = { 'content-type': 'application/javascript' };
  res.push('/main.js', headers, function(err, stream) {
    if (err) return;

    stream.end('alert("hello from push stream!");');
  });

  res.render('index', { title: 'Express' });
};
I also need to tell my Jade layout to request /main.js:
!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(src='/main.js')
  body!= body
With that, I get the alert() pushed into cache. Once I click OK, the normal express page loads:


Kudos to Fedor Indutny on that API—I think that is an improvement on the original SPDY push that I had hacked into node-spdy. It is easy and makes sense. This is why I write the books, not the original code.


Day #360

1 comment:

  1. I downloaded node-spdy module.But I cannot find the file push_stream.js

    ReplyDelete