¡La versión 2.0 de Play ya está lista! Ayúdanos a traducir la documentación de la útlima versión y sigue nuestro progreso.

Manuales, tutoriales & referencias

Consulte

Contenidos

Elija la versión

Buscar

Busque con google

Libros

Programación asincrónica con HTTP

Esta sección explica cómo trabjar con pedidos http de manera asincrónica en una aplicación de Play para obtener las aplicaciones típicas long-polling, streaming y otras aplicaciones del estilo comet que pueden escalar hasta miles de conexiones concurrentes.

Suspendiendo solicitudes HTTP

Play está pensado para trabajar con requests HTTP muy cortos. Usa conjunto de hilos de ejecución (thread pool) de longitud fija para procesar los pedidos HTTP encolados por el conector HTTP. Para obtener resultados óptimos, el pool de hilos ha de ser tan pequeño como sea posible. Normalmente usamos el valor óptimo de número de procesadores + 1 como valor por defecto del tamaño del pool.

Esto significa que si el tiempo de proceso de un request es muy largo (por ejemplo, esperando a un cálculo muy extenso) bloqueará el pool y la aplicación no podrá atender otros request con la misma eficiencia. Por supuesto, se podrían añadir más hilos al pool de ejecución, pero esto implica recursos desperdiciados y además, el tamaño del pool nunca será infinito.

Tomemos como ejemplo una aplicación de chat en la que los navegadores envían un request HTTP bloqueante y esperan a que aparezca un nuevo mensaje. Estos requests pueden ser muy largos (normalmente varios segundos) y bloquean el pool de hilos de ejecución. Si tu plan es permitir que se conecten simultáneamente 100 usuarios a la aplicación de chat, necesitarás un pool de al menos 100 hilos. Vale, esto es factible. Pero, ¿y si queremos 1000 usuarios? ¿o 10.000?

Para resolver estos casos, Play permite suspender temporalmente un request. El pedido HTTP seguirá conectado, pero la ejecución del mismo se sacará del pool y se intentará de nuevo más tarde. Puede decirle a Play que intente retomar la ejecución del pedido HTTP luego de un lapso de tiempo espcífico, o esperar a que el valor de una Promise esté disponible.

Nota. Puedes ver un ejemplo real en samples-and-tests/chat.

Por ejemplo, esta acción lanza un job muy largo y espera a su terminación antes de devolver los resultados al response HTTP.

public static void generatePDF(Long reportId) {
    Promise<InputStream> pdf = new ReportAsPDFJob(report).now();
    InputStream pdfStream = await(pdf);
    renderBinary(pdfStream);
}

Aquí usamos await(…) para decirle a Play que suspenda la atención del request hasta que el proceso retorne el valor de Promise<InputStream>.

Reanudaciones (continuations)

Como play tiene que recuperar el hilo que estaba utilizando para servir otras requests, necesariamente tiene que suspender tu código. En la versión anterior de Play, el equivalente del await(…) era waitFor(…), que suspendía tu acción, y después la llamaba desde el principio.

Para que sea más fácil trabajar con código asincrónico hemos introducido las reanudaciones (continuations). Las reanudaciones permiten que se suspenda tu código y se reanude transparentemente. Así puedes escribir tu código de forma sumamente imperativa:

public static void computeSomething() {
    Promise<String> delayedResult = veryLongComputation(…);
    String result = await(delayedResult);
    render(result);
}

De hecho aquí, tu código se ejecutará en dos pasos, en dos hilos diferentes. Pero como ves, esto es transparente para el código de tu aplicación.

Mediante el uso de await(…) y las reanudaciones, podrías escribir el siguiente bucle:

public static void loopWithoutBlocking() {
    for(int i=0; i<=10; i++) { 
         Logger.info(i);
         await("1s");
    }
    renderText("Fin del bucle");
}

Incluso cuando se usa un sólo hilo para procesar las requests, que es el comportamiento por defecto en modo desarrollo, Play es capaz de ejecutar concurrentemente estos bucles para varios requests al mismo tiempo.

Un ejemplo más realista consiste en recuperar asincrónicamente el contenido de URLs remotas. El siguiente ejemplo ejecuta tres solicitudes HTTP en paralelo: cada llamada al método play.libs.WS.WSRequest.getAsync() ejecuta un request GET de manera asincrónica y devuelve un play.libs.F.Promise. El método de acción suspende la request HTTP entrante mediante la llamada a await(…) a la espera de que se completen las tres instancias de Promise. Cuando estas llamadas remotas tengan una respuesta, otro hilo resumirá el proceso y generará una response.

public class AsyncTest extends Controller {
  public static void 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);
    // Se suspende el proceso aqui, hasta la finalización de las tres llamadas remotas.
    List<WS.HttpResponse> httpResponses = await(promises);
    render(httpResponses);
  }
}

Callbacks

Una forma diferente de implementar el ejemplo anterior de tres llamadas remotas asincrónicas es mediante el uso de Callbacks. Esta vez, la llamada a await(…) incluye una implementación de play.libs.F.Action, que es un callback, es decir un método que se ejecuta cuando las promises han terminado.

public class AsyncTest extends Controller {
  public static void 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);
    // Suspender el proceso aquí hasta que las tres llamadas remotas estén completas.
    await(promises, new F.Action<List<WS.HttpResponse>>() {
      public void invoke(List<WS.HttpResponse> httpResponses) {
        render(httpResponses);
      }
    });
  }
}

Streaming de respuestas HTTP

Ahora que ya sabe como hacer bucles sin bloquear el request, puede que quiera enviar los datos al navegador tan pronto como tenga alguna parte de los resultados disponibles. Este es el objetivo del tipo de respuesta HTTP Content-Type:Chunked. Permite enviar la response HTTP varias veces dividiéndola en varias porciones (chunks). El navegador recibirá las porciones tan pronto como sean publicadas.

Mediante el uso de await(…) y las reanudaciones, puede hacerlo de esta forma:

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

Incluso si la generación de CSV tarda una hora, Play es capaz de procesar simultáneamente varias request usando un solo hilo, devolviendo los datos generados al cliente tan pronto como estén disponibles.

Uso de WebSockets

Los WebSockets son una forma de abrir un canal de comunicación bidireccional entre un navegador web y tu aplicación. En el lado del navegador, se abre un socket utilizando urls del tipo “ws://”:

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

En el lado de Play se declara una ruta WS:

WS   /helloSocket            MyWebSocket.hello

MyWebSocket es un controlador de tipo WebSocketController. Un controlador de WebSocket es como el controlador estándar HTTP pero con algunas diferencias:

  • Tiene un objeto request, pero no un objeto response.
  • Tiene acceso a la sesión, pero sólo para lectura.
  • No tiene renderArgs, routeArgs ni flash.
  • Puede leer parámetros sólo a partir del patrón de la ruta o desde la cadena QueryString.
  • Tiene dos canales de comunicación: inbound (entrada) y outbound (salida).

Cuando el cliente se conecta al socket ws://localhost:9000/helloSocket, Play invoca al método de acción MyWebSocket.hello. Cuando el método termina, se cierra el socket.

Así que un ejemplo muy básico de socket podría ser:

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

En este caso, cuando el cliente se conecta al socket, recibe el mensaje ‘Hello Guillaume’, y luego Play cierra el socket.

Normalmente, no querremos cerrar el socket inmediatamente, pero es fácil lograrlo con await(…) y las reanudaciones.

Por ejemplo, un servidor básico de eco:

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")) {
                          outbound.send("Bye!");
                          disconnect();
                      } else {
                          outbound.send("Echo: %s", frame.textData);
                      }
                  }
             }
             if(e instanceof WebSocketClose) {
                 Logger.info("Socket closed!");
             }
        }
    }
}

En el ejemplo anterior, la serie de ‘if’ anidados y ‘cast’, es sumamente tediosa de escribir y propensa a errores. Y es aquí donde Java muestra sus limitaciones. Incluso en el caso sencillo que vemos aquí no es fácil de manejar. Y para casos más complicados donde pueden combinarse varios streams, y tener más tipos de eventos, se vuelve una verdadera pesadilla.

Por eso hemos introducido la librería play.libs.F que implementa una comparación de báisca patrones en Java.

Así que podemos escribir el ejemplo del eco de esta forma:

public static void echo() {
    while(inbound.isOpen()) {
         WebSocketEvent e = await(inbound.nextEvent());
         for(String quit: TextFrame.and(Equals("quit")).match(e)) {
             outbound.send("Bye!");
             disconnect();
         }
         for(String msg: TextFrame.match(e)) {
             outbound.send("Echo: %s", frame.textData);
         }
         for(WebSocketClose closed: SocketClosed.match(e)) {
             Logger.info("Socket closed!");
         }
    }
}

Próximos pasos

Ahora, veremos como hacer Requests con Ajax.