httpプロトコルにおけるブロック転送符号化(Chunked transfer encoding)の紹介


最近、同僚が作成したサービスを呼び出すサービスを作成する必要があります.製品のユーザー数が多いため、バックエンドに複数のリクエストを同時に発行して処理する必要があります.サービス全体は、次のようなプロセスです.
 
接続を確立し、命令を書き、データを読み取り、操作を終了します.
 
バックエンドはキャッシュ、DBを操作する必要があるため、処理時間が長い場合があります.この処理方法は生まれながらにしてepollを使って処理するのに適しています(ここでもう一つの原因は、「epoll」はいつも同僚のG点をスタンプすることができるので、これはいったい何なのか試してみます)、pythonを使ってクライアントを書いて、処理すると発見性が指をさすほどよくなります.
 
そのヒントを得て、epollを使用してバックエンドサービスにインタフェースを送信するhelperクラスを書くつもりです.そうすれば、キューを処理したり、他のサービスにリクエストを送信したりするために、1つ(最大2つ)のプロセスしか起動しません.しかしこのときurllib 2を便利に使うことができないので、httpの頭を自分で書く必要があります.最初は順調に進んだが、後で問題にぶつかった.socket readがいつ終わるか分からない.
 
SOcket readがいつ終わるか分からないのは、バックエンドサービスで2つのhttpリターンに出会ったからです.1つの戻りヘッダは、次のとおりです.
と書く
< HTTP/1.1 200 OK
< Date: Mon, 07 Oct 2013 12:00:52 GMT
< Server: Apache/2.2.16 (Debian)
< X-Powered-By: PHP/5.3.3-7+squeeze17
< Vary: Accept-Encoding
< Content-Length: 51
< Content-Type: text/html
 の中 Content-Lengthは、返されるbodyの体積がどれくらいなのかを示しているので、この値を覚えておけば、対応する長さの内容を読むとsocketを閉じることができます.この処理方式は比較的簡単である.
 
しかし、私はもう一つの頭に出会った.
と書く
< HTTP/1.1 200 OK
< Server: nginx/1.0.5
< Date: Mon, 07 Oct 2013 12:03:12 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/5.3.6-13ubuntu3.10
 中にはありません 」,代わりに「Transfer-Encoding:chunked」を使って、RFC(http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html)3.6.1節では、「Transfer-Encoding:chunked」の場合、bodyがどのように構成されているかを詳細に定義しています.wiki(http://zh.wikipedia.org/wiki/%E5%88%86%E5%9D%97%E4%BC%A0%E8%BE%93%E7%BC%96%E7%A0%81)の中の言い方は、こうです. 
 
と書く
通常、HTTP応答メッセージで送信されるデータは送信全体であり、Content-Longthメッセージヘッダフィールドは、データの長さを表す.クライアントは、応答メッセージの終了と、その後の応答メッセージの開始とを知る必要があるため、データの長さは重要である.しかしながら、ブロック転送符号化を用いて、データは一連のデータブロックに分解され、1つ以上のブロックで送信される.サンプルサーバは、送信内容の合計サイズを予め知る必要がなく、データを送信することができる.通常、データブロックのサイズは一致するが、必ずしもそうではない.
 
簡単に言えば、bodyのデータは1つ1つで、各ブロックの先頭には現在のブロックのサイズが独立して表示されています.長さ0の「last-chunk」に出会った後、データ転送が終了したことを示します(原文は: The chunked encoding is ended by any chunk whose size is zero).
 
もともと私は自分でコードを書いてこの部分の解析を完成する必要があったが、urllib 2の実現の中で、また関連して返されたコードを処理すべきだと思って、pythonのソースコードの中で、Lib/http plib.pyの下で対応する実現を見つけた. HTTPResponseのもう一つの方法は「_read_chunked」と呼ばれ、50行のコードで対応する作業を完了しました.たぶんこのようなものです.
    def _read_chunked(self, amt):
        assert self.chunked != _UNKNOWN
        chunk_left = self.chunk_left
        value = []
        while True:
            if chunk_left is None:
                line = self.fp.readline(_MAXLINE + 1)
                if len(line) > _MAXLINE:
                    raise LineTooLong("chunk size")
                i = line.find(';')
                if i >= 0:
                    line = line[:i] # strip chunk-extensions
                try:
                    chunk_left = int(line, 16)
                except ValueError:
                    # close the connection as protocol synchronisation is
                    # probably lost
                    self.close()
                    raise IncompleteRead(''.join(value))
                if chunk_left == 0:
                    break
            if amt is None:
                value.append(self._safe_read(chunk_left))
            elif amt < chunk_left:
                value.append(self._safe_read(amt))
                self.chunk_left = chunk_left - amt
                return ''.join(value)
            elif amt == chunk_left:
                value.append(self._safe_read(amt))
                self._safe_read(2)  # toss the CRLF at the end of the chunk
                self.chunk_left = None
                return ''.join(value)
            else:
                value.append(self._safe_read(chunk_left))
                amt -= chunk_left

            # we read the whole chunk, get another
            self._safe_read(2)      # toss the CRLF at the end of the chunk
            chunk_left = None

        # read and discard trailer up to the CRLF terminator
        ### note: we shouldn't have any trailers!
        while True:
            line = self.fp.readline(_MAXLINE + 1)
            if len(line) > _MAXLINE:
                raise LineTooLong("trailer line")
            if not line:
                # a vanishingly small number of sites EOF without
                # sending the trailer
                break
            if line == '\r
': break # we read everything; close the "file" self.close() return ''.join(value)

 
簡単に言えばreadlineを使ってsocketからbodyを読み込んで、
 
  • 現在がブロックの開始である場合、現在の行を用いてブロックサイズ
  • を算出する.
  • そうでない場合、ブロックの内容のように読み、現在のブロックの体積を0
  • に減少する.
  • サイズが0のブロックであれば、コンテンツ受信完了
  • を示す.
     
    rfcは厳密に書かれていますが、難解です.他のライブラリのソースコードを調べるのは問題を理解しやすい方法です.
     
    検索のついでにcurlの小さな機能を見つけました.
     
    ページの内容を取得したい場合は、「curl「url」を使用します.
    リクエスト、responseのヘッダを見たら、「-v」パラメータを付けることができます.
    送信、受信の各バイトに何が入っているかを見たい場合は、「--trace、--trace-ascii」を加えることができます.