Sunday, May 8, 2011

GOAWAY and HEADERS in SPDY (the gem)

‹prev | My Chain | next›

The last two SPDY control frames needed in the SPDY gem are GOAWAY and HEADERS.

The GOAWAY control frame is used to indicate normal termination of a stream (as opposed to errors which are handled by RST_STREAM). It is meant as mechanism for informing the other end of communication that further communication on the current stream is no longer necessary (the example given in the draft is for a machine reboot).

From a structural point of view, according to the latest SPDY protocol draft (#2), the GOAWAY control frame:
  +----------------------------------+
|1| 1 | 7 |
+----------------------------------+
| 0 (flags) | 4 (length) |
+----------------------------------|
|X| Last-good-stream-ID (31 bits) |
+----------------------------------+
Is very similar to the PING frame already supported by the SPDY gem:
  +----------------------------------+
|1| 1 | 6 |
+----------------------------------+
| 0 (flags) | 4 (length) |
+----------------------------------|
| 32-bit ID |
+----------------------------------+
The only differences are the type (7 for GOAWAY and 6 for PING) and the last 32 bits of the frame. Stream IDs, such as the one in GOAWAY are 31 bits whereas PING IDs are 32 bits. In practical terms, I cannot reuse the entire implementation, but I can very nearly copy and paste the RSpec specs:
    describe "GOAWAY" do
describe "the assembled packet" do
before do
@goaway = SPDY::Protocol::Control::Goaway.new
@goaway.create(:stream_id => 42)
@frame = Array(@goaway.to_binary_s.bytes)
end
specify "starts with a control bit" do
@frame[0].should == 128
end
specify "followed by the version (2)" do
@frame[1].should == 2
end
specify "followed by the type (6)" do
@frame[2..3].should == [0,6]
end
specify "followed by flags (0)" do
@frame[4].should == 0
end
specify "followed by the length (always 4)" do
@frame[5..7].should == [0,0,4]
end
specify "followed by the last good stream ID (1 ignored bit + 31 bits)" do
@frame[8..11].should == [0,0,0,42]
end
end
end
Implementing that spec is darn near trivial thanks mostly to the magic of the Bindata gem:
      class Goaway < BinData::Record
hide :u1

bit1 :frame, :initial_value => CONTROL_BIT
bit15 :version, :initial_value => VERSION
bit16 :type, :value => 6

bit8 :flags, :value => 0
bit24 :len, :value => 4

bit1 :u1
bit31 :stream_id

def parse(chunk)
self.read(chunk)
self
end

def create(opts = {})
self.stream_id = opts.fetch(:stream_id, 1)
self
end
end
That's all there is to it.

Last, and by no means least, is the HEADERS control frame. HEADERS is used to send async name/value data back and forth (cookies, CGI params or POST data in vanilla HTTP). In the current draft of the protocol, this tends to happen just as much in SYN_STREAM and SYN_REPLY, but that will change in draft #3. In the very rough notes for the next draft, name/value pairs will only be included in HEADERS making this control frame quite important.

The packet diagram in version 2 of the draft spec:
  +----------------------------------+
|C| 1 | 8 |
+----------------------------------+
| Flags (8) | Length (24 bits) |
+----------------------------------+
|X| Stream-ID (31bits) |
+----------------------------------+
| Unused (16 bits) | |
|-------------------- |
| Name/value header block |
+----------------------------------+
Again, I can write the specs for that with relative ease:
    describe "HEADERS" do
it "can parse a HEADERS packet"
describe "the assembled packet" do
before do
@headers = SPDY::Protocol::Control::Headers.new
@headers.create(:stream_id => 42)
@frame = Array(@headers.to_binary_s.bytes)
end
specify "starts with a control bit" do
@frame[0].should == 128
end
specify "followed by the version (2)" do
@frame[1].should == 2
end
specify "followed by the type (8)" do
@frame[2..3].should == [0,8]
end
specify "followed by flags (8 bits)" do
@frame[4].should == 0
end
specify "followed by the length (24 bits)" do
# 4 bytes (stream ID)
# 2 bytes (unused)
# N bytes for compressed NV section
@frame[5..7].should == [0,0,53]
end
specify "followed by the stream ID (1 ignored bit + 31 bits)" do
@frame[8..11].should == [0,0,0,42]
end
specify "followed by 16 unused bits" do
@frame[12..13].should == [0,0]
end
specify "followed by name/value pairs"
end
end
(the length is know from compressing the same name/value pairs for other frames)

I can then implement the spec, re-using the confusingly named :header Bindata type:
      class Headers < BinData::Record
attr_accessor :uncompressed_data
include Helpers

header :header
bit16 :unused
string :data, :read_length => lambda { header.len - 6 }

def create(opts = {})
build({:type => 8, :len => 6}.merge(opts))
end
end
The :header type is defined in the SPDY gem. I think the name will have to change so that it does not get confused with the HEADERS control frame. The :header data type was merely meant to encapsulate common frame structures for the packets using name/value pairs. I will worry about the naming another day.

As for the last spec (the name/value block), it it sufficient to note the expected size of the compressed block (NV blocks are always compressed to save space):
        specify "followed by name/value pairs" do
@frame[14..-1].size.should == 47
end
The brilliant thing here is that the Helpers mixin already takes care of this for me. So I have the entire spec passing. For completeness' sake, I add a spec that parses a complete HEADERS frame, then re-assembles it so that the spec can verify that parse + build work as expected.

That is a good stopping point for the night. I think I will spend a bit of time refactoring tomorrow and then move on to some experiments with the various control frames.


Day #14

No comments:

Post a Comment