sábado, 11 de abril de 2015

Peticiones HTTP asíncronas con OkHttp

OkHttp

Es una librería de código abierto la cual permite realizar operaciones tanto en HTTP como en SPDY de manera sencilla y eficiente en ambientes Java (versión 1.7 como mínimo) y Android (2.3 como mínimo), sin necesidad de cambiar el código de la aplicación entre ambas plataformas, con una interfaz fluida.

Entre las virtudes que tiene esta librería es que automáticamente evalúa las características de la conexión y determina cómo operar de mejor manera, por ejemplo si se tiene soporte para SPDY, contenido HTTP comprimido (GZIP), recuperación de algunos errores en la conexión, entre otras, la cuales se pueden encontrar en su página principal: http://square.github.io/okhttp

Internamente está usando Okio, el cual permite optimizar las operaciones I/O mediante "Non-blocking I/O" (NIO).

Actualmente se encuentra en la versión 2.3.0 y como puede verse en su registro de Maven Central, es un proyecto en activo desarrollo: http://mvnrepository.com/artifact/com.squareup.okhttp/okhttp

Ejemplo Simple

OkHttp permite realizar todas las operaciones HTTP. En este caso se realizará una petición GET, para demostrar lo sencillo que es usar esta librería:
import java.io.IOException;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class Example {

  private static final OkHttpClient client = new OkHttpClient();

  public static void main(String[] args) throws IOException {
    Request request = new Request.Builder()
      .url("http://www.textfiles.com/art/sunlogo.txt")
      .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) {
      throw new IOException("Error in the response: " + response);
    }

    System.out.println(response.body().string());
  }
}

Esto imprimirá en la consola el resultado de una petición GET a la URL "http://www.textfiles.com/art/sunlogo.txt", lo cual será un logo algo nostálgico.

En la documentación de la librería se pueden encontrar algunos ejemplos un poco más avanzados: https://github.com/square/okhttp/wiki/Recipes

Ejemplo Asíncrono

Como se mencionaba inicialmente, OkHttp permite realizar operaciones HTTP de manera asíncrona y es en este caso en el cual se desarrollará un ejemplo un poco más completo, para explicar y demostrar mejor esta característica.

Para este ejemplo se desarrollará una aplicación Java, la cual realice una petición HTTP, la cual demorará 10 segundos en responder. La idea es que el hilo principal (main en este caso) continúe su ejecución a pesar de que no se ha obtenido la respuesta de la petición (HTTP).

Se usará la librería MockServer para subir un servicio local, el cual tenga el retraso (delay) que se necesita para este caso.

El ejemplo completo se puede encontrar en: https://github.com/guillermo-varela/OkHttp-example

Lo primero entonces será crear el archivo de configuración de Gradle con las librerías necesarias.

Nota: quienes aún no conozcan del todo Gradle pueden consultar: http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

build.gradle

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'eclipse'

sourceCompatibility = 1.7
targetCompatibility = 1.7
version = '1.0.0'

mainClassName = 'example.Example'

jar {
    manifest {
        attributes 'Implementation-Title': 'OkHttp Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.squareup.okhttp:okhttp:2.3.0'
    compile 'org.mock-server:mockserver-netty:3.9.6'
}

A continuación se tiene la clase con el código del servidor local y el petición asíncrona, junto con la explicación de lo que se hizo, aunque se tratará que el código sea lo más auto-documentado posible:
package example;

import java.io.IOException;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.time.DateFormatUtils;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.Delay;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;

import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class Example {

  private static final OkHttpClient client = new OkHttpClient();
  private static final ClientAndServer mockServer = ClientAndServer.startClientAndServer(8080);

  static {
    mockServer.when(
        HttpRequest.request()
          .withPath("/test")
        )
        .respond(
            HttpResponse.response()
              .withBody("OK")
              .withDelay(new Delay(TimeUnit.SECONDS, 10))
        );

    client.setConnectTimeout(15, TimeUnit.SECONDS);
    client.setWriteTimeout(15, TimeUnit.SECONDS);
    client.setReadTimeout(15, TimeUnit.SECONDS);
  }

  private static void print(String message) {
    System.out.println(message + ": " + DateFormatUtils.format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.sss"));
  }

  private static void doRequest() {
    print("Starting doRequest method");

    Request request = new Request.Builder()
      .url("http://localhost:8080/test")
      .build();

    client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Request request, IOException e) {
        e.printStackTrace();
        stopMockServer();
      }

      @Override
      public void onResponse(Response response) throws IOException {
        print("Response [" + response.body().string() + "]");
        stopMockServer();
      }

      private void stopMockServer() {
        print("Stopping MockServer");
        if (mockServer != null) {
          mockServer.stop();
        }
      }
    });

    print("Ending doRequest method");
  }

  public static void main(String[] args) {
    print("Starting main method");
    doRequest();
    print("Ending main method");
  }
}
  • Línea 20: Se crea el cliente HTTP de la librería OkHttp. Este cliente puede ser compartido para todos los hilos de la aplicación, siempre y cuando no se cambie su configuración (timeouts, interceptores, etc.)
  • Línea 21: Se crea e inicia el servidor local de MockServer en el puerto 8080.
  • Líneas 24-32: Dentro del bloque estático, se configura MockServer para que todas las peticiones a "http://localhost:8080/test" tengan el texto "OK" y se produzca con un demora de 10 segundos.
  • Líneas 34-36: Se indica al cliente HTTP que tenga timeouts superiores al delay recién configurado.
  • Líneas 39-41: Definición de un método para que al imprimir un texto por consola se le adicione la fecha y hora actual. Esto con el fin de comprobar el comportamiento asíncrono de la petición HTTP.
  • Líneas 43-72: Es el método "doRequest" y es el que realiza la petición asíncrona usando el cliente HTTP de OkHttp (OkHttpClient).
    • Líneas 46-48: Se indica que la petición se hará a la URL "http://localhost:8080/test".
    • Línea 50: Se solicita al cliente HTTP (variable client) que cree un nuevo llamado usando el método "newCall", pero en lugar de ejecutarlo usando el método "execute" como se hizo en el primer ejemplo, se indica que se agregue a la cola de llamados asíncronos usando el método "enqueue", es decir no se está ejecutando el llamado HTTP, sino que se está programando para su futura ejecución, por lo cual es una operación que no bloquea la ejecución. Normalmente la ejecución del llamado HTTP se inicia inmediatamente, pero si hay otros llamados en espera de ejecución puede tomar un tiempo.
    • Líneas 51-69: Para indicar qué hacer cuando se obtenga la respuesta del llamado HTTP, se crea una clase anónima de la interfaz "Callback" en la cual se definen operaciones tanto en caso de obtener la respuesta (onResponse), como en caso de error (onFailure). En caso de que no se necesite la respuesta, estos métodos pueden quedar vacíos. Adicionalmente se desarrolló un método extra "stopMockServer" el cual detiene la ejecución del servidor local MockServer, y se invoca tanto caso de éxito como de error.
  • Líneas 74-78: Es el método main de la aplicación y allí simplemente se indica que se debe ejecutar el método "doRequest".
Al ejecutar esta clase se obtiene un resultado como el siguiente:

Starting main method: 2015-04-11T22:22:11.011
Starting doRequest method: 2015-04-11T22:22:11.011
Ending doRequest method: 2015-04-11T22:22:11.011
Ending main method: 2015-04-11T22:22:11.011
Response [OK]: 2015-04-11T22:22:22.022
Stopping MockServer: 2015-04-11T22:22:22.022

Como puede verse, la ejecución del método main (y del método doRequest) se realizó en menos de un milisegundo. Internamente OkHttp creó un hilo (dentro de un pool de hilos) y fue allí donde se procesó el llamado HTTP y se ejecutó el callback desarrollado.

Una manera adicional de comprobarlo es adicionando puntos de interrupción (breakpoints), uno al final del método "main" y otro en el método "onResponse" del callback:
Figura 1 - Hilo adicional para los llamados HTTP

De esta manera se puede, por ejemplo informar a un sistema externo que un usuario de nuestra aplicación a realizado alguna acción, sin generar demoras adicionales en la respuesta al usuario por dicho llamado. Esto sin necesidad de manipular manualmente hilos dentro del código de la aplicación.

Notas adicionales sobre los hilos de OkHttp

Referencias

http://square.github.io/okhttp
https://github.com/square/okhttp
https://github.com/square/okhttp/wiki/Recipes
http://square.github.io/okhttp/javadoc
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
http://www.mock-server.com

1 comentario:

  1. Hola aquí en mi domicilio hicieron muchos cambios para ocultar cosas y pues no se vale voy a intentar buscar así y ojalá tenga yo éxito muchas grasias y muy buen trabajo

    ResponderBorrar