Thursday, April 28, 2011

RSpec Driven PING Packets

‹prev | My Chain | next›

With a bit of experience assembling SPDY frame with the SPDY gem, tonight I will attempt to build additional frames.

Currently, the gem is capable of SYN_STREAM and SYN_REPLY control frames. The other frames in Draft #2 of the SPDY protocol include: RST_STREAM, SETTINGS, NOOP, PING, GOAWAY, HEADERS, and WINDOW_UPDATE.

I start with an easy (but potentially useful) one: PING. The PING frame looks like:
  +----------------------------------+
|1| 1 | 6 |
+----------------------------------+
| 0 (flags) | 4 (length) |
+----------------------------------|
| 32-bit ID |
+----------------------------------+
Or, in RSpec format:
  context "PING" do
describe "the assembled packet" do
specify "starts with a control bit"
specify "followed by the version (1)"
specify "followed by the type (6)"
specify "followed by flags (0)"
specify "followed by the length (always 4)"
end
end
The definition of those specify statements can be as follows:
  context "PING" do
describe "the assembled packet" do
before do
@ping = SPDY::Protocol::Control::Ping.new
@ping.create(:stream_id => 1)
@frame = @ping.to_binary_s
end
specify "starts with a control bit" do
@frame[0].should == "\x80"
end
specify "followed by the version (1)" do
@frame[1].should == "\x01"
end
specify "followed by the type (6)" do
@frame[2..3].should == "\x00\x06"
end
specify "followed by flags (0)" do
@frame[4].should == "\x00"
end
specify "followed by the length (always 4)" do
@frame[5..7].should == "\x00\x00\x04"
end
end
end
The first expectation is a bit of a cheat. The first bit is a control bit (which is on), but I am testing the resulting first two bytes (1000 0000 == 128 = \x80). I need to come up with a better scheme for that (maybe just rely on bindata's implementation?), but that will suffice to drive implementation today. The remaining expectations are straight forward.

Implementing those specs is similarly straight forward (largely thanks to the Bindata gem):
      class Ping < BinData::Record
bit1 :frame, :initial_value => CONTROL_BIT
bit15 :version, :initial_value => VERSION
bit16 :type, :value => 6

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

bit32 :stream_id

def create(opts = {})
self.stream_id = opts.fetch(:stream_id, 1)
self
end
end
Unfortunately, I cannot use the Header class already in the SPDY gem. It expects the stream ID to be 31 bits. Oddly, the specification for PING packets is 32 bits (all other control frames are 31). Similarly, I cannot use the Helpers mixin, both because it is coupled with the Header implementation and because it expects a data section (no data in a PING). Regardless, the Bindata gem makes for a very clean implementation.

As for parsing a PING:
  context "PING" do
it "can parse a PING packet" do
ping = SPDY::Protocol::Control::Ping.new
ping.parse(PING)

ping.stream_id.should == 1
ping.type.should == 6

ping.to_binary_s.should == PING
end
Where the PING constant is defined as:
PING = "\x80\x01\x00\x06\x00\x00\x00\x04\x00\x00\x00\x01"
The implementation is trivial (again, thanks to Bindata):
        def parse(chunk)
self.read(chunk)
self
end
With that, I have a nice, executable specification for the SPDY PING packet:
➜  spdy git:(master) rspec ./spec/protocol_spec.rb:149 -cfs
Run filtered using {:line_number=>149}

SPDY::Protocol
PING
can parse a PING packet
the assembled packet
starts with a control bit
followed by the version (1)
followed by the type (6)
followed by flags (0)
followed by the length (always 4)

Finished in 0.00509 seconds
6 examples, 0 failures
I spend a little time knocking out the RST_STREAM frame and then call it a night.

Day #4

No comments:

Post a Comment