§HTTP レスポンスのストリーミング
§標準的なレスポンスと Content-Length ヘッダ
HTTP 1.1 以降、複数の HTTP リクエストとレスポンスにまたがって単一のコネクションを使いまわすためには、サーバはレスポンスと一緒に適切な Content-Length
HTTP ヘッダを送信する必要があります。
デフォルトでは、SimpleResult を返却する際、次のように Content-Length
ヘッダを省略することができます。
def index = Action {
Ok("Hello World")
}
この場合、送信しようとしているコンテンツが明らかなため、Play はコンテンツサイズを自動的に計算して、適切なヘッダを生成することができます。
文字列をバイト列に変換するために使われたエンコーディングにしたがって
Content-Length
をヘッダを計算しなければならないので、このようなテキストベースのコンテンツを返すことは見た目ほど単純なことではないことに 注意 してください。
ところで、以前、レスポンスボディは play.api.libs.iteratee.Enumerator
を用いて指定されていることを説明しました。
def index = Action {
SimpleResult(
header = ResponseHeader(200),
body = Enumerator("Hello World")
)
}
これは、Content-Length
ヘッダを適切に計算するために、 Play は enumerator 全体を消費し、そのコンテンツを全てメモリにロードしなければならないことを意味します。
§大量のデータ送信
シンプルな enumerator であればメモリに全て読み込んで処理しても問題ありませんが、データセットが巨大な場合はどうでしょうか?例えば、大きなファイルを web クライアントへ送り返すような場合です。
まず、ファイルのコンテンツを読む Enumerator[Array[Byte]]
を定義してみましょう。
val file = new java.io.File("/tmp/fileToServe.pdf")
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)
シンプルですね? 次に、この enumerator を使って、レスポンスボディを指定します。
def index = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)
SimpleResult(
header = ResponseHeader(200),
body = fileContent
)
}
問題はここです。Content-Length
ヘッダを指定していないので、 Play 自身がこれを計算しなければならず、これには enumerator のコンテンツを全て消費してメモリにロードし、レスポンスサイズを計算するしか方法がありません。
これは、メモリにロードさせたくないくらい大きなファイルの場合に問題があります。これを避けるためには、Content-Length
ヘッダを自分で指定する必要があります。
def index = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)
SimpleResult(
header = ResponseHeader(200, Map(CONTENT_LENGTH -> file.length.toString)),
body = fileContent
)
}
こうすると、Play は body enumerator の内容をチャンクに分割して少しずつ読み込み、読み込んだチャンクを随侍 HTTP レスポンスにコピーします。
§ファイルの送信
もちろん、Play には、ローカルファイルを送信するヘルパが用意されています。
def index = Action {
Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}
このヘルパはファイル名から Content-Type
ヘッダを計算してくれます。さらに、 web ブラウザがどのようにこのレスポンスを取り扱うべきかを指定する Content-Disposition
ヘッダも追加してくれます。デフォルトでは、Content-Disposition: attahment; filename=fileToServe.pdf
という指定により、 web ブラウザはこのファイルのダウンロードをするかどうかをユーザに確認します。
次のようにすると、ファイル名を指定することもできます。
def index = Action {
Ok.sendFile(
content = new java.io.File("/tmp/fileToServe.pdf"),
fileName = _ => "termsOfService.pdf"
)
}
ファイルを inline
で送信したい場合は、次のようにします。
def index = Action {
Ok.sendFile(
content = new java.io.File("/tmp/fileToServe.pdf"),
inline = true
)
}
inline
の場合、 web ブラウザはファイルをダウンロードするのではなく、単にファイルの内容を web ブラウザのウインドウ内に表示するたけなので、ファイル名を指定する必要はありません。これは、テキストや HTML、画像など、Web ブラウザがネイティブ対応しているコンテンツ・タイプのファイルを送信する場合に便利です。
§チャンクレスポンス
今のところ、ストリーミングを始める前にコンテンツの長さを計算することができるため、うまくファイルの内容をストリーミングすることができていました。しかし、コンテンツのサイズが事前にわからないような動的に生成されるコンテンツをストリーミングする場合はどうでしょうか?
このような種類のレスポンスを返すためには、 チャンク転送エンコーディング を利用します。
チャンク転送エンコーディング は HTTP 1.1 で定義されているデータ転送メカニズムの一つで、 web サーバがコンテンツをいくつかのチャンクに分けて送信する、というものです。このレスポンスを送信するためには
Content-Length
ヘッダの代わりにTransfer-Encoding
HTTP レスポンスヘッダを使います。Content-Length
ヘッダがないので、サーバはレスポンスをクライアント(通常は web ブラウザ)へ送信し始める前にコンテンツの長さを知る必要はありません。つまり、 web サーバは動的に生成されるコンテンツの最終的な長さを知ることなく、レスポンスを送り始めることができます。各チャンクの長さはチャンクの内容の直前に送信されます。これによって、クライアントはチャンクの受信が終わったことを認識できます。最後に、長さがゼロのチャンクを送信すると、データ転送は完了です。
http://en.wikipedia.org/wiki/Chunked_transfer_encoding
この方式の利点は、データを ライブ に提供できる、つまり利用できるようになったデータを即座にチャンクとして送信できることです。欠点は、 web ブラウザがコンテンツサイズを知らないため、ダウンロードプログレスバーを正しく表示できないということです。
仮に、あるデータを動的に生成する InputStream
を提供するサービスがあるとします。まず、その InputStream
をラップする Enumerator
を作る必要があります。
val data = getDataStream
val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
ここでは、ChunkedResult
を使ってこれらのデータをストリームすることができます。
def index = Action {
val data = getDataStream
val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
ChunkedResult(
header = ResponseHeader(200),
chunks = dataContent
)
}
いつも通り、これをもっと簡単に記述するヘルパが用意されています。
def index = Action {
val data = getDataStream
val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
Ok.stream(dataContent)
}
次のように、 チャンクに分割されたデータを返す際にも、あらゆる Enumerator
が利用できます。
def index = Action {
Ok.stream(
Enumerator("kiki", "foo", "bar").andThen(Enumerator.eof)
)
}
サーバによって送信されたレスポンスを確認してみると、次のようになります。
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
4
kiki
3
foo
3
bar
0
3 つのチャンクと、レスポンスを閉じる最後の空のチャンクを受信することができました。
次ページ: Comet ソケット