Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-01-01) 08. 流式的Http响应

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

(本文译者是爱国者)

流式的HTTP响应

Http响应和Content-Length头

在Http 1.1里,为了在一条TCP连接上进行多次请求和响应的发送,服务器必须为Content-Length选择合适的值. 但一般情况下,你无需主动指定Content-Length的值. 比如发送一段简单的文本,Play能根据文本的长度计算出Content-Length的值:

def index = Action {
  Ok("Hello World")
}
> **注意: **对于文本响应,`Content-Length`的值除了和字符串的长度有关外,还跟所使用的字符编码有关。`Content-Length`的值表示要传输的字节流大小。

之前我们提到,使用`play.api.libs.iteratee.Enumerator`指定Http响应正文:

def index = Action {
  SimpleResult(
    header = ResponseHeader(200),
    body = Enumerator("Hello World")
  )
}

实际上,Play要遍历整个`Enumerator`才能算出`Content-Length`的值,并且必须把`Enumerator`放在内存里

#### 发送大量的数据

如果只是发送少量的数据,采用上述简单的方法是没问题的,但对于要发送大数据量的情形,比如需要向客户端发送一个几兆的文件,就需要用到流式处理

假设我们需要将一个几兆的PDF文件发送给客户端,仍采用`Enumerator[Array[Byte]]`的办法:

val file = new java.io.File("/tmp/fileToServe.pdf")
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)

def index = Action {

  val file = new java.io.File("/tmp/fileToServe.pdf")
  val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)    

  SimpleResult(
    header = ResponseHeader(200),
    body = fileContent
  )
}

此时,Play会整个PDF文件加载到内存,然后计算`Content-Length`的值。 这在高并发场景下容易造成内存不足。为了避免这个问题,我们需要手动指定`Content-Length`的值。

def index = Action {

  val file = new java.io.File("/tmp/fileToServe.pdf")
  val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)    

  SimpleResult(
    header = ResponseHeader(200, Map(CONTENT_LENGTH -> file.length.toString)),
    body = fileContent
  )
}

这样的话,Play会一边读取PDF文件的内容,一边往Http响应体发送字节流。

#### 文件的服务

针对常见的文件服务场景, Play提供了一些工具函数:

def index = Action {
  // 将/tmp/fileToServe.pdf 发送给客户端
  Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}

这个`Ok.sendFile`函数会自动计算`Content-Type`的值,并提供`Content-Disposition`的值告诉浏览器该如何处理这个Http响应。默认的行为是往添加一个`Content-Disposition: attachment; filename=fileToServe.pdf`的Http header, 告诉浏览器下载文件. 使用这些工具函数能提高程序的开发效率.

你可以指定文件下载时使用的名字:

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

如果你希望浏览器显示文件的内容,而不是下载它,那么可以这样做:

def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    inline = true  // 告诉浏览器直接显示文件的内容
  )
}

#### 分块响应(chunked response)

如果无法事先计算响应正文的大小, 就需要使用**分块传输编码(Chunked transfer encoding)**技术。

> **分块传输编码(Chunked transfer encoding)**是Http 1.1引入的一种数据传输方式。使用这种数据传输方式时,服务器需要设置`Transfer-Encoding`响应头,但无需设置`Content-Length`。由于没有指定`Content-Length`,服务器在向客户端(通常是浏览器)开始传输响应数据时,无需知道内容的总长度(可能事先也无法知道).
服务器可以不断地向客户端发送动态产生的数据。数据以分块的形式进行传输。在向客户端传输每个数据块之前, 服务器会先告诉客户端该数据块的大小,这样客户端就能知道每次要接收多少字节的信息量。当服务器向客户端传输一个长度为0的数据块时,表明整个传输过程结束,客户端可以关闭Http连接。

关于**分块传输编码(Chunked transfer encoding)**的更多内容,请参阅[[[http://en.wikipedia.org/wiki/Chunked_transfer_encoding](http://en.wikipedia.org/wiki/Chunked_transfer_encoding)]]

这种传输方式的优点是允许我们**实时**传输数据,也就是说只要数据准备就绪(哪怕是一小部分),就可以立即发送。缺点是客户端(或浏览器)无法知道还要接收多少字节信息,因此无法准确显示剩余下载时间。

在下面的例子中,我们假设提供了一个能不断产生数据的输入流`InputStream`的服务. 首先,我们需要从该流构建一个`Enumerator`:

val data = getDataStream
val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)

然后我们可以使用`ChunkedResult`以流的形式发送数据:

def index = Action {

  val data = getDataStream
  val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)

  ChunkedResult(
    header = ResponseHeader(200),
    chunks = dataContent
  )
}

和平常一样,Play也为这种场景提供了辅助函数:

def index = Action {

  val data = getDataStream
  val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)

  Ok.stream(dataContent)
}

当然,我们也可以使用`Enumerator`来创建若干数据块

def index = Action {
  Ok.stream(
    Enumerator("kiki", "foo", "bar").andThen(Enumerator.eof)
  )
}

提示: Enumerator.callbackEnumeratorEnumerator.pushEnumerator是两种以命令式代码风格创建活性非阻塞迭代器(enumerators)的快捷方式。

你可以使用一些Http剖析工具查看上述例子的Http响应文本:

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

4
kiki
3
foo
3
bar
0

从剖析的结果来看,服务器向客户端发送了三个数据块,分别是_kiki,foo_和_bar_,最后发送一个0字节的数据块告诉客户端整个传输过程结束。

原文:https://github.com/playframework/Play20/wiki/ScalaStream

comments powered by Disqus