Play 2.0 用户指南 - 异步HTTP编程 --针对Scala开发者

Stella981
• 阅读 546

处理异步结果


    为什么需要异步结果?

   
    目前为止,我们能够直接向客户端发送响应。

    然而情况不总是这样:结果可能依赖于一个繁重的计算和一个长时间的web service调用。

    缘于 Play 2.0 的工作方式,action代码必须尽可能的快(如,非阻塞)。那,未能生成最终结果前,应该返回什么呢?答案是返回一个 promise(承诺?) of response!
    A Promise [Result] 最终会赎回一个Result类型的值。使用 Promise[Result] 替换正常的Result,我们可以无阻塞的快速生成结果。该响应是一个返回Result的承诺(Promise)。

    等待响应的时候,web客户端將会被阻塞,但服务器不会被阻塞,空闲资源可以移做它用。    

    怎样创建Promise[Result]

   

    为了创建Promise[Result],我们首先需要另一个promise:该promise將为我们计算实际的结果值。

val promiseOfPIValue: Promise[Double] = computePIAsynchronously()
val promiseOfResult: Promise[Result] = promiseOfPIValue.map { pi =>
  Ok("PI value computed: " + pi)    
}

    所有的 Play 2.0 的异步调用API会返回 Promise。不管你是使用 play.api.libs.WS API调用外部web服务,还是借助Akka分配异步任务,亦或使用 play.api.libs.Akka 在actors间通信。

    一个简单的异步执行代码块并获取一个 Promise 对象的方法是使用 play.api.libs.concurrent.Akka助手:

val promiseOfInt: Promise[Int] = Akka.future {
  intensiveComputation()
}

    注意:该intensiveComputation计算单元將运行在另一个线程中,或者运行位于Akka集群的远程服务器中。    

    异步结果

    迄今为止,我们都使用 SimpleResult 来发送一个异步响应,我们需要一个 AsyncResult 类来封装实际的 SimpleReslut:

def index = Action {
  val promiseOfInt = Akka.future { intensiveComputation() }
  Async {
    promiseOfInt.map(i => Ok("Got result: " + i))
  }
}

    注意:Async { ... }是一个助手方法,用于从Promise[Result]中构建AsyncResult。    

    处理超时

    超时处理,常常用于避免浏览器因某些错误而遭长时间阻塞。这种情形很容易处理:

def index = Action {
  val promiseOfInt = Akka.future { intensiveComputation() }
  Async {
    promiseOfInt.orTimeout("Oops", 1000).map { eitherIntorTimeout =>
      eitherIorTimeout.fold(
        timeout => InternalServerError(timeout),
        i => Ok("Got result: " + i)    
      )    
    }  
  }
}

    HTTP流响应

    标准响应和Content-Length

    从HTTP 1.1开始,为保证单个打开的连接能服务于多个HTTP请求和响应,服务器必须针对响应发送适当的Content-Length请求头。
    默认情况,当发回一个响应结果时,你并没有指定Content-Length头信息,例如:

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

    然而,因为该内容是已知的,Play能够自行计算该长度并产生适当的响应头信息。
    注意:基于文本的内容不像表面看上去哪么简单, Content-Length 头需要根据字符编码计算,并把字符转换成字节。

    实际上,我们前面已经看到,response body 被指定使用一个play.api.libs.iteratee.Enumerator:

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

    意味着,为了正确计算Content-Length,Play必须消耗整个enumerator,并把内容全部加到内存中。

    大数据发送

    如果对于简单的Enumerators,把内容全部加载到内存不是问题,那么大量数据怎么办呢?比方说我们要给客户端返回一个大的文件。
    我们首先看看如何创建Enumerator [Array[Byte]]列举该内容:

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

    它看起来正确吗?我们仅使用enumerator指定 response body:

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
  )
}

    实际上这是有问题的。我们没有指定Content-Length长度,Play必须自行计算。唯一的方法是消耗整个enumerator,并將内容全部加载到内存中,最后方能计算响应的长度。
    对于大文件,这是有问题的。我们不希望内容都加载到内存中。为了避免这种情况,我们需要手动指定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將以懒加载的方式使用enumerator,一块一块的將可用数据拷贝到HTTP响应中。

    处理文件

    Play为处理本地文件提供了一个便利方法:

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

    该方法也会根据文件名确定Content-Type内容,并添加Content-Disposition元素来指定浏览器的处理方式。默认是通过添加Content-Disposition : attachment ; filename =fileToServe.pdf 响应头信息,指定浏览器下载该文件。
    你也可以自定义文件名:

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

    你也能通过不指定文件名,避免浏览器下载该文件,而仅让浏览器显示该文件内容,如text,HTML或图片等这些浏览器原生支持的文件类型。

    分块响应

    目前为止,发送响应前,我们就计算好响应体长度,一切都工作得很好。但是,需要动态计算的长度怎么办?长度无法获取的情况下怎么办?
    这种类型的响应,我们必须使用Chunked transfer encoding。

    Chunked transfer encoding是HTTP 1.1提供的一种数据传输机制,服务器將内容分成多块传送。它使用Transfer-Encoding HTTP响应头替代Content-Length, 以跳过长度限制。由于Content-Length不再使用,数据在发送给客户端(通常是web浏览器)前,不需要提前计算长度了。在得知内容的总长度前,服务器可以动态的生成并传输内容。

    每个块大小在发送前被正确的指定,以便浏览器通知何时该块数据接收完成。数据传输將在最后一个长度为零的块处中断。

    这种机制的优点是我们可以实时的传送数据。只要块数据可用,我们就发送它。缺陷是,既然内容长度无法获知,浏览器无法显示正确的下载进度。
    比方我们有某个服务,利用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
  )
}

    一如既往,我们提供了便利方法完成同样工作:

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)
  )
}

    Tip:Enumerator.callbackEnumerator and Enumerator.pushEnumerator convenient ways to create reactive non-blocking enumerators in an imperative style.

    我们可以查看服务器发回的响应:

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

4
kiki
3
foo
3
bar
0

    我们接收到了三块数据,最后在接到到空块后关闭该响应。

    Comet sockets

   
    使用 分块 response 创建Comet Socket

    Chunked responses的其中一个用处是创建Comet sockets。一个Comet响应不过是一个包含