ぬけラボ

φ(..)メモメモ

HTTP1.1のTransfer-Encoding: chunkedを試してみる

RFC2616 Transfer-Encoding: chunked を試すために、
curlとかnetcatとかrubyで色々やってみます。

仕様

とりあえず仕様を確認してみます。

Chunked-Body   = *chunk
                 last-chunk
                 trailer
                 CRLF

chunk          = chunk-size [ chunk-extension ] CRLF
                 chunk-data CRLF
chunk-size     = 1*HEX
last-chunk     = 1*("0") [ chunk-extension ] CRLF

chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
chunk-ext-name = token
chunk-ext-val  = token | quoted-string
chunk-data     = chunk-size(OCTET)
trailer        = *(entity-header CRLF)

curl

curlからデータを送ってみます。

server

netcatでサーバを起動

$ nc -l 8888
curl client

headerにchunkedやkeepaliveの情報を付加すればchunkedになると聞いてやってみます。

$ vim hoge.txt
hogehoge
fugafuga

$ curl -X POST -T "hoge.txt" -H 'Content-type: text/plain' --header "Transfer-Encoding: chunked" --header "Connection: Keep-Alive" http://localhost:8888/resource/test
request

server側にrequestが来ました。
bodyに16進数でsizeが入っています。0CRLFCRLFで1chunkと見るようです。

ただ、bodyの後に余計にCRLFが1つ入ってしまっています。仕様を見た限りでは必要無さそうです。
また、host.txtの内容を増やしていくと、どこかのタイミングでchunkが分割されるようです。

POST /resource/test HTTP/1.1
User-Agent: curl/7.28.0
Host: localhost:8888
Accept: */*
Content-type: text/plain
Transfer-Encoding: chunked
Connection: Keep-Alive
Expect: 100-continue

12
hogehoge
fugafuga

0

netcat

clientにもnetcatを使ってみます。

server

netcatでサーバを起動

$ nc -l 8888
client

netcatクライアント
curlのrequestを真似しました。

$ host=localhost
$ port=8888
$ body=hogehoge\\nfugafuga
$ size=`expr ${#body} | xargs -I{} printf '%x\n' {}`
$ echo "POST /resource/test HTTP/1.1\r\nHost: ${host}:${port}\r\nContent-type: text/plain\r\nTransfer-Encoding: chunked\r\nConnection: Keep-Alive\r\nExpect: 100-continue\r\n\r\n${size}\r\n${body}\r\n0\r\n\r\n" | nc $host $port
request

server側にrequestが来ました。
netcatでは、標準入力が閉じられるとコネクションが閉じられるようです。

POST /resource/test HTTP/1.1
Host: localhost:8888
Content-type: text/plain
Transfer-Encoding: chunked
Connection: Keep-Alive
Expect: 100-continue

12
hogehoge
fugafuga
0

ruby

最後にrubyからもデータを送ってみます。
といってもクライアントはnetcatをpipeで開いて送っています。

tcp server

request内容を出力するTCP Server

$ vim tcpserver.rb
require "socket"

c = TCPServer.new(7777)

while true
  sock = c.accept
  while buf = sock.gets
    puts buf
  end
  sock.close
end

c.close

$ ruby tcpserver.rb
client

Rubyからnetcatをパイプで開いてデータを送ってみます。
1度headerを投げたあとに10回bodyを送ってコネクションを切ります。

require 'open3'

class ChunkTest
  def initialize(host, port)
    @host = host
    @port = port
    @cmd = "nc"
  end

  def run
    Open3.popen3("#{@cmd} #{@host} #{@port}") do |stdin, stdout, stderr, wait_thr|
      begin
        i = 0
        loop do
          body = "hogehoge#{i}\nfugafuga#{i}"
          size = body.size
          head = \
            "POST /resource/test HTTP/1.1\r\nUser-Agent: curl/7.28.0\r\nHost: #{@host}:#{@port}\r\nContent-type: text/plain\r\nTransfer-Encoding: chunked\r\nConnection: Keep-Alive\r\nExpect: 100-continue\r\n\r\n"
          if i == 0
            stdin << head
          elsif i > 10
            stdin << "0\r\n\r\n"
            break
          else
            chunk = "#{size.to_s(16)}\r\n#{body}\r\n"
            stdin << chunk
          end
          i += 1
        end
      ensure
        stdin.close
      end
    end
  end
end

chunk = ChunkTest.new('localhost', 8888)
chunk.run
request
POST /resource/test HTTP/1.1
User-Agent: curl/7.28.0     
Host: localhost:8888       
Content-type: text/plain    
Transfer-Encoding: chunked  
Connection: Keep-Alive      
Expect: 100-continue        
                            
13                          
hogehoge1                   
fugafuga1                   
13                          
hogehoge2                   
fugafuga2                   
13                          
hogehoge3                   
fugafuga3                   
13                          
hogehoge4                   
fugafuga4                   
13                          
hogehoge5                   
fugafuga5                   
13                          
hogehoge6                   
fugafuga6                   
13                          
hogehoge7                   
fugafuga7                   
13                          
hogehoge8                   
fugafuga8                   
13                          
hogehoge9                   
fugafuga9                   
15                          
hogehoge10                  
fugafuga10   
0    
           
chunked server

以上の実験でchunkedでのデータのやり取りが分かってきました。
サーバーにcool.ioを使用してchunkedなrequestを処理してみます。
受け取ったrequest bodyを画面出力します。

require 'cool.io'
require 'http_parser'

ADDR = '127.0.0.1'
PORT = 8888

class EchoServerConnection < Cool.io::TCPSocket
  def on_connect
    puts "#{remote_addr}:#{remote_port} connected"
    @parser = Http::Parser.new(self)
  end

   def on_message_begin
    @headers = nil
  end

  def on_close
    puts "#{remote_addr}:#{remote_port} disconnected"
  end

  def on_headers_complete(headers)
    @headers = headers
  end

  def on_read(data)
    @parser << data
  end

  def on_body(data)
    puts data
  end
 end

event_loop = Cool.io::Loop.default
Cool.io::TCPServer.new(ADDR, PORT, EchoServerConnection).attach(event_loop)

puts "Echo server listening on #{ADDR}:#{PORT}"
event_loop.run
request

無事にbodyを取り出せました。

$ ruby coolio.rb
Echo server listening on 127.0.0.1:8888
127.0.0.1:49895 connected
hogehoge1
fugafuga1
hogehoge2
fugafuga2
hogehoge3
fugafuga3
hogehoge4
fugafuga4
hogehoge5
fugafuga5
hogehoge6
fugafuga6
hogehoge7
fugafuga7
hogehoge8
fugafuga8
hogehoge9
fugafuga9
hogehoge10
fugafuga10
127.0.0.1:49895 disconnected

終わりに

HTTP1.1のTransfer-Encoding: chunkedがなかなか分からず苦労しました。
間違っている部分があれば、ご指摘ご助言等頂けると幸いです。