Operaciones HTTP En Android Con El Cliente HttpURLConnection

Este artículo explicará de qué forma se puede enviar y recibir datos hacia un servidor desde una aplicación Android, con el propósito de introducir a los desarrolladores a la creación de servicios en la nube desde fuentes de datos externas.

En primera instancia se expondrán conocimientos introductorios sobre el protocolo HTTP, el cual habilita el intercambio de información.

Luego verás a través de un ejemplo práctico como leer y postear comentarios en un servidor externo. Y Adicionalmente se integrarán las tareas asíncronas para asegurar el correcto funcionamiento del hilo principal.

Finalmente se concluye con aspectos adicionales del mecanismo usado para la conexión y observaciones que pueden ayudarte en la conexión de las aplicaciones Android.

Introducción al funcionamiento del protocolo HTTP

Las conexiones que haremos desde nuestras aplicaciones Android hacia los servidores web siguen un estándar internacional llamado Hipertext Transfer Protocol HTTP. Este protocolo consiste en reglas sencillas de transferencia de recursos o archivos entre equipos interconectados a una red.

Al equipo que hace la petición para enviar u obtener datos se le llama Cliente y al que contiene el recurso o el espacio para almacenar es llamado Servidor.

La comunicación se establece a través de una petición de envío, la cual contiene los datos del cliente, como el sistema operativo que usa, el navegador web desde donde se hace la petición, la ubicación del archivo solicitado (URL), etc.

Diagrama de una petición y respuesta en el protocolo HTTP
Una petición puede tener múltiples objetivos dependiendo del método que se elija. Los tipos de peticiones más comunes son el Retorno de datos y la Publicación de datos. Técnicamente se les conoce como los métodos GET y POST.

La búsqueda de una página web a través de la URL es un buen ejemplo de una petición GET, donde el cliente especifica la URL y el servidor retorna en la información HTML necesaria para que el navegador realice su respectivo parsing.

El ejemplo más popular del método POST se refleja en el envío de información desde un formulario hacia la base de datos del servidor. Aquí hacemos lo contrario, dictaminamos los datos y el servidor los recibe para almacenarlos y darles persistencia.

Cada vez que entras a hermosa programación desde tu navegador la comunicación HTTP sería similar a esta:

GET /index.html
Host: www.hermosaprogramación.com
User Agent: Mozilla/4.0 (Compatible; MSIE 7.0; Windows NT 6.0)
Accept: */*

Dicha petición es recibida por el Servidor, quién arroja la respuesta dependiendo del estado del recurso solicitado, que en este caso es el archivo HTML que representa el Home de Hermosa Programación.

HTTP/1.0 200 OK
Date: Fri, 27 Dec 2014 23:59:59 GMT
Content-Type: text/html
Content-Length: 1467

<html>
<body>
<h1>Hermosa Programación</h1>
(El contenido restante)
  .
  .
  .
</body>
</html>

Como se nota, el estado de respuesta es 200 OK, un código que significa que todo marcha sobre ruedas con este recurso. En seguida se ubican metadatos asociados a la fecha de consulta, el tipo de contenido enviado, su tamaño y al final el contenido HTML. Con esta información, tu navegador web ya puede implementar la lógica necesaria para mostrar la página web.

Ahora veamos el esquema de una petición POST, cuyo objetivo es enviar los datos Nombre y Precio hacia la base de datos del servidor:

POST /data/Insertar-Productos.php HTTP/1.0
From: frog@jmarshall.com
User-Agent: HTTPTool/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 25

Nombre=Motorola&Precio=2000

En este caso se indica la ruta de ubicación donde se publicaran los resultados, una cuenta de correo como el origen de la petición, el tipo de contenido estándar para los formularios, el tamaño de los datos y finalmente dos pares clave-valor encriptados con UTF-8.

Si deseas que tus aplicaciones tengan acceso total a la conexión de tu dispositivo Android es necesario que incluyas los siguientes permisos en tu archivo AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Ejemplo: Aplicación Monstalkers Para Lectura Y Posteo De Comentarios

Monstalkers es una aplicación de ejemplo creada para mostrar el uso del cliente HttpURLConnection.

Su objetivo es mostrar como cargar comentarios almacenados en un servidor externos a través del parsing de un arreglo JSON.

Adicionalmente permitirá al usuario crear y enviar su propio comentario al servidor.

Puedes descargar el código siguiendo estas instrucciones:

En cuanto a diseño, Monstalkers se compone de dos actividades.

La primera es la actividad principal llamada MainActivity, donde se muestra un ListView que despliega todos los comentarios por orden cronológico.

Su Action Bar tiene dos botones de acción, los cuales permiten añadir un nuevo comentario y refrescar la vista respectivamente.
Aplicación Android para Añadir Comentarios a un Servidor
Cuando se presiona el action button para añadir, inmediatamente se despliega la actividad FormActivity, la cual contiene un sencillo campo de edición para digitar el comentario. El usuario decidirá el fin del comentario presionando el botón Enviar o Cancelar.
Actividad Android con un Formulario para añadir Comentarios

Usar el cliente HttpURLConnection

La clase HttpUrlConnection del paquete java.net.* permite a nuestros dispositivos android asumir las características de un cliente HTTP ligero. Su funcionamiento está condicionado a las versiones superiores a Gingerbread. Para versiones anteriores debes usar el cliente HttpClient de Apache.

Con esta clase podremos recibir y enviar información a través de la web, lo que potenciará nuestras aplicaciones Android. A continuación se muestran los pasos que se deben realizar para establecer una conexión exitosa.

Comprobar si la conexión a la red es posible

Antes de iniciar el cliente Http debes comprobar si la conexión del dispositivo está habilitada, ya que puede ser posible que el Wi-fi no esté disponible o simplemente la conexión de red está fuera del rango. Para comprobar el estado de conexión usaremos los métodos getActiveNetworkInfo() e isConnected():

ConnectivityManager connMgr = (ConnectivityManager) 
    getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
    // Operaciones http
} else {
    // Mostrar errores
}

Se ha usado la clase ConnectivityManager para obtener las características actuales de la conexión. Esta información la guardamos en un elemento del tipo NetworkInfo con el método getActiveNetworkInfo(). Luego comprobamos si se retornó algún dato y si además el dispositivo está conectado con isConnected().

Abrir la conexión hacia el servidor

El primer paso para iniciar la comunicación es abrir la conexión hacia el recurso alojado en el servidor. Para ello se usa el método openConnection() de la clase URL. El resultado que se obtenga debe ser casteado a HttpUrlConnection para que el cliente sea instanciado:

URL url = new URL("http://monstalkers.hostoi.com/data/get_all_comments.php");   
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

Obtener datos con el método GET

Si deseas descargar datos desde la URL especificada simplemente usas el método getIntpuStream() para obtener el flujo de datos asociado al recurso que se encuentra en esa dirección:

URL url = new URL("http://monstalkers.hostoi.com/data/get_all_comments.php");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
 InputStream in = new BufferedInputStream(urlConnection.getInputStream());
 // Acciones a realizar con el flujo de datos
finally {
 urlConnection.disconnect();
}

Es importante que al finalizar tus operaciones de conexión liberes la memoria asociada a la instancia de la conexión realizada. Para ello usa el método disconnect(), el cual pone a disposición de nuevo una futura reconexión.

Recuerda que un tipo InputStream debe ser decodificado para interpretar su contenido, ya sea texto plano, imagen, JSON, audio, etc. Dependiendo del objetivo así mismo debes usar los métodos y técnicas correspondientes.

Postear información con el método POST

Si deseas publicar información en un servidor debes abrir la conexión al igual que con GET. Luego se indica a la conexión que se permite el envío de datos hacia el servidor con el método setDoOutput().

Seguidamente se declaran los datos que se enviarán al destino, para los cuales debes declarar el tamaño que ocuparán para ser transmitidos por el flujo.

Si su tamaño es fijo, entonces usa el método setFixedLengthStreamingMode(), quién recibe como parámetro la cantidad de bytes.

Si el tamaño es incierto (normalmente esta situación se da en transmisiones Streaming), entonces usa setChunkedStreamingMode().

Reuniendo todas estas características, Monstalkers hace su publicación de comentarios de la siguiente forma:

// Obtener la conexión
HttpURLConnection con = null;

try {
    // Construir los datos a enviar
    String data = "body=" + URLEncoder.encode(comment,"UTF-8");

    con = (HttpURLConnection)url.openConnection();

    // Activar método POST
    con.setDoOutput(true);

    // Tamaño previamente conocido
    con.setFixedLengthStreamingMode(data.getBytes().length);

 // Establecer application/x-www-form-urlencoded debido a la simplicidad de los datos
    con.setRequestProperty("Content-Type","application/x-www-form-urlencoded");

    OutputStream out = new BufferedOutputStream(con.getOutputStream());

    out.write(data.getBytes());
    out.flush();
    out.close();

} catch (IOException e) {
    e.printStackTrace();
} finally {
    if(con!=null)
    con.disconnect();
}

El código anterior muestra como se envía el cuerpo de un comentario digitado por el usuario en la actividad ActivityForm. En primera instancia se construye una cadena llamada data, donde guardaremos el par clave-valor del comentario con el estándar de formularios “clave= valor”. Para ello usamos la clave “body” como representación del cuerpo del comentario como tal, cuyo valor se encuentra almacenado en la variable comment.

Luego se encriptó el parámetro con el método estático encode de la clase URLEncoder. Esta clase permite ajustar la cadena al sistema UTF-8 para que sea compatible con el tipo de contenido application/x-www-form-urlencoded, de acuerdo a los estándares de envío de información para el método POST establecido por la W3C.

Aunque en este ejemplo se usa un solo par clave-valor, a menudo necesitarás enviar múltiples pares. Para ello solo debes concatenarlos con el carácter ‘&’ de acuerdo al estándar.

El siguiente paso fue declarar el envío de datos con setDoOutput(), seguido se estableció el valor en bytes de nuestros datos y además se añadió el tipo de contenido con setRequestProperty(). Este método te permite añadir cabeceras a la petición que envíes.

Finalmente se obtiene acceso al sistema de archivos del servidor con getOutputStream(), el cual retorna en un flujo de datos en donde escribirás los datos de los comentarios a través de los métodos flush() y write().

Subir un archivo hacia un servidor con el método POST

En la sección anterior se usó el tipo de contenido application/x-www-form-urlencoded para enviar datos atómicos hacia el servidor, pero… ¿Qué hacer para subir un archivo?

Una de las formas es usar el tipo de contenido multipart/form-data del estándar W3C para enviar datos binarios de gran tamaño. Pero la verdad me resulta complicado preparar todas las sentencias de texto requeridas para dirigir el flujo de información.

En su lugar puedes publicar el archivo directamente como bytes puros a través de getOutputStream() de la siguiente forma:

File file = new File("ruta_del_archivo");
URL url = new URL("http://www.tuservidor.com/");
HttpURLConnection con = null;

try{
    con = (HttpURLConnection)url.openConnection();

    // Activar método POST
    con.setDoOutput(true);

    // Tamaño desconocido
    con.setFixedLengthStreamingMode(0);

    OutputStream out = con.getOutputStream();
    // Usas tu método ingeniado para convertir el archivo a bytes
    out.write(convertfileToBytes(file));
    out.flush();
    out.close();
}finally{
    if(con!=null)
    con.disconnect();
}

Establecer peticiones HTTP en segundo plano usando AsyncTask

El tiempo que tarda la transmisión de datos en la red depende de muchos factores, como el tamaño de la información a intercambiar, los tiempos de latencia, la congestión del servidor o incluso la ejecución de múltiples tareas distintas en el sistema operativo. Cualquiera de estas situaciones puede prolongar el tiempo de una petición indefinidamente.

Pero como tú ya sabes, las tareas asíncronas nos auxilian en estas situaciones. Solo debemos crear una nueva instancia de la clase AsyncTask e incluir la petición al servidor en el método doInBackground() y luego actualizar los resultados visuales en onPostExecute().

Lee también Uso de Hilos y tareas asíncronas (AsyncTask) en Android

Por ejemplo, la obtención de todos los comentarios que se han guardado en el servidor de monstalker ha sido encapsulada en una clase llamada GetCommentsTask:

public class GetCommentsTask extends AsyncTask<URL, Void, List<String>> {

    @Override
    protected List doInBackground(URL... urls) {

        List comments = null;

        try {

            // Establecer la conexión
            con = (HttpURLConnection)urls[0].openConnection();

            // Obtener el estado del recurso
            int statusCode = con.getResponseCode();

            if(statusCode!=200) {
                comments = new ArrayList();
                comments.add("El recurso no está disponible");
                return comments;
            }
            else{

                /*
                Parsear el flujo con formato JSON a una lista de Strings
                que permitan crean un adaptador
                 */
                InputStream in = new BufferedInputStream(con.getInputStream());

                JSONCommentsParser parser = new JSONCommentsParser();

                comments = parser.readJsonStream(in);

            }

        } catch (Exception e) {
            e.printStackTrace();

        }finally {
            con.disconnect();
        }
        return comments;

    }

    @Override
    protected void onPostExecute(List s) {

        /* Se crea un adaptador con el el resultado del parsing
        que se realizó al arreglo JSON
         */
        ArrayAdapter adapter = new ArrayAdapter(
                getBaseContext(),
                android.R.layout.simple_list_item_1,
                s);

        // Relacionar adaptador a la lista
        comments.setAdapter(adapter);
    }
}

La secuencia de la tarea tendría la siguiente estructura:

  • Abrir la conexión hacia la URL en doInBackground().
  • Obtener el estado del recurso con getResponseCode(). Si la respuesta no es 200 OK, entonces preparar un valor de retorno que indique el error sucedido.
  • En caso de éxito, entonces se obtiene el flujo de datos con getInputStream()
  • Parsear o formatear dependiendo del flujo. En este caso particular la información viene como un arreglo de objetos JSON. Para traducirlo en información legible se ha implementado la clase JSONCommentsParser, la cual usa la clase android.util.JsonReader para leer los bytes y recorrer la estructura, de tal forma que al final se retornen los datos que interesan (una lista de comentarios).
  • Finalmente se realizan las acciones necesarias en onPostExecute(). MonsTalkers implementa crea un adaptador nuevo basado en el parámetro recibido y luego lo asocia al ListView.

Si deseas formatear un flujo a Bitmap puedes usar el método estático decodeStream() de la clase BitmapFactory, el cual recibe como parámetro un tipo InputStream. Si en lugar de un bitmap, tienes texto plano, entonces puedes usar un objeto InputStreamReader, el cual puede convertir bytes a caracteres (char) y finalmente convertir a String.

Otros métodos para peticiones HTTP en Android

El método por defecto asociado a un objeto HttpURLConnection es GET. Cuando abres la conexión es sencillo obtener el flujo asociado a la URL con tan solo usar getInputStream().

El método POST tampoco es complicado de emplear, ya que solo se activa con setDoOutput() para estructurar los datos a enviar y escribir sobre el flujo de salida.

Pero en el caso de otros métodos como OPTIONS, HEAD, PUT, DELETE o TRACE debes usar el método setRequestMethod() para hacer efectiva la petición. No obstante este también se puede usar para especificar los métodos GET y POST.

con.setRequestMethod("GET");

Otra característica adicional de la clase HttpURLConnection es la capacidad de establecer los tiempos de caducidad de conexión y tiempos de caducidad de lectura a través de los métodos setConnectTimeout() y setReadTimeout():

// Expirar a los 10 segundos si la conexión no se establece
con.setConnecTimeout(10000)
// Esperar solo 15 segundos para que finalice la lectura
con.setReadTimeout(15000)

Conclusiones y Recomendaciones

El cliente HttpURLConnection nos permite usar métodos de petición de forma eficaz y amigable. Solo se indica la ubicación del recurso a través de una URL, luego defines tu propósito. Si el objetivo es obtener información, entonces asegúrate de crear un método que parsee la información con el formato correcto.

Por el contrario si se enviarán datos, entonces procura constituir pares clave-valor siguiendo el estándar. Adicionalmente recuerda usar tareas asíncronas ejecutar tus peticiones, de lo contrario el framework de Android te arrojará una excepción perteneciente al modo restringido, donde se te explica que no es permitido ejecutar peticiones en el hilo principal.

Aunque este cliente es una alternativa congruente, Google recomienda usar la librería HTTP Volley. Esta herramienta permite transmitir datos a través de la red en un alto nivel, debido a que establece una capa superior para evitar manejar el flujo de datos directamente, relaciona la fase de parsing con facilidad y optimiza el rendimiento de las operaciones. Pero este será un tema para otro artículo.