Documentation

§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 {
  Result(
    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)    
    
  Result(
    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)    
    
  Result(
    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)

ここでは、Ok.chunked を使ってこれらのデータをストリームすることができます。

def index = Action {
  val data = getDataStream
  val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
  
  Ok.chunked(dataContent)
}

次のように、 チャンクに分割されたデータを返す際にも、あらゆる Enumerator が利用できます。

def index = Action {
  Ok.chunked(
    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 つのチャンクと、レスポンスを閉じる最後の空のチャンクを受信することができました。

Next: Comet ソケット


このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。