10.在HTTP下进行异步编程

来源:互联网 发布:淘宝魔镜插件免费版 编辑:程序博客网 时间:2024/05/21 11:08

10.在HTTP下进行异步编程

本节将介绍在play里如何进行异步处理以实现典型的长轮询(long-polling)、流以及其他Comet-style类型的应用程序以支持上千个同时发生的连接。

暂停http请求

Play使用的是短小的请求,它使用固定的线程池来处理http连接者的查询请求。为了达到最佳效果,线程池应该尽可能小。我们通常使用的最适合的数字就是处理器数量+1来设置默认池大小。

也就是说如果请求的处理时间很长的话(比如等待一个长时的计算),这个请求就会耗尽线程池,使应用程序的响应变得很慢。当然可以扩大线程池,但这样会浪费资源,而且线程池不可能是无限的。

考虑一下聊天室应用,浏览器发送一个阻塞式的http请求用于等待显示新的信息。这个请求将会非常这长(比如几分钟),并且会打破线程池。如果计划允许100个用户同时连接聊天室程序,那么我们就需要提供至少100个线程,当然,这是可行的。但如果是1000个,或100000个呢?

在这种情况下,play允许你暂停一个请求。http请求将停留在连接状态,但请求执行将被从线程池中弹出,过会再试。你即可在固定等待的时间内告诉play去测试一下该请求,也可等待一个允许值变为可能情况。

小提示: 请看一下真实的示例samples-and-tests/chat

比如,下面这个动作将要加载一个非常耗时的job并且一直等到job完成返回结果:

public static void generatePDF(Long reportId) {

   Promise<InputStream> pdf = new ReportAsPDFJob(report).now();

    InputStreampdfStream = await(pdf);

   renderBinary(pdfStream);

}

这里,我们使用await(…)来让Play暂停请求,直到Promise<InputStream> 值返回redeemed。

Continuations

因为框架需要收回线程以便为其他请求服务,因此play就必须暂停你的代码。在之前的play版本中await(…) 等价于waitFor(…),用于暂停你的action,之后又重新调用。

为了易于约定我们介绍的异步代码,Continuations允许代码被暂停和被透明恢复,因此书写如下代码是非常必要的:

public static void computeSomething() {

   Promise<String> delayedResult = veryLongComputation(…);

    String result =await(delayedResult);

    render(result);

}

在这里,事实上你的代码将分成两步用两个不同的线程来执行。但这些代码对你来说是完全透明的。

使用await(…)和continuations,你可能需要写一个循环:

public static void loopWithoutBlocking() {

    for(int i=0;i<=10; i++) {

        Logger.info(i);

        await("1s");

    }

   renderText("Loop finished");

}

当使用一个线程处理这个请求时,在默认的开发模式下,Play能够同时为不同的请求运行这些循环。

更实际的示例是异步从远程URL获取内容。下面将并行运行三个远程http请求:每个都调用play.libs.WS.WSRequest.getAsync()方法来执行一个GET请求,异步返回一个play.libs.F.Promise。action方法通过调用三个Promise组合实例的await(…)方法来暂停进入的http请求。当三个远程调用返回结果后,线程将自动恢复并且渲染response。

public class AsyncTest extends Controller {

 

  public staticvoid remoteData() {

   F.Promise<WS.HttpResponse> r1 =WS.url("http://example.org/1").getAsync();

   F.Promise<WS.HttpResponse> r2 =WS.url("http://example.org/2").getAsync();

   F.Promise<WS.HttpResponse> r3 =WS.url("http://example.org/3").getAsync();

 

   F.Promise<List<WS.HttpResponse>> promises =F.Promise.waitAll(r1, r2, r3);

 

    //暂停处理,直到所有三个远程调用结束

   List<WS.HttpResponse> httpResponses = await(promises);

 

   render(httpResponses);

  }

}

回调Callbacks

还可以使用回调实现上面的示例。这次,await(…)方法包含了play.libs.F.Action实现,当三个远程调用结束后就调用这个回调方法。

public class AsyncTest extends Controller {

 

  public staticvoid remoteData() {

   F.Promise<WS.HttpResponse> r1 =WS.url("http://example.org/1").getAsync();

   F.Promise<WS.HttpResponse> r2 =WS.url("http://example.org/2").getAsync();

   F.Promise<WS.HttpResponse> r3 =WS.url("http://example.org/3").getAsync();

 

   F.Promise<List<WS.HttpResponse>> promises =F.Promise.waitAll(r1, r2, r3);

 

    //暂停处理,直到所有三个远程调用结束

    await(promises,new F.Action<List<WS.HttpResponse>>() {

      public voidinvoke(List<WS.HttpResponse> httpResponses) {

       render(httpResponses);

      }

    });

  }

}

HTTP response流streaming

既然不用中心请求也可执行循环,你或许会希望向浏览器发送结果变量的部分数据(不是全部)。这就是Content-Type:Chunked HTTP response大量httpresponse类型。它允许你多次使用多个块来发送http response。浏览器将实时接收这些块。

使用await(…)和continuations,就可以实现这个功能:

public static void generateLargeCSV() {

    CSVGeneratorgenerator = new CSVGenerator();

   response.contentType = "text/csv";

   while(generator.hasMoreData()) {

          StringsomeCsvData = await(generator.nextDataChunk());

         response.writeChunk(someCsvData);

    }

}

即使CSV生成需要1个小时,play也能同时使用单个线程处理多个请求,一旦为客户端的数据生成好后,play就会向客户端发送。

使用WebSockets

WebSockets是一种在浏览器和应用程序间实现双向通信的途径。在浏览器端使用“ws://” url:

newSocket("ws://localhost:9000/helloSocket?name=Guillaume")

在play端需要声明一条WS路由:

WS  /helloSocket           MyWebSocket.hello

MyWebSocket是一个WebSocketController。一个 WebSocket控制器和一个标准的http控制器很相似,但处理的内容不同:

  • 它有一个请求对象,但没有response对象
  • 它有一个可访问的session,但是只读的
  • 它没有renderArgs, routeArgs和flash域
  • 它只能从路由模式和QueryString里读取params
  • 它拥有两个通信通道:一进一出

当客户连接到ws://localhost:9000/helloSocket套接字时, Play将调用MyWebSocket.hello动作方法。一旦MyWebSocket.hello动作方法存在,套接字就会被关闭

因此一个非常基础的套接字示例应该是这个样子:

public class MyWebSocket extends WebSocketController {

 

    public staticvoid hello(String name) {

       outbound.send("Hello %s!", name);

    }

}

在这里,当客户端连接到socket时,它将接收到‘Hello Guillaume’消息,play随后将关闭这个socket。

当然,通常情况下你不需要立即关闭socket,用await(…)和continuations也能实现。

比如一个基础的Echo服务器:

public class MyWebSocket extends WebSocketController {

 

    public staticvoid echo() {

       while(inbound.isOpen()) {

            WebSocketEvent e = await(inbound.nextEvent());

             if(einstanceof WebSocketFrame) {

                  WebSocketFrameframe = (WebSocketFrame)e;

                 if(!e.isBinary) {

                     if(frame.textData.equals("quit")) {

                         outbound.send("Bye!");

                         disconnect();

                     } else {

                         outbound.send("Echo: %s", frame.textData);

                     }

                  }

             }

             if(einstanceof WebSocketClose) {

                Logger.info("Socket closed!");

             }

        }

    }

 

}

在上面的示例里,嵌套的‘if’和‘cast’很乏味,而且容易出错。即使这个简单示例也不容易处理。更复杂的多个联合线程的情况下将会有更多的事件类型,这将是一个恶梦。

这就是为什么我们要向你介绍在play.libs.F库里的基础的模式匹配的原因。

现在我们重写一下echo示例:

public static void echo() {

   while(inbound.isOpen()) {

        WebSocketEvent e = await(inbound.nextEvent());

        

         for(Stringquit: TextFrame.and(Equals("quit")).match(e)) {

            outbound.send("Bye!");

            disconnect();

         }

 

         for(Stringmsg: TextFrame.match(e)) {

            outbound.send("Echo: %s", frame.textData);

         }

        

        for(WebSocketClose closed: SocketClosed.match(e)) {

            Logger.info("Socket closed!");

         }

    }

}

0 0
原创粉丝点击