読者です 読者をやめる 読者になる 読者になる

HTTPチャンク転送をPythonで受ける

Python

HTTP1.1には、「アプリケーションが明示的にちょっとづつデータを送る」ための、チャンク転送符号化という仕様があります。

ヘッダの項目について
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
転送方式そのものの説明
http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1

要するに、

Transfer-Encoding: chunked

というヘッダがあると、レスポンス本文は、

後続のバイト数 CRLF
データの実体... CRLF
後続のバイト数 CRLF
データの実体... CRLF

という感じになります。この形式でレスポンスが得られれば、クライアントがゆっくりとデータを受信するとき、バッファの大きさをあらかじめ確定できるので、受信の真っ最中に想定サイズを超えるような厄介な問題を防ぐことができ、また、データ形式が線形なものであれば、「受信したところまで表示」できるかもしれません。

ひとつのレスポンスが平気で10MB超えたりして、さらに、そんなリクエストを並列で10本以上走らせる、なんていう場合、ひとつのジョブが可変長バッファを物理メモリ上で10MB以上占有する(10MBをちょっと超えるだけで、メモリ再配置のために瞬間的に30MB占有する)なんてのは厳しいですね。まぁ、数KBごとに、バイト列をそのままディスクか別のソケットに送出できればいいんですが、前後関係に依存してデータ加工してから保存しないといけないなど、単純にたらい回しするわけにもいかない場合もあります。

前置きが長くなりました。というわけで、「いちどに処理していい小さな単位」として、HTTPのチャンクを活かせるなら、ぜひ活かしたいものです。が、残念なことに、Pythonのhttplibは、レスポンスがせっかくチャンク化されていても、通常のHTTPレスポンスと同じインターフェースで操作することを強制してしまいます。つまり、httplibを使うと、チャンクが連続した区切り目のないデータなってしまうというわけ。う〜ん、こんなふうにできたらいいのに。

response = connection.getresponse()
chunk = response.read_chunk() #できたらいいな
while chunk:
    process_for_unit(chunk)
    chunk = response.read_chunk() #できたらいいな

…と、思っていたら、なんと、そのまんまできるコードがありました。

http://projects.dowski.com/view/iresponse

HTTPResponseを継承した、__iter__を持つIterableResponseクラスだそうです。while-readじゃなくて、for-inで受信データを受け取れます。で、チャンク転送ならイテレーションの単位がチャンクになります。ナイス。これで、巨大なレスポンスでも、チャンク転送なら、随時処理してはメモリを片付けられる。モーションJPEGどんと来い。

あれ?このdowskiって名前、見たことあるぞ。と思ったら、CherryPyのバグレポのときお世話になった人でした。そうか、たしかにCherryPyはチャンク転送できるんだから、クライアント側でも対称な作りができて欲しい。でないと、CherryPyのストリーミング出力をPythonだけで確認できないもんね。