ドキュメント

§ストリーミングHTTPレスポンス

§標準的なレスポンスとContent-Lengthヘッダー

HTTP 1.1以降、単一の接続を維持して複数のHTTPリクエストとレスポンスを提供するには、サーバーは適切なContent-Length HTTPヘッダーをレスポンスと共に送信する必要があります。

デフォルトでは、単純な結果(例:以下)を送信する際にContent-Lengthヘッダーを指定しません。

def index = Action {
  Ok("Hello World")
}

もちろん、送信するコンテンツが既知であるため、Playはコンテンツサイズを自動的に計算し、適切なヘッダーを生成できます。

注記:テキストベースのコンテンツの場合、文字をバイトに変換するために使用される文字エンコーディングに従ってContent-Lengthヘッダーを計算する必要があるため、見た目ほど単純ではありません。

実際、レスポンス本文はplay.api.http.HttpEntityを使用して指定されていることを既に説明しました。

def action = Action {
  Result(
    header = ResponseHeader(200, Map.empty),
    body = HttpEntity.Strict(ByteString("Hello world"), Some("text/plain"))
  )
}

つまり、Content-Lengthヘッダーを正しく計算するために、Playはコンテンツ全体を消費してメモリに読み込む必要があります。

§大量のデータの送信

コンテンツ全体をメモリに読み込むことに問題がない場合、大規模なデータセットはどうでしょうか?大きなファイルをウェブクライアントに返す必要があるとしましょう。

まず、ファイルコンテンツのSource[ByteString, _]を作成する方法を見てみましょう。

val file                          = new java.io.File("/tmp/fileToServe.pdf")
val path: java.nio.file.Path      = file.toPath
val source: Source[ByteString, _] = FileIO.fromPath(path)

簡単そうですね?このストリーミングされたHttpEntityを使用してレスポンス本文を指定するだけです。

def streamed = Action {
  val file                          = new java.io.File("/tmp/fileToServe.pdf")
  val path: java.nio.file.Path      = file.toPath
  val source: Source[ByteString, _] = FileIO.fromPath(path)

  Result(
    header = ResponseHeader(200, Map.empty),
    body = HttpEntity.Streamed(source, None, Some("application/pdf"))
  )
}

実際には、ここに問題があります。ストリーミングされたエンティティでContent-Lengthを指定しないため、Playはそれを自分で計算する必要があり、これを行う唯一の方法は、ソースコンテンツ全体を消費してメモリに読み込み、レスポンスサイズを計算することです。

これは、完全にメモリに読み込みたくない大きなファイルでは問題になります。そのため、Content-Lengthヘッダーを自分で指定するだけです。

def streamedWithContentLength = Action {
  val file                          = new java.io.File("/tmp/fileToServe.pdf")
  val path: java.nio.file.Path      = file.toPath
  val source: Source[ByteString, _] = FileIO.fromPath(path)

  val contentLength = Some(Files.size(file.toPath))

  Result(
    header = ResponseHeader(200, Map.empty),
    body = HttpEntity.Streamed(source, contentLength, Some("application/pdf"))
  )
}

このようにして、Playは本体ソースを遅延的に消費し、利用可能になり次第、データの各チャンクをHTTPレスポンスにコピーします。

§ファイルの提供

もちろん、Playはローカルファイルを提供するという一般的なタスクのために使いやすいヘルパーを提供しています。

def file = Action {
  Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}

このヘルパーは、ファイル名からContent-Typeヘッダーを計算し、Content-Dispositionヘッダーを追加して、ウェブブラウザがこのレスポンスをどのように処理するかを指定します。デフォルトでは、HTTPレスポンスにContent-Disposition: inline; filename=fileToServe.pdfヘッダーを追加することで、このファイルをインラインで表示します。

独自のファイル名を提供することもできます。

def fileWithName = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    fileName = _ => Some("termsOfService.pdf")
  )
}

注記:計算されたヘッダーが正確にContent-Disposition: inlineになる場合(ファイル名をnullとして返す場合:fileName = _ => null)、RFC 6266セクション4.2によると、コンテンツをインラインでレンダリングすることはデフォルトであるため、Playによって送信されません。

このファイルを添付ファイルとして提供したい場合

def fileAttachment = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    inline = false
  )
}

これで、ウェブブラウザがファイルをダウンロードしようとせず、ウェブブラウザウィンドウにファイルコンテンツを表示するだけになるため、ファイル名を指定する必要はありません。これは、テキスト、HTML、画像など、ウェブブラウザでネイティブにサポートされているコンテンツタイプに役立ちます。

§チャンクレスポンス

現時点では、コンテンツの長さをストリーミングする前に計算できるため、ファイルコンテンツのストリーミングでうまく機能します。しかし、コンテンツサイズが利用できない動的に計算されたコンテンツはどうでしょうか?

この種のレスポンスには、**チャンク転送エンコーディング**を使用する必要があります。

**チャンク転送エンコーディング**は、Hypertext Transfer Protocol(HTTP)のバージョン1.1におけるデータ転送メカニズムであり、ウェブサーバーは一連のチャンクでコンテンツを提供します。プロトコルで必要となるContent-Lengthヘッダーの代わりに、Transfer-Encoding HTTPレスポンスヘッダーを使用します。Content-Lengthヘッダーが使用されないため、サーバーはクライアント(通常はウェブブラウザ)へのレスポンスの送信を開始する前に、コンテンツの長さを知る必要がありません。ウェブサーバーは、動的に生成されたコンテンツの合計サイズを知る前に、レスポンスの送信を開始できます。

各チャンクのサイズは、チャンクそのものの直前に送信されるため、クライアントはそのチャンクのデータの受信が完了した時点を認識できます。データ転送は、長さゼロの最終チャンクによって終了します。

https://en.wikipedia.org/wiki/Chunked_transfer_encoding

利点は、利用可能になり次第データチャンクを送信するため、データを**ライブ**で提供できることです。欠点は、ウェブブラウザがコンテンツサイズを知らないため、適切なダウンロード進捗バーを表示できないことです。

どこかに、いくつかのデータを計算する動的なInputStreamを提供するサービスがあるとしましょう。まず、このストリームのSourceを作成する必要があります。

val data                               = getDataStream
val dataContent: Source[ByteString, _] = StreamConverters.fromInputStream(() => data)

これで、Ok.chunkedを使用してこれらのデータをストリーミングできます。

def chunked = Action {
  val data                               = getDataStream
  val dataContent: Source[ByteString, _] = StreamConverters.fromInputStream(() => data)
  Ok.chunked(dataContent)
}

もちろん、チャンクデータの指定には任意のSourceを使用できます。

def chunkedFromSource = Action {
  val source = Source.apply(List("kiki", "foo", "bar"))
  Ok.chunked(source)
}

サーバーから送信されたHTTPレスポンスを検査できます。

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

4
kiki
3
foo
3
bar
0

レスポンスを閉じる最後の空のチャンクに続いて、3つのチャンクを取得します。

次:Comet


このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信して自由に貢献してください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を開始してください。