#处理异步结果
1.为什么使用异步Results?
直到现在,我们只能计算直接发送到客户端的result。这种方式并非总是奏效,result或许要花费高昂的计算代价,或者很长的web服务调用。
因为Play2.0工作时,action代码必须尽可能得快(例如非阻塞IO)。那么如果我们还没有计算完毕我们要返回什么结果呢?答案是响应一个
结果的承诺(对承诺有结果)。
2.如何创建一个Promise<Result>。
要创建一个承诺结果,我们首先需要一个给我吗计算出结果的真实数据的承诺:
Promise<Double> promiseOfPIValue = computePIAsynchronously(); Promise<Result> promiseOfResult = promiseOfPIValue.map( new Function<Double,Result>() { public Result apply(Double pi) { return ok("PI value computed: " + pi); } } );
Play2.0的异步API方法会给你一个承诺(promise)。例如在你通过Play.libs.WS的API调用外部Web服务时,
或者你使用akka计划异步任务,或者使用akka和Actors沟通时。
一种简单的方法是使用akka的一段异步程序来获得一个Promise,这一过程在新的线程中执行,如:
Promise<Integer> promiseOfInt = Akka.future( new Callable<Integer>() { public Integer call() { intensiveComputation(); } } );
3.异步结果
现在我们要用到Results.Status了,在发送异步结果前,需要使用Results.AsyncResult包装一下实际的结果。,如:
public static Result index() { Promise<Integer> promiseOfInt = Akka.future( new Callable<Integer>() { public Integer call() { intensiveComputation(); } } ); async( promiseOfInt.map( new Function<Integer,Result>() { public Result apply(Integer i) { return ok("Got result: " + i); } } ) ); }
注意async()是用来从Promise<Result>建立一个AsyncResult的帮助方法。
#流Http响应
1.标准响应和Content-Length头
自Http 1.1,保持一个单一的连接为若干个Http请求和响应服务,服务器在做出响应时
必须发送正确的Content-Length Http头。
默认的,你可以发送一个简单的result,例如:
public static Result index() { return ok("Hello World") }
在这个例子中你没有设置Content-Length头,当然,这是因为你发送的数据大家都认识,
Play能够为你计算出内容长度(注意基于文本的内容长度与你的编码格式有关),
并为你生成一个正确的头。
为了能计算出内容的长度(大小),你需要将全部的相应数据加载到内存中。
2.文件服务
如果把简单的内容数据全部装载进内存不是个问题,那么如果要装载一个巨大的数据呢?
比如说,我们要把一个很大的文件返回给web客户端。Play为这种常见的任务体统了易用的帮助方法:
public static Result index() { return ok(new java.io.File("/tmp/fileToServe.pdf")); }
另外,这个方法也会自动为你计算文件名的Content-Type头。并且还会添加一个说明web客户端如果操作
这个响应的Content-Disposition头。默认使用Content-Disposition: attachment; filename=fileToServe.pdf
询问web浏览器下载该文件。
3.分块响应
现在,能够横好的和文件流内容一起工作了,因为我们讷讷感在将文件流化前算出它的长度来。
但是如何动态计算一个得不到内容大小的文件呢?
这种类型的响应我们得使用分块传输编码。
分块传输编码在Http 1.1中十一中数据传输机制,Web服务器提供若干块数据。它使用Transfer-Encoding Http响应头
而不是Content-Length头,不过Content-Length也是需要的,但是协议不用它。
因为服务器在开始将响应传输到客户端之前并不需要知道内容总共有多大。
每个块能够正确发送自身的大小,客户端可以正确接收这个块。数据传输最终以一个大小为0的块结束(就是说,
所有块发送完毕后,还要发送一个大小为0的块)。
这种方式的优势在于,我们可以提供实时数据。就是说一旦得到一个数据块,我们可以立即发送出去。缺点是,因为客户端
不知道整个内容的大小,因而不能显示一个正确的下载进度条。
比如说,我们有一个计算一些数据的动态输入流的服务。我们可以直接使用Play的分块响应来流化其内容:
public static Result index() { InputStream is = getDynamicStreamSomewhere(); return ok(is); }
你也可以设置你自己的分块响应器。Play的JavaAPI支持文本和二进制的分块流(通过String和byte[])。
public static index() { // Prepare a chunked text stream Chunks<String> chunks = new StringChunks() { // Called when the stream is ready public void onReady(Chunks.Out<String> out) { registerOutChannelSomewhere(out); } } // Serves this stream with 200 OK ok(chunks); }
当流可以安全的写是onReady方法被调用。它为你提供了Chunks。Out,你可以向里面写。
#Comet 套接字(Comet sockets)
注意:详细的Comet技术请参阅Bayeux Protocol。http://svn.cometd.org/trunk/bayeux/bayeux.html
1.使用分块响应创建Comet套接字
分块响应(chunked responses)的一个很有用的用途是创建Comet 套接字,Comet套接字只是一个仅包含<script>元素的text/html分块响应。
每个分块里,我们写一个包含JavaScript的<script>标签,立即由web浏览器执行。通过这种方式,我们可以把事件
由服务器端发送到客户端:对每个消息而言,把它包装进JavaScript回调函数里,发给分块响应即可。
我们来写一个对这一概念的证明:创建一个产生调用<script>标签调用浏览器console.log功能的举例:
public static Result index() { // Prepare a chunked text stream Chunks<String> chunks = new StringChunks() { // Called when the stream is ready public void onReady(Chunks.Out<String> out) { out.write("<script>console.log('kiki')</script>"); out.write("<script>console.log('foo')</script>"); out.write("<script>console.log('bar')</script>"); out.close(); } } response().setContentType("text/html"); ok(chunks); }
在浏览器上访问这个Action,你会看到有三个时间在浏览器控制台被记录。
2.使用play.libs.Comet帮助方法。
我们提供了一个Comet的帮助来操作这些comet分块流。它可以支持String和Json。
前面的代码可以改写成这样:
public static Result index() { Comet comet = new Comet("console.log") { public void onConnected() { sendMessage("kiki"); sendMessage("foo"); sendMessage("bar"); close(); } }; ok(comet); }
3.永远的iframe技术
标准的技术是写一个Comet套接字,在一个iframe中加载无限循环的Comet响应,并且指定一个回调函数调用其父Fram。
public static Result index() { Comet comet = new Comet("parent.cometMessage") { public void onConnected() { sendMessage("kiki"); sendMessage("foo"); sendMessage("bar"); close(); } }; ok(comet); }
Html页面类似于这样:
<script type="text/javascript"> var cometMessage = function(event) { console.log('Received event: ' + event) } </script> <iframe src="/comet"></iframe>
#WebSocket
1.用WebSocket代替Comet
Comet Socket是一种向web浏览器发送活动事件的黑客手段。Comet仅支持从服务器到客户端的单向通信。要把事件推到服务器端,web浏览器要使用Ajax请求。
现代Web浏览器原生支持WebSockets的通过双向实时通信。
WebSocket是一个网络技术,通过一个单一传输控制协议(TCP)套接字,提供双向,全双工的通信通道。 WebSocket的API是由W3C制定,WebSocket协议已经被IETF规范为RFC6455。
WebSocket被设计为在Web浏览器和Web服务器上实现,但它可以通过任何客户端或服务器应用程序使用。因为一般的非80端口的tcp连接经常被除家庭环境外的管理员封锁,
当复用一个端口实现多个Websocket服务时,可以通过增加额外的协议开销,从而避开这些限制。这使得web应用提供实时的双向通信成为可能。
在Websocket出现之前,要实现全双工的双向通信只能使用Comet通道。
Comet的传输并不可靠,同时,由于TCP握手和HTTP头的存在,在传输小消息时它会低效。Websocket旨在不影响网络安全的前提下解决这些问题。
2.处理Websocket
到现在为止,我们使用一个简单的操作方法来处理标准的HTTP请求和发送标准的HTTP结果。WebSockets是一个完全不同的猛兽,无法通过标准的行动处理。
为了处理WebSocket你的方法必须返回WebSocket的,而不是一个Result: public static WebSocket<String> index() { return new WebSocket<String>() { // Called when the Websocket Handshake is done. public void onReady(WebSocket.In<String> in, WebSocket.Out<String> out) { // For each event received on the socket, in.onMessage(new Callback<String>() { public void invoke(String event) { // Log events to the console println(event); } }); // When the socket is closed. in.onClose(new Callback0() { public void invoke() { println("Disconnected") } }); // Send a single 'Hello!' message out.write("Hello!"); } } }
一个WebSocket可以访问请求头(从HTTP请求发起的WebSocket连接),使您可以检索标准头文件和会话数据。但它不会访问任何请求体,也不访问HTTP响应。
当WebSocket准备好了,你得到输入和输入的通道。
在这个例子中,我们打印每个消息到控制台,我们发送一个Hello!消息:
public static WebSocket<String> index() { return new WebSocket<String>() { public void onReady(WebSocket.In<String> in, WebSocket.Out<String> out) { out.write("Hello!"); out.close() } } }