Wednesday, June 8, 2011

A Re-Usable SPDY Server Push Solution

‹prev | My Chain | next›

Up tonight, I would like to finish off my server push work on node-spdy. Specifically, I would like the implementation a little smaller and actually re-usable.

Currently, I have node-spdy sending a push stream after it sends a response:
Response.prototype._push_stream = function() {
if (this._pushed) return false;
// TODO don't hard code this
if (this.streamID != 1) return false;

this._push(this);

this._pushed = true;
};
If the two guard clauses do not match, then the _push() callback is invoked. I am sending the Response into the _push() callback so that the callback has access to methods like:
Response.prototype.push_file = function(filename, url) {
var push_stream = createPushStream(this.cframe, this.c, url);

push_stream.write(fs.readFileSync(filename));

push_stream.end();
};
(and push_file() has access to the SPDY control frame, the connect object, etc)

But now that I think about it, I am not convinced that I need the guard clause in the _push_stream() method—at least not in _pushStream() proper. I may have added them during the spike. Removing them leaves me with:
Response.prototype._push_stream = function() {
this._push(this);
};
I can probably get right of that indirection, assuming that I have not broken anything. The vows.js specs still pass without the guard clauses, but that is because the test is quite simple. To be sure, I also ensure that the test/basic-push-server.js example server still works. It does (no RST_STREAMS).

But...

There are now two push streams in the SPDY session (from the SPDY tab of Chrome's about:net-internals):
t=1307586375807 [st= 34]     SPDY_SESSION_PUSHED_SYN_STREAM  
--> associated_stream = 1
--> flags = 2
--> status: 200
url: https://localhost:8082/style.css
version: http/1.1
--> id = 2
...
t=1307586375827 [st= 54] SPDY_STREAM_ADOPTED_PUSH_STREAM


#####
# And, shortly thereafter...

t=1307586375874 [st=101] SPDY_SESSION_PUSHED_SYN_STREAM
--> associated_stream = 3
--> flags = 2
--> status: 200
url: https://localhost:8082/style.css
version: http/1.1
--> id = 6

...
t=1307586375868 [st= 95] SPDY_STREAM_ADOPTED_PUSH_STREAM
The first push-stream was associated with the original SPDY request (stream ID #1). The second push-stream was associated with a different stream ID (3), which is the POST request for an AJAX resource. The whole purpose of push streams is to reduce the number of requests and responses so this is no good.

Fortunately, a solution is now easily accomplished in the push() callback that is established when the server is started. I need to change it from:
var options = {
//...
push: function(pusher) {
pusher.push_file("pub/style.css", "https://localhost:8082/style.css");
pusher.push_file("pub/spdy.jpg", "https://localhost:8082/spdy.jpg");
}
};

var server = spdy.createServer(options, function(req, res) {
//...
});
I add a guard clause to only push for the initial SPDY stream:
var options = {
//...
push: function(pusher) {
if (pusher.streamID > 1) return;

pusher.push_file("pub/style.css", "https://localhost:8082/style.css");
pusher.push_file("pub/spdy.jpg", "https://localhost:8082/spdy.jpg");
}
};
With that, I am back to my nice server push of resources along with the homepage, followed by the AJAX POST and nothing else (no requests for images, CSS, etc.):
t=1307589500211 [st= 69]     SPDY_STREAM_ADOPTED_PUSH_STREAM  
t=1307589500213 [st= 71] SPDY_SESSION_SEND_DATA
--> flags = 1
--> size = 18
--> stream_id = 3
t=1307589500216 [st= 74] SPDY_SESSION_SYN_REPLY
--> flags = 0
--> connection: keep-alive
status: 200 OK
version: HTTP/1.1
--> id = 3
t=1307589500251 [st=109] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 18
--> stream_id = 3
t=1307589500251 [st=109] SPDY_SESSION_RECV_DATA
--> flags = 0
--> size = 0
--> stream_id = 3
That marks the end of the SPDY session. Since the last stream ID is 3, I know that only two requests from the client were issued. Client stream IDs are odd in SPDY while server initiated streams are even. Thus, stream #1 replied with the homepage (and got some associated server push streams) and stream #3 replied to the AJAX request. Nice.

For a basic implementation, I am satisfied with stopping there. Before stopping for the night, however, I would like to answer a question from last night's work. Namely, is it possible to reply with different push data depending on the resource being requested? That is, if a client requests the home page, can I send certain resources back via push stream, but if the client requests a members-only page, can I send back separate push data?

The answer is yes, but the route is a little indirect. The Response class, being properly encapsulated with low coupling, has no knowledge of the request:
var Response = exports.Response = function(cframe, c) {
stream.Stream.call(this);
this.cframe = cframe;
this.streamID = cframe.data.streamID;
this.c = c;

this.statusCode = 200;
this._headers = {
'Connection': 'keep-alive'
};
this._written = false;
this._reasonPhrase = 'OK';
this._push = function() {};

// For stream.pipe and others
this.writable = true;
};
It does have access to that cframe thingy. And it turns out that the cframe thingy is the SPDY control frame of the original request. So I can dig down into that object in order to only push data when requesting the homepage with a GET request:
  push: function(pusher) {
//if (pusher.streamID > 1) return;
var req = pusher.cframe.data.nameValues;
if (req.url != "/") return;
if (req.method != "GET") return;

pusher.push_file("pub/style.css", "https://localhost:8082/style.css");
pusher.push_file("pub/spdy.jpg", "https://localhost:8082/spdy.jpg");
}
It would be nicer to have a solution that involved fewer dots, but I will leave that for another day. For now, I think I have a pretty decent SPDY server push implementation.


Day #44

No comments:

Post a Comment