Thursday, May 26, 2011

Spiking SPDY Server Push

‹prev | My Chain | next›

Tonight I continue to explore server push in SPDY.

First I define a very skeleton class for a push stream in node-spdy:
/**
* Initiate SYN_STREAM
*/
Response.prototype.writeHead = function(code, reasonPhrase, headers) {
// TODO: I *think* this should only be built inside this class
throw new Error("PushStream#writeHead invoked externally");
};

/**
* Write data
*/
Response.prototype.write = function(data, encoding) {
return this._write(data, encoding, false);
};

/**
* Write any data (Internal)
*/
Response.prototype._write = function(data, encoding, fin) {
throw new Error("Please implement");
};

/**
* End stream
*/
Response.prototype.end = function(data, encoding) {
this.writable = false;
return this._write(data, encoding, true);
};

/**
* Mirroring node.js default API
*/
Response.prototype.setHeader = function(name, value) {
throw new Error("Not implemented for push stream");
};

/**
* Mirroring node.js default API
*/
Response.prototype.getHeader = function(name) {
throw new Error("Not implemented for push stream");
};

/**
* Cloning node.js default API
*/
Response.prototype.removeHeader = function(name) {
throw new Error("Not implemented for push stream");
};
As indicated by the throw statements, I am an fairly certain that most of what was required for the Response class will not be needed in PushStream. Ideally just writing back the data.

Then I start to noodle things through a bit and...

But wait a second, the write method in Response is only sending out data:
Response.prototype._write = function(data, encoding, fin) {
if (!this._written) {
this._flushHead();
}
encoding = encoding || 'utf8';

if (data === undefined) {
data = new Buffer(0);
}

var dframe = createDataFrame(this.c.zlib, {
streamID: this.streamID,
flags: fin ? enums.DATA_FLAG_FIN : 0,
}, Buffer.isBuffer(data) ? data : new Buffer(data, encoding));

return this.c.write(dframe);

};
A reply to a HTTP request coming in over a SYN_STREAM needs both a SYN_REPLY and a DATA response. The write() method in Response is only sending the DATA frame. What about the SYN_REPLY?

Ah. I get it now. The Response class is responsible for sending out both the header and the data frame.

So I create a private _flushHead() method in PushStream (based on the method of the same name in Response):
/**
* Flush buffered head
*/
Response.prototype._flushHead = function() {
var headers = this._headers;

var cframe = createControlFrame(this.c.zlib, {
type: enums.SYN_STREAM,
flags: enums.FLAG_UNIDIRECTIONAL,
streamID: this.streamID,
assocStreamID: this.associatedStreamId
}, headers);

return this.c.write(cframe);
};
Included here is the type, which is a SYN_STREAM. The Response class had been returning a SYN_REPLY in response to an already open SYN_STREAM (e.g. one that requested the homepage). Here I am trying to initiate a server push, hence the new SYN_STREAM. Also in there is the unidirectional flags as required by SPDY server push (there will not be a back-and-forth here, just push from the server). The new stream ID and the associated ID are set in the constructor.

(Actually, after double-checking in the enums.js file, I find that it is CONTROL_FLAG_UNIDIRECTIONAL)

Another requirement of the server push spec is that the URL is set in the SYN_STREAM. Like the stream ID and associated stream ID, the headers are currently being assigned in the constructor. For this spike, I will explicitly set the URL to the stylesheet being used. If this works, the stylesheet will make its way into the browser's cache—ultimately preventing the browser from needing to request it. So, the header:
var PushStream = exports.PushStream = function(cframe, c) {
stream.Stream.call(this);
this.streamID = 2; // TODO auto-increment even numbers per: http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft2#TOC-Stream-creation

this.associatedStreamId = cframe.data.streamID;
this.c = c;

this._headers = {
url: "https://localhost:8081/style.css"
};
};
I think this is starting to come together. Next up, is sending the actual stylesheet data. For that, I copy the write method directly from Response:
/**
* Write any data (Internal)
*/
Response.prototype._write = function(data, encoding, fin) {
if (!this._written) {
this._flushHead();
}
encoding = encoding || 'utf8';

if (data === undefined) {
data = new Buffer(0);
}

var dframe = createDataFrame(this.c.zlib, {
streamID: this.streamID,
flags: fin ? enums.DATA_FLAG_FIN : 0,
}, Buffer.isBuffer(data) ? data : new Buffer(data, encoding));

return this.c.write(dframe);
};
This method writes the just crafted header (unless it has already done so). Then it builds the DATA frame and writes it back to the connect stream.

Last up tonight it doing a little work back in the Response class. The server push will be initiated back in the Response class because server push needs an associated stream ID in order to be valid. With that in mind, I define the previously stubbed Reponse#createPushStream method:
/**
* Server push
*/
Response.prototype.createPushStream = function() {
return new PushStream(this.cframe, this.c)
};
The control frame (from the original browser request) will be sufficient to determine the associated stream ID and the connect stream will provide the transport for the server push.

Tomorrow I will add the actual data and (without a doubt) perform quite a bit of debugging because I have really played fast and loose with this code tonight as I simple tried to wrap my brain around it. Even so, I am pleased with the progress tonight. I feel as though I am close to having a workable server push in SPDY.


Day #31

No comments:

Post a Comment