§流式 HTTP 响应
§标准响应和 Content-Length 标头
从 HTTP 1.1 开始,为了保持单个连接以服务多个 HTTP 请求和响应,服务器必须在响应中发送适当的 Content-Length
HTTP 标头。
默认情况下,当您发送回一个简单的结果时,您不会指定 Content-Length
标头,例如
public Result index() {
return ok("Hello World");
}
当然,由于您发送的内容是已知的,Play 能够为您计算内容大小并生成适当的标头。
注意:对于基于文本的内容,它并不像看起来那么简单,因为
Content-Length
标头必须根据用于将字符转换为字节的字符编码进行计算。
实际上,我们之前看到响应主体是使用 play.http.HttpEntity
指定的
public Result index() {
return new Result(
new ResponseHeader(200, Collections.emptyMap()),
new HttpEntity.Strict(ByteString.fromString("Hello World"), Optional.of("text/plain")));
}
这意味着为了正确计算 Content-Length
标头,Play 必须消耗整个内容并将其加载到内存中。
§发送大量数据
如果将整个内容加载到内存中不是问题,那么大型数据集呢?假设我们要将一个大型文件返回给 Web 客户端。
让我们首先看看如何为文件内容创建一个 Source[ByteString, _]
java.io.File file = new java.io.File("/tmp/fileToServe.pdf");
java.nio.file.Path path = file.toPath();
Source<ByteString, ?> source = FileIO.fromPath(path);
现在看起来很简单吧?让我们使用这个流式 HttpEntity 来指定响应主体
public Result index() {
java.io.File file = new java.io.File("/tmp/fileToServe.pdf");
java.nio.file.Path path = file.toPath();
Source<ByteString, ?> source = FileIO.fromPath(path);
return new Result(
new ResponseHeader(200, Collections.emptyMap()),
new HttpEntity.Streamed(source, Optional.empty(), Optional.of("text/plain")));
}
实际上我们这里有一个问题。由于我们在流式实体中没有指定 Content-Length
,Play 将不得不自己计算它,而唯一的方法是消耗整个源内容并将其加载到内存中,然后计算响应大小。
对于我们不想完全加载到内存中的大型文件来说,这是一个问题。因此,为了避免这种情况,我们只需要自己指定 Content-Length
标头。
public Result index() {
java.io.File file = new java.io.File("/tmp/fileToServe.pdf");
java.nio.file.Path path = file.toPath();
Source<ByteString, ?> source = FileIO.fromPath(path);
Optional<Long> contentLength = null;
try {
contentLength = Optional.of(Files.size(path));
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
return new Result(
new ResponseHeader(200, Collections.emptyMap()),
new HttpEntity.Streamed(source, contentLength, Optional.of("text/plain")));
}
这样,Play 将以惰性方式消耗主体源,并在数据可用时立即将每个数据块复制到 HTTP 响应中。
§提供文件服务
当然,Play 提供了易于使用的助手,用于执行提供本地文件的常见任务
public Result index() {
return ok(new java.io.File("/tmp/fileToServe.pdf"));
}
此助手还会根据文件名计算 Content-Type
标头,并添加 Content-Disposition
标头以指定 Web 浏览器应如何处理此响应。默认情况下,通过在 HTTP 响应中添加标头 Content-Disposition: inline; filename=fileToServe.pdf
来将此文件显示为 inline
。
您也可以提供自己的文件名
public Result index() {
return ok(new java.io.File("/tmp/fileToServe.pdf"), Optional.of("fileToServe.pdf"));
}
注意:如果计算出的标头最终恰好为
Content-Disposition: inline
(在将null
作为文件名传递时),则 Play 不会发送它,因为根据 RFC 6266 第 4.2 节,将内容渲染为内联本身就是默认行为。
如果您想将此文件作为 attachment
提供
public Result index() {
return ok(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
的服务,该服务计算一些数据。我们可以使用分块响应让 Play 直接流式传输此内容。
public Result index() {
InputStream is = getDynamicStreamSomewhere();
return ok(is);
}
您也可以设置自己的分块响应构建器。
public Result index() {
// Prepare a chunked text stream
Source<ByteString, ?> source =
Source.<ByteString>actorRef(256, OverflowStrategy.dropNew())
.mapMaterializedValue(
sourceActor -> {
sourceActor.tell(ByteString.fromString("kiki"), null);
sourceActor.tell(ByteString.fromString("foo"), null);
sourceActor.tell(ByteString.fromString("bar"), null);
sourceActor.tell(new Status.Success(NotUsed.getInstance()), null);
return NotUsed.getInstance();
});
// Serves this stream with 200 OK
return ok().chunked(source);
}
方法Source.actorRef
创建一个 Pekko Streams Source
,该 Source
物化成一个 ActorRef
。然后,您可以通过向 actor 发送消息来发布元素到流中。另一种方法是创建一个扩展 ActorPublisher
的 actor,并使用 Stream.actorPublisher
方法创建它。
我们可以检查服务器发送的 HTTP 响应。
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
4
kiki
3
foo
3
bar
0
我们得到三个块和一个最终的空块,它关闭了响应。
有关使用 Pekko Streams 的更多信息,您可以参考Pekko Streams 文档。
下一步:Comet
发现此文档中的错误?此页面的源代码可以在此处找到。阅读完文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。