§流式 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 服务器可以在知道动态生成内容的总大小之前开始传输响应。每个块的大小在块本身之前发送,以便客户端可以知道何时完成接收该块的数据。数据传输由长度为零的最后一个块终止。
优点是我们可以实时提供数据,这意味着我们可以在数据可用时立即发送数据块。缺点是由于 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
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛 与社区进行交流。