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

处理异步结果


    为什么需要异步结果?

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

    然而情况不总是这样:结果可能依赖于一个繁重的计算和一个长时间的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將为我们计算实际的结果值。

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

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

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

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

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

    异步结果

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

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

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

    处理超时

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

1def index = Action { 2 val promiseOfInt = Akka.future { intensiveComputation() } 3 Async { 4 promiseOfInt.orTimeout("Oops", 1000).map { eitherIntorTimeout => 5 eitherIorTimeout.fold( 6 timeout => InternalServerError(timeout), 7 i => Ok("Got result: " + i) 8 ) 9 } 10 } 11}

    HTTP流响应

    标准响应和Content-Length

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

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

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

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

1def index = Action { 2 SimpleResult( 3 header = ResponseHeader(200), 4 body = Enumerator("Hello World") 5 ) 6}

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

    大数据发送

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

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

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

1def index = Action { 2 3 val file = new java.io.File("/tmp/fileToServe.pdf") 4 val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file) 5 6 SimpleResult( 7 header = ResponseHeader(200), 8 body = fileContent 9 ) 10}

    实际上这是有问题的。我们没有指定Content-Length长度,Play必须自行计算。唯一的方法是消耗整个enumerator,并將内容全部加载到内存中,最后方能计算响应的长度。
    对于大文件,这是有问题的。我们不希望内容都加载到内存中。为了避免这种情况,我们需要手动指定Content-Length的长度。

1def index = Action { 2 3 val file = new java.io.File("/tmp/fileToServe.pdf") 4 val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file) 5 6 SimpleResult( 7 header = ResponseHeader(200, Map(CONTENT_LENGTH -> file.length.toString)), 8 body = fileContent 9 ) 10}

    通过这种方式,Play將以懒加载的方式使用enumerator,一块一块的將可用数据拷贝到HTTP响应中。

    处理文件

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

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

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

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

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

    分块响应

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

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

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

    这种机制的优点是我们可以实时的传送数据。只要块数据可用,我们就发送它。缺陷是,既然内容长度无法获知,浏览器无法显示正确的下载进度。
    比方我们有某个服务,利用InputStream流动态的操纵数据。首先我们为该流创建一个Enumerator:

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

    我们现在可以通过ChunkedResult处理这些数据:

1def index = Action { 2 3 val data = getDataStream 4 val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data) 5 6 ChunkedResult( 7 header = ResponseHeader(200), 8 chunks = dataContent 9 ) 10}

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

1def index = Action { 2 3 val data = getDataStream 4 val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data) 5 6 Ok.stream(dataContent) 7}

    当然我们也可以用任何的Enumerator指定块数据:

1def index = Action { 2 Ok.stream( 3 Enumerator("kiki", "foo", "bar").andThen(Enumerator.eof) 4 ) 5}

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

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

1HTTP/1.1 200 OK 2Content-Type: text/plain; charset=utf-8 3Transfer-Encoding: chunked 4 54 6kiki 73 8foo 93 10bar 110

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

    Comet sockets

   
    使用 分块 response 创建Comet Socket

    Chunked responses的其中一个用处是创建Comet sockets。一个Comet响应不过是一个包含<script>元素的 text/html 响应。每个响应块写入的<script>标签都被浏览器 立即执行。通过这种方式,我们可以实时的为浏览器发送事件:每一段消息,我们都可以包装入<script>标签中,声明一个JavaScript回调函数,并写入响应块中。

    让我们开始写第一个简陋的协议:一个enumerator生成几个<script>标签,调用浏览器的 console.log 方法:

1def comet = Action { 2 val events = Enumerator( 3 """<script>console.log('kiki')</script>""", 4 """<script>console.log('foo')</script>""", 5 """<script>console.log('bar')</script>""" 6 ) 7 Ok.stream(events >>> Enumerator.eof).as(HTML) 8}

    如果你从浏览器中访问该action,你將会在浏览器控制台中看到三个日志记录。
    提示:编写 events >>> Enumerator.eof 只是 events.andThen(Enumerator.eof)的另一种方式。

    我们可以使用play.api.libs.iteratee.Enumeratee更好的改写该例子。that is just an adapter to transform an Enumerator [A] into another Enumerator
    [B] .让我们使用它来封装一个标准的消息响应:

1import play.api.templates.Html 2 3// Transform a String message into an Html script tag 4val toCometMessage = Enumeratee.map[String] { data => 5 Html("""<script>console.log('""" + data + """')</script>""") 6} 7 8def comet = Action { 9 val events = Enumerator("kiki", "foo", "bar") 10 Ok.stream(events >>> Enumerator.eof &> toCometMessage) 11}

提示:编写 events >>> Enumerator.eof &> toCometMessage 只是  events.andThen(Enumerator.eof).through(toCometMessage) 的另一种形式。

    使用 play.api.libs.Comet 助手

    我们提供了一个Comet助手类来处理这类Comet chunked streams,实现几乎和我们这里编写的一样。
    注意:实际上他为你做了更多事,考虑到浏览器的兼容性,他会先推送一些空数据来判断浏览器的兼容性,他支持String和JSON两种类型。它也可以被扩展以支持更多消息类型。

    我们使用它来重写前面的例子:

1def comet = Action { 2 val events = Enumerator("kiki", "foo", "bar") 3 Ok.stream(events &> Comet(callback = "console.log")) 4}

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

    无限使用iframe技术(???)

    标准的Comet Socket技术是给一个iframe无限的推送并指定调用父窗口的回调函数数据。

1def comet = Action { 2 val events = Enumerator("kiki", "foo", "bar") 3 Ok.stream(events &> Comet(callback = "parent.cometMessage")) 4}

    HTML看起来会像这样:

1<script type="text/javascript"> 2 var cometMessage = function(event) { 3 console.log('Received event: ' + event) 4 } 5</script> 6 7<iframe src="https://my.oschina.net/comet"></iframe>

   

    WebSockets

    使用WebSockets取代Comet sockets

    Comet sockets是一种向浏览器发送实时数据的hack技术。他仅仅提供了服务器和客户端交流的一种形式。给服务器端发送数据,客户端只能发送Ajax请求。

    Note: It is also possible to achieve the same kind of live communication the other way around by
    using an infinite HTTP request handled by a custom BodyParser that receives chunks of input
    data, but that is far more complicated.

    现代的浏览器借助WebSockets原生支持双向实时通信。
   
    WebSockets是一种通过单一的TCP socket连接提供双向的,全双工连接通道的web技术。WebSockets API被W3C标准化,WebSockets协议也被IEF以RFC 6455形式标准化。
   
    WebSockets被设计于在浏览器和服务器中实现,但它其实可用于任何的服务器端或客户端应用。由于管理员常常拒绝本地环境以外非80端口的普通TCP连接,WebSockets必须找到一种方式规避该限制,还需提供一些类似的功能让单一的TCP连接服务于多个WebSockets请求。

    WebSockets也为那些需要实时,双向通信的应用提供帮助。WebSockets实现以前,这种双向通信只能使用Comet通道实现。然而Comet实现的设施并不可靠,缘于TCP的握手机制和HTTP协议开销,对于短消息传输是非常低效的。
    WebSocket协议旨在解决这些问题,而不影响现有的web安全的假设妥协。
    http://en.wikipedia.org/wiki/WebSocket

    处理WebSockets

    现在为止,我们使用action实例处理标准HTTP请求,发送标准HTTP响应。WebSockets是如些的独特而不能使用常规的Action处理。
    为了处理WebSockets请求,需使用WebSocket替代Action:

1def index = WebSocket.using[String] { request => 2 3 // Log events to the console 4 val in = Iteratee.foreach[String](println).mapDone { _ => 5 println("Disconnected") 6 } 7 8 // Send a single 'Hello!' message 9 val out = Enumerator("Hello!") 10 11 (in, out) 12}

    WebSocket具备检索请求头(从HTTP请求头启动一个WebSocket连接)能力,允许你取回标准头消息和session数据。然而它不具备访问 request body 和HTTP response body 的能力。

    当通过这种方式建立WebSocket的时候,我们必须返回一对in和out频道。
        in频道是一个Iteratee[A,Unit](A是消息类型,我们这里使用String),它会被每一个消息通知,当客户端关闭socket的时,会收到EOF。
        out频道是一个Enumerator[A],它將产生待发送给客户端的消息。可以通过服务器端发送一个EOF来关闭它。

    在该例子中,我们简单的將消息叠代发送给控制台。为了发送消息,我们创建了个虚拟的简单enumerator来发送 Hello! 消息。
    提示:你可以访问http://websocket.org/echo.html测试WebSockets。只需把地址设为ws://localhost :9000 .

    让我们编写另外一个例子,丢弃输入值,发送Hello!后关闭该socket。

1def index = WebSocket.using[String] { request => 2 3 // Just consume and ignore the input 4 val in = Iteratee.consume[String]() 5 6 // Send a single 'Hello!' message and close 7 val out = Enumerator("Hello!") >>> Enumerator.eof 8 9 (in, out) 10}
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

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