Saturday, May 7, 2011

SETTINGS Control Frame in the SPDY Gem

‹prev | My Chain | next›

Tonight I am back to SPDY gem land. Specifically, I would like to get the remaining SPDY control frames into the gem. Currently supported are:
rspec ./spec/protocol_spec.rb -cfs

SPDY::Protocol
NV
SYN_STREAM
SYN_REPLY
PING
DATA
RST_STREAM
I love me some good executable documentation.

According to draft #2 of the SPDY protocol, the supported control frames are:
2.7 Control frames
2.7.1 SYN_STREAM
2.7.2 SYN_REPLY
2.7.3 RST_STREAM
2.7.4 SETTINGS
2.7.5 NOOP
2.7.6 PING
2.7.7 GOAWAY
2.7.8 HEADERS
2.7.9 WINDOW_UPDATE
First up, a bit of housekeeping. The spec order might as well follow the order laid out in the draft protocol. Also the DATA frames are separate from the control frames. Something like this should do:
SPDY::Protocol
data frames
DATA
control frames
SYN_STREAM
SYN_REPLY
RST_STREAM
PING
NV
Nice. The name-value frames are not technically control frames themselves, but are embedded in SYN_STREAM, SYN_REPLY, and HEADERS frames, so it makes sense to include its specification in there (that is where the draft protocol defines them).

So I add describe blocks to spec/protocol_spec.rb for the missing sections:
    describe "SETTINGS"
describe "NOOP" do
specify "not implemented (being dropped from protocol)" do
# NOOP
end
end

describe "PING" do
# ... already done
end

describe "GOAWAY"
describe "HEADERS"
I mark the NOOP frame as not implemented / won't implement. Per the notes for the upcoming draft 3 of the SPDY protocol, NOOP is going away, so there is not much sense in working on adding support for it.

That leaves 3 control frames that need to be supported. First up is the SETTINGS frame. SETTINGS are somewhat similar to HTTP cookies in normal HTTP. They store data and can persist. Just like with cookies, the server can set a value in the client and request the client resend it with every request.

SETTINGS differ from cookies in that there are only 5 preset values currently allowed (integer ID): SETTINGS_UPLOAD_BANDWIDTH (1), SETTINGS_DOWNLOAD_BANDWIDTH (2), SETTINGS_ROUND_TRIP_TIME (3), SETTINGS_MAX_CONCURRENT_STREAMS (4), and SETTINGS_CURRENT_CWND (5). It is meant to be extensible in the future, but for now, it is just those 5 possible settings.

From the draft, the packet looks like:
  +----------------------------------+
|1| 1 | 4 |
+----------------------------------+
| Flags (8) | Length (24 bits) |
+----------------------------------+
| Number of entries |
+----------------------------------+
| ID/Value Pairs |
| ... |
In RSpec form, that ought to read something like:
   SETTINGS
the assembled packet
starts with a control bit (PENDING: Not Yet Implemented)
followed by the version (2) (PENDING: Not Yet Implemented)
followed by the type (4) (PENDING: Not Yet Implemented)
followed by flags (PENDING: Not Yet Implemented)
followed by the length (24 bits) (PENDING: Not Yet Implemented)
followed by the number of entries (32 bits) (PENDING: Not Yet Implemented)
followed by ID/Value Pairs (PENDING: Not Yet Implemented)
The API for a settings beasty ought to take a hash argument with one or more of the five symbols as keys. For example, if I wanted to indicate the round trip time as 300 milliseconds, I might craft the packet as:
          @settings = SPDY::Protocol::Control::Settings.new
@settings.create(
:settings_round_trip_time => 300
)
Thus, to inspect the bytes in the produced frame, my setup block might look like:
    describe "SETTINGS" do
describe "the assembled packet" do
before do
@settings = SPDY::Protocol::Control::Settings.new
@settings.create(
:settings_round_trip_time => 300
)
@frame = Array(@settings.to_binary_s.bytes)
end
end
The first spec for the frame is that it needs to start with a control bit:
        specify "starts with a control bit" do
@frame[0].should == 128
end
I can make that pass with:
      class Settings < BinData::Record
bit1 :frame, :initial_value => CONTROL_BIT

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

def create(opts = {})
self
end
end
Most of the next specs are more or less copies of similar fields in other packets. The length fields requires a bit of noodling... actually not too much. The number of bytes after the length field is determined entirely by the ID/Value pairs. Since IDs are always 32 bits and values are 32 bits as well, the number of bytes is number of entries * (4 bytes + 4 bytes). Since my setup block has one entry, I should expect a length of 8. Similarly, I should expect the length (the number of entries) to be 1:
    describe "SETTINGS" do
describe "the assembled packet" do
before do
@settings = SPDY::Protocol::Control::Settings.new
@settings.create(
:settings_round_trip_time => 300
)
@frame = Array(@settings.to_binary_s.bytes)
end
# ...
specify "followed by the length (24 bits)" do
@frame[5..7].should == [0,0,8]
end
specify "followed by the number of entries (32 bits)" do
@frame[8..11].should == [0,0,0,1]
end
end
end
To implement, I can modify the create method to calculate the length and size:
        def create(opts = {})
self.len = opts.size * 8
self.number_of_entries = opts.size
self
end
With that passing, I can refactor to DRY up the length and size calculations, making use of Bindata's lambdas:
      class Settings < BinData::Record
bit1 :frame, :initial_value => CONTROL_BIT
bit15 :version, :initial_value => VERSION
bit16 :type, :value => 4

bit8 :flags, :value => 0
bit24 :len, :value => lambda { number_of_entries * 8 }
bit32 :number_of_entries

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

def create(opts = {})
self.number_of_entries = opts.size
self
end

end
With that, I am down to one more spec in need of implementation:
SPDY::Protocol
data frames
SETTINGS
the assembled packet
starts with a control bit
followed by the version (2)
followed by the type (4)
followed by flags
followed by the length (24 bits)
followed by the number of entries (32 bits)
followed by ID/Value Pairs (PENDING: Not Yet Implemented)
Given that I am creating a RTT of 300 milliseconds ID/Value pair, I should expect the ID/Value frames to be:
        specify "followed by ID/Value Pairs" do
@frame[12..19].should == [0,0,0,3, 0,0,1,44]
end
The value 3 corresponds to SETTINGS_ROUND_TRIP_TIME, 144 is 300 milliseconds in byte format (300 = 0000 0001 0010 1100 == 1 44). At first, the spec fails because the implementation so far defines nothing after the number of entries. I change the failure message by defining the ID/Value pairs as:
        array :headers, :initial_length => :number_of_entries do
bit32 :id
bit32 :data
end
And to get the spec to pass, I iterate through the options passed into create, look up the corresponding constant value and add to the ID/Value headers:
        def create(opts = {})
self.number_of_entries = opts.size
opts.each do |k, v|
key = SPDY::Protocol.const_get(k.to_s.upcase)
self.headers << { :id => key , :data => v }
end
self
end
With that, I have completed SETTINGS support in the SPDY gem. Now that I am in back in the flow of this, hopefully I can knock out GOAWAY and HEADERS tomorrow.


Day #13

No comments:

Post a Comment