I figured out a minor issue with SPDY server push and HTML pages last night. It occurs to me that pushing other HTML pages into the browser's cache before the original server request is fulfilled is a tad silly.
I makes far more sense to push out only resources needed for the page itself (e.g. images, Javascript, CSS) at the same time as the requested page. Only once all of those resources have been sent should secondary web pages get sent.
Currently, my express-spdy app is sending out three pages and a CSS stylesheet thusly:
var app = module.exports = express.createServer({I am unsure how I would like the post-response push API to work, but the simplest thing that I can think of for experimentation is:
//...
push: function(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;
pusher.push_file("public/one.html", "https://localhost:3000/one.html");
pusher.push_file("public/two.html", "https://localhost:3000/two.html");
pusher.push_file("public/three.html", "https://localhost:3000/three.html");
pusher.push_file("public/stylesheets/style.css", "https://localhost:3000/stylesheets/style.css");
}
});
var app = module.exports = express.createServer({The express-spdy package already supports the
// ...
push: function(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;
pusher.push_file("public/stylesheets/style.css", "https://localhost:3000/stylesheets/style.css");
},
push_after: function(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;
pusher.push_file("public/one.html", "https://localhost:3000/one.html");
pusher.push_file("public/two.html", "https://localhost:3000/two.html");
pusher.push_file("public/three.html", "https://localhost:3000/three.html");
}
});
push
callback (by way of node-spdy), so I would only need to add support for push_after
. Let's see if that's easy...In the
Response
class, I use a bit of indirection to invoke that push
callback. It ultimately get called as part of the write()
method, just after sending out headers:To get the desired effect, I could push out the post-response resources after the data frame has been written:
Response.prototype._write = function(data, encoding, fin) {
if (!this._written) {
this._flushHead();
this._push_stream();
}
//
};
Response.prototype._write = function(data, encoding, fin) {But... SPDY data frames can include a FIN flag. If set, a FIN flag indicates that the stream is closed. This means that no more data can be sent on the stream and no more pushed data associated with that stream can be initiated. If the headers for a push stream had been initiated via SYN_STREAM frame prior to sending a data FIN (or just plain FIN), then the data could still be pushed. But, as that code is written, it is possible to
// ...
this.c.write(dframe);
this._push_after_response_stream();
};
write()
a FIN data frame and then attempt to initiate a server push which is a SPDY no-no.A quick and not-too-dirty solution would be to write the post-response push data after
write()
, but not in an end()
:Response.prototype.write = function(data, encoding) {An
var stream = this._write(data, encoding, false);
this._push_after_response_stream();
return stream;
};
Response.prototype.end = function(data, encoding) {
this.writable = false;
return this._write(data, encoding, true);
};
end()
sets the FIN flag whereas a write()
does not. Unfortunately, express.js seems to favor calls to end()
.So instead, I opt for an ugly hack. In the "private"
_write()
method, I send a normal data frame and a separate FIN data frame. In between those, I push any post-response push streams:Response.prototype._write = function(data, encoding, fin) {That ain't pretty, but it ought to get the job done for today. Firing up the browser, I find that it does, indeed, work:
//...
// Write the data frame
var dframe = createDataFrame(this.getStreamCompressor(), {
streamID: this.streamID,
flags: 0
}, Buffer.isBuffer(data) ? data : new Buffer(data, encoding));
this.c.write(dframe);
// Push any post-response push streams
this._push_after_response_stream();
// Write the data FIN if this if fin
if (fin) {
var dfin = createDataFrame(this.getStreamCompressor(), {
streamID: this.streamID,
flags: enums.DATA_FLAG_FIN
}, new Buffer(0));
this.c.write(dfin);
}
};
#####Checking out the failure case, I move the post-response push after the data FIN has been sent:
# Response headers
t=1309198692649 [st= 80] SPDY_SESSION_SYN_REPLY
--> flags = 0
--> connection: keep-alive
content-length: 303
content-type: text/html
status: 200 OK
version: HTTP/1.1
x-powered-by: Express
--> id = 1
#####
# Pre-response push (CSS)
t=1309198692657 [st= 88] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> status: 200
url: https://localhost:3000/stylesheets/style.css
version: http/1.1
--> id = 2
#####
# Response data (the requested web page)
t=1309198692658 [st= 89] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 303
--> stream_id = 1
######
# Post response push
# Post response push #1
t=1309198692658 [st= 89] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/one.html
version: http/1.1
--> id = 4
# Post response push #2
t=1309198692658 [st= 89] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/two.html
version: http/1.1
--> id = 6
# Post response push #3
t=1309198692658 [st= 89] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/three.html
version: http/1.1
--> id = 8
#####
# Finally, the data FIN for the response:
t=1309198692658 [st= 89] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 1
t=1309198692666 [st= 97] SPDY_STREAM_ADOPTED_PUSH_STREAM
Response.prototype._write = function(data, encoding, fin) {After trying to load this in the browser, I find that it does fail as expected:
// ...
// Write the data FIN if this if fin
if (fin) {
var dfin = createDataFrame(this.getStreamCompressor(), {
streamID: this.streamID,
flags: enums.DATA_FLAG_FIN
}, new Buffer(0));
this.c.write(dfin);
}
// Push any post-response push streams
this._push_after_response_stream();
};
t=1309199284990 [st= 61] SPDY_SESSION_RECV_DATAAfter attempting to push on a stream that was just closed (#1), Chrome correctly sends a RST_STREAM packet informing the server that it behaving badly. Nice.
--> flags = 0
--> size = 303
--> stream_id = 1
t=1309199284990 [st= 61] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 1
t=1309199284990 [st= 61] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 1
--> flags = 2
--> content-type: text/html
status: 200
url: https://localhost:3000/one.html
version: http/1.1
--> id = 4
t=1309199284990 [st= 61] SPDY_SESSION_SEND_RST_STREAM
--> status = 8
--> stream_id = 4
That is a good stopping point for today. I will pick back up tomorrow attempting to come up with a nice API for post-response push.
Day #59
No comments:
Post a Comment