mrubyでHTTP/1.1のKeep-Aliveで複数回リクエストを出してみた
サンプルが無い問題
そもそも、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。