文档

§流式 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 必须消耗整个内容并将其加载到内存中。

§发送大量数据

如果将整个内容加载到内存中不是问题,那么对于大型数据集呢?假设我们要将一个大型文件返回给 Web 客户端。

首先,让我们看看如何为文件内容创建一个 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 头部以指定 Web 浏览器应如何处理此响应。默认情况下,通过在 HTTP 响应中添加 Content-Disposition: inline; filename=fileToServe.pdf 头部,将以 inline 方式显示此文件。

您也可以提供自己的文件名。

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

注意:如果计算出的头部最终恰好是 Content-Disposition: inline(当返回 null 作为文件名时:fileName = _ => null),它将不会被 Play 发送,因为根据 RFC 6266 第 4.2 节,默认情况下会以内联方式呈现内容。

如果您想以 attachment 方式提供此文件

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

现在您不必指定文件名,因为 Web 浏览器不会尝试下载它,而只会将文件内容显示在 Web 浏览器窗口中。这对于 Web 浏览器原生支持的内容类型(如文本、HTML 或图像)很有用。

§分块响应

目前,它与流式文件内容配合得很好,因为我们能够在流式传输之前计算内容长度。但是,对于没有可用内容大小的动态计算内容呢?

对于这种类型的响应,我们必须使用分块传输编码

分块传输编码是超文本传输协议 (HTTP) 版本 1.1 中的一种数据传输机制,其中 Web 服务器以一系列块的形式提供内容。它使用 Transfer-Encoding HTTP 响应头部而不是 Content-Length 头部,否则协议将需要该头部。由于没有使用 Content-Length 头部,因此服务器不需要在开始向客户端(通常是 Web 浏览器)传输响应之前知道内容的长度。Web 服务器可以在知道动态生成内容的总大小之前开始传输响应。

每个块的大小在块本身之前发送,以便客户端可以知道何时完成接收该块的数据。数据传输由长度为零的最后一个块终止。

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

优点是我们可以实时提供数据,这意味着我们可以在数据可用时立即发送数据块。缺点是由于 Web 浏览器不知道内容大小,因此无法显示正确的下载进度条。

假设我们有一个服务,它在某个地方提供一个动态的 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

我们得到三个块,然后是一个最终的空块,它关闭了响应。

下一步:Comet


发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛 与社区进行交流。