non vorrei lavorare

昔はおもにプログラミングやガジェット系、今は?

mrubyでHTTP/1.1のKeep-Aliveで複数回リクエストを出してみた

おはようございます。先週は次男が、熱を出し、今週は長男が熱を出しました。長男の熱は、寝不足とkindleの車のゲームのやり過ぎが原因のようですが。。@kjunichiです。

サンプルが無い問題

そもそも、HTTPクライアントの例がHTTP/1.0だったり、あってもHTTP/1.1でConnection: close指定という パターンに世の中は溢れていた。

KeepAliveにするも、2回目のリクエストの応答が来ない

s = TCPSocket.open("127.0.0.1", 8080)
s.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts s.read
puts "<----"
s.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts s.read
s.close

としたが、2回目のリクエストが貰えない。 当初、発行出来たのかも分からなかった。

そこで、自分が割とつかえるNode.jsでHTTPサーバーをサンプルコード 切り貼りで作成した。

//server.js
const http = require('http'); //httpモジュールのインポート
const server = http.createServer(); 

server.on('request', function(req, res) {
  console.log("onRequest");
  const myhtml = "<html><body><a href=\"/\">link</a></body></html>"
  res.writeHead(200, {'Content-Type': 'text/html',/*'Content-Length': myhtml.length,*/'Connection':'keep-alive'}); 
  res.write(myhtml);// resの中身を出力
  res.end();
})
server.on('connection', function(socket) {
  console.log("A new connection was made by a client.");
  socket.setTimeout(30 * 1000);
  // 30 second timeout. Change this as you see fit.
})
server.listen(8080,"0.0.0.0")

node.jsで作成したサーバー側を念のため、Firefoxでアクセスして、サーバー側はそれなりに動いていることを 確認

Socket#recv

一晩寝たら、CのAPIのrecvを思いだし、Socket#readの代わりにSocket#recvを使えば、行けそうなことが分かった。 Socket#readは接続がクローズされるまでブロックされるから、駄目だったのだ。

Transfer-Encodeing: chunked

node.jsで標準のモジュールでhttpサーバーを作ると、Connection: keep-aliveだと 応答のTransfer-Encodingがchunked指定で返されることが判明。

Content-Length方式なら、楽勝だったのに、まぁ、node.jsならこっちで返す方が、node.jsらしいけど。

chunkedな応答への対応

Transfer-Encodingがchunkedの場合、Body部が以下の形式で返される。

16進数で長さ\r\n
指定された長さの文字列\r\n

0\r\nの有無だけで駄目

いちいち、長さを見て云々より、最終行は0\r\nだから、これの有無を見つければ、良いかというと、 以下のパターンで破綻する。

1\r\n
0\r\n
10\r\n
0123456789abcdef\r\n
0\r\n

というパターンもあり、結局Body部の先頭から地道にパースするしかなさそう。

mrubyで16進数を10進数に変換

Integer("0xff")

でCRubyと同様に10進数に変換出来た。

h2oで確認

h2oの場合、静的コンテンツはContent-Length方式で応答が返された。もしかすると、 ファイルサイズによってはchunked方式で返されるのかもしてないが。。。

また、hhvmを動かし試したら、こちらはchunkedでnode.jsの時は0xffの形式だったが、 phpinfoで試したこともあり、0xffff形式で、返ってくることが分かった。

mrubyにHTTP::Parser

mrubyにHTTP::Parserがあることに気付くが、 \r\n\r\nまで取得できた状態で、パースすると、メソッドがGETに設定されて 返ってきて、結局自前である程度パースして、サーバーからの応答を取得後 こちらを使って、細かなヘッダー部のパースをお願いする使い方が吉なのか?

成果物

def readSomeChunkedData(s,data,body)
  chunks = data.split("\r\n")
  if chunks[0] == "0"
    return true
  else
    for idx in 0..chunks.size-1
      next if idx % 2 == 1
      return if chunks[idx] == "0"

      hex = "0x" + chunks[idx]
      readSize = Integer(hex)-chunks[idx+1].length
      chunks[idx+1] << s.recv(readSize+2) if readSize > 0
      body[0] << chunks[idx+1]
    end
  end
  return false
end

def readChunkedData(s,data)
  body=[""]
  until readSomeChunkedData(s,data,body)
    data = s.recv(1024)
  end
  body[0]
end

def readBody(s,data,hdr)
  if hdr[:isChunked]
    body = readChunkedData(s, data)
  else
    # Content-Length
    readSize = hdr[:contentLength] - data.length
    data << s.recv(readSize) if readSize >0
    body = data
  end
  body
end

def analyzeHeader(header)
  headers = header.split("\r\n")

  for h in headers do
    h.downcase!
    if h.index("content-length:") == 0
      contentLength = h.split(":")[1].to_i
    end
    if h.index("transfer-encoding:" ) == 0
      if h.index("chunked") >0
        isChunked = true
      end
    end
  end
  {:isChunked => isChunked, :contentLength => contentLength}
end

def readHeader(s, data)
  buf = s.recv(1024)
  data << buf
  if buf.index("\r\n\r\n") == nil
    readHeader(s,data)
  end
  data
end

def parse(s,data)

  if data.index("\r\n\r\n") == nil
    data = readHeader(s, data)
  end

  header = data.split("\r\n\r\n")
  p header
  rtn=analyzeHeader(header[0])
  if header.size  == 1
    body = readBody(s, s.recv(1024),rtn)
  else
    body = readBody(s,header[1],rtn)
  end
  body
end

s = TCPSocket.open("127.0.0.1", 8080)
s.write("POST /hhvm/test.php HTTP/1.1\r\nHost: localhost\r\nContent-Length: 3\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nA=b")
s.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts parse(s,s.recv(256))
puts "<----"
s.write("GET /hhvm/test.php HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
puts parse(s,s.recv(2))
s.close

まとめ

mrubyというか、mruby-socketでHTTP/1.1のKeep-aliveでのコネクションを維持して複数回リクエストをサーバに 投げることができた。

Keep-aliveだとサーバからの応答をそれなりにパースする必要があり、世の中にサンプルが見つからなかったのも、 まぁ、やってみてなんとなく分かった気がする。

Socket#gets的なものを実装した方が、良かった気もするが、それだと、recvの呼び出し回数が増えそうだったので、 やめた。 (\r\nがあるか。recvで確認して、無ければ、サイズ増やして、再度確認、見つかったら、そこまでrecvするといった 具合で、recvの呼び出し回数が増えるのではと)

もしかして、速すぎる最適ってやつかもw。

関連記事