play框架使用起来(12)

Posted zyhlal

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了play框架使用起来(12)相关的知识,希望对你有一定的参考价值。

HTTP异步编程

1.1 HTTP请求挂起#

      Play的设计初衷在于完成较短的请求。通过HTTP接口,Play使用固定的线程池来处理请求队列。为了达到理想的效果,线程池应该设计得尽可能小。最典型的情况:以处理程序的数量+1作为最佳值来设定线程池的大小。

      这意味着如果某个请求非常耗时(比如处理长时间的运算),它将会阻塞线程池并且影响应用程序的响应能力。当然,可以通过增大线程池大小来解决问题,但是这样会造成资源的极大浪费,而且线程池的大小也不可能无止尽地增大。

      试想聊天应用程序的例子:浏览器发送一个阻塞的HTTP请求,这个HTTP请求的作用是等待新消息后显示。这种类型的HTTP请求会占用很长时间(通常是好几秒),从而导致线程池的阻塞。如果有100个用户同时连接这个聊天应用程序,那么至少需要提供100个线程。这还是可以接受的,如果有1000个用户呢?甚至有10000个呢?

      为了解决这种情况,Play允许临时挂起HTTP请求。挂起的HTTP请求仍然保持连接,但是该请求的执行会被移出线程池并稍后进行尝试。根据需要,Play可以在一段固定的延时后恢复现场,继续执行请求。

      下例Action使用now()方法调用ReportAsPDFJob,该Job需要较长的处理时间。按照正常的情况,程序必须等待ReportAsPDFJob执行完后,才能向HTTP发送响应结果:

public static void generatePDF(Long reportId) 
   
Promise<InputStream> pdf = new ReportAsPDFJob(report).now();
   
InputStream pdfStream = await(pdf);
    renderBinary
(pdfStream);
      Play提供了更加高效的解决方案,调用await()方法将请求挂起,并释放占用的线程。当Promise<InputStream>完成后,恢复现场,继续执行后续操作。


1.2 Continuations#

      如果请求非常耗时,就把正在执行的代码挂起,并将占用的线程释放出来为其他的请求提供服务。这种高效的解决方案称为Continuations。在Play的早期版本中并没有await()方法,但可以等价地使用waitFor()方法。两者的效果相同,waitFor()方法也是将Action挂起,并在需要的时候重新调用。

      在应用中引入Continuations技术是为了使代码的异步处理变得简单化。由于Continuations允许显式地挂起和重用代码,因此可以采用如下方式:

public static void computeSomething() 
   
Promise<String> delayedResult = veryLongComputation(…);
   
String result = await(delayedResult);
    render
(result);
      事实上,这段代码会分为两步执行,并先后占用两个线程,但对于应用程序的代码却是透明的。以下是基于Continuations实现的循环:
public static void loopWithoutBlocking() 
   
for(int i=0; i<=10; i++)
         
Logger.info(i);
         await
("1s");
   

    renderText
("Loop finished");
      上述示例中,假设Play在DEV模式下运行(只占用一个线程),但仍可以同时处理多个请求。

      Cotinuations主要通过使用控制器调用await()方法实现,该方法接收两种不同类型的参数(事实上有6种重载的方法,但是主要应用场景为2种)。

  • 第一种:采用timeout的方式来调用await()方法,参数类型可以为毫秒,或者为字符串类型的字面表达式(例如1s代表一秒钟)。
  • 第二种:采用Future对象的方式来调用,并且通常情况下会使用Play的Promise(定义在lib.F中,实现了java Future类)。当Promise完成后会返回并继续执行其余的处理。值得注意的是,Promise中可以触发多个事件,比如waitAny()方法参数中包含多个事件,当完成了其中任何一个,该Promise便能够返回并继续执行。


      因此,上述的两种方式都会导致在未来某个时间点触发事件。第一种为预先指定,而第二种需要根据Promise完成的时间。

      Play引入Cotinuations,使得编写同类事件结构的代码更加简单:

//相关处理A
await
(timeout or promise);//等待promise执行完毕
//相关处理B

      在等待处理的过程中,服务器将HTTP线程释放出来,因此Play能够并发处理更多的请求,而且非常高效。当timeout时间到达或者Promise执行完毕,后续的代码会继续执行,并且不需要开发者编写任何与线程唤起相关的方法。

      使用timeout的方式来调用await()方法:

public static void useTimeout() 
   
for(int i=0; i<=10; i++)
         
Logger.info(i);
         await
("1s");
   

    renderText
("Execute finished");

      以上这段代码在执行过程中,一共释放了10次线程,并且在每秒等待结束后重新唤起。从开发者的角度看,这个处理过程是非常透明的,并且允许直观地构建应用(而不需要担心创建非阻塞应用,因为这些都交由Play进行处理)。

      使用Promise的方式来调用await()方法:

public static void usePromise()
       
    F
.Promise<WS.HttpResponse> promise1=WS.url("http://domain1.com").getAsync();
    F
.Promise<WS.HttpResponse> promise2=WS.url("http://domain2.com").getAsync();
    F
.Promise<List<WS.HttpResponse>> promises = F.Promise.waitAll(promise1, promise2);
    await
(promises);
    renderText
("Execute finished");

      上述代码使用了lib.F中的waitAll()方法,需要等待promise1和promise2都处理完成后,才能够继续执行后续处理。类似地,Play还提供了waitAny(),waitEither()等方法。


1.3  HTTP流式响应

由于Play提供在非阻塞的情况下轮询处理请求的功能,读者可能会有这样的设想:服务器端能否实现只要生成了一部分可用的结果数据就马上发送给浏览器。在Play中实现这个功能完全没有问题,而实现的关键就是以Content-Type:Chunked作为HTTP的响应类型。它允许将HTTP响应分成不同的块(chunk)分批发送,只要这些分块一被发出,浏览器立马就能接收到。以下是使用await()方法和Continuations的实现:

public static void generateLargeCSV() 
   
CSVGenerator generator = new CSVGenerator();
    response
.contentType = "text/csv";
   
while(generator.hasMoreData())
         
String someCsvData = await(generator.nextDataChunk());
          response
.writeChunk(someCsvData);
   

      虽然生成这个CSV文件需要花费1个小时,但是Play能够实现只使用一个线程同时处理好几个请求,并且一旦生成可用的数据后就马上发送给客户端。


1.4  WebSocket介绍

1> WebSocket介绍#

      WebSocket的目标是通过在浏览器和应用程序之间建立一条通信的频道,实现两者的双向通信。在Play中的WebSocket实现如下:

      在浏览器端,可以使用“ws://” URL方式建立socket连接:

new WebSocket("ws://localhost:9000/helloSocket?name=Guillaume")
      服务器端配置对应的WebSocket路由规则:
WS   /helloSocket            MyWebSocket.hello
      MyWebSocket是自定义的WebSocket控制器,继承于WebSocketController。WebSocket控制器和标准的HTTP控制器有些类似,但两者在概念上有所区别:
  • WebSocket控制器只有request对象,没有response对象。
  • WebSocket控制器可以访问session,但访问权限是只读的。
  • WebSocket控制器没有renderArgs,routeArgs以及flash作用域。
  • WebSocket控制器只能从路由模式或者以查询字符串的形式来读取参数。
  • WebSocket控制器拥有inbound和outbound两种通信频道。


      当客户端(即浏览器)通过ws://localhost:9000/helloSocket 建立socket连接时,Play会调用MyWebSocket控制器中的hello Action方法,一旦MyWebSocket.hello方法结束,该socket连接就会自动关闭:

public class MyWebSocket extends WebSocketController 
 
   
public static void hello(String name)
        outbound
.send("Hello %s!", name);
   

 
      针对上述例子,如果客户端建立了socket连接,就会收到“Hello Guillaume”消息,之后Play将socket连接关闭。

      当然,大部分情况下并不需要急于将socket连接关闭,可以使用await()方法进行一些适当的扩展。以下程序使用了Continuations,使服务器具有应答功能:

public class MyWebSocket extends WebSocketController 
 
   
public static void echo()
       
while(inbound.isOpen())
             
WebSocketEvent e = await(inbound.nextEvent());
             
if(e instanceof WebSocketFrame)
                 
WebSocketFrame frame = (WebSocketFrame)e;
                 
if(!e.isBinary)
                     
if(frame.textData.equals("quit")) play框架使用起来(17)

play框架使用起来(17)

play框架使用起来(11)

play框架使用起来

play框架使用起来(15)

play框架使用起来(15)