AndroidWebServer

La aplicación

Para construir un servidor web es necesario contar con un programa que haga las veces de “servidor”, es decir, que esté escuchando siempre en uno o varios sockets a la espera de algún cliente para servirle la información solicitada.
La interfaz gráfica será muy sencilla, una etiqueta para mostrar la IP del servidor, un botón para encender o apagar el servidor web y una consola para ver que es lo que está sucediendo, es decir llevar un registro de eventos.

Antes de continuar: Este trabajo está también disponible en versión PDF, lo cual considero, es más cómodo de leer. Lo puedes descargar desde aquí.

Interfaz gráfica

Como se menciona anteriormente, la interfaz es bastante simple, pensando en que de inicio el servidor no es una aplicación que tenga que ser manipulada constantemente por el usuario final.
La interfaz queda abierta a ampliaciones, según se vaya dotando de funcionalidad a esta aplicación.

ServerSocket

Sobre la plataforma Android, el manejo de los sockets es similar al que se realiza en cualquier otra aplicación de escritorio.
Así que simplemente abrimos un ServerSocket
//Fragmento de código del archivo WebServerActivity.java
ServerSocket ss;
try {
    ss = new ServerSocket(puerto);
    while (!isCancelled()) {
        Socket entrante = ss.accept();
//...

Permisos

Para hacer uso de los sockets es necesario otorgar permiso de INTERNET en el manifiesto de la aplicación, lo cual se consigue añadiendo la línea
<uses-permission android:name="android.permission.INTERNET" />
Ya que de otro modo  obtendremos una excepción de IO.

Complicaciones con el manejo de los sockets

En Android no es posible trabajar con sockets en el hilo principal, por tanto no es posible utilizar el código anteriormente mostrado dentro de cualquier método. Por ejemplo, no podemos colocarlo en el método onCreate, ni colocarlo directamente dentro de algún manejador de eventos ya que al tratar de ejecutarlo nos lanzará la excepción:
android.os.NetworkOnMainThreadException
Por tanto es necesario utilizar una clase interna que extienda de AsyncTask para trabajar con el ServerSocket
//Fragmento de código del archivo WebServerActivity.java
public class ServidorWeb
    extends AsyncTask<WebServerActivity, String, Void> {

En cuyo método sobreescrito doInBackground, añadiremos el código necesario para inicializar el socket:
//Fragmento de código del archivo WebServerActivity.java
protected Void doInBackground(WebServerActivity... arg0) {
    try {
        ss = new ServerSocket(puerto);
        while (!isCancelled()) {
            Socket entrante = ss.accept();

Para comenzar la ejecución basta con crear un objeto de nuestra clase nueva, ServidorWeb y llamar al método execute, por la manera en la que se trabajó, al método es necesario pasarle la referencia a la clase de nuestra actividad.
//Fragmento de código del archivo WebServerActivity.java
ServidorWeb servidorWeb;
servidorWeb = new ServidorWeb();
servidorWeb.execute(this);

Registro de eventos

En la interfaz gráfica incluimos un control (View) para llevar el registro de eventos, un símil de consola, pero sin serlo necesariamente, ya que no admite parámetros de entrada ni escritura sobre ella.
Para añadir texto bastaría con llamar al método setText, sin embargo es aquí en donde existe otra limitante de Android, y es que los controles solo pueden ser manipulados directamente por el hilo que los creó, todos los controles son creados por el hilo de UI, y no se pueden modificar por otro sino ese. Recordaremos que los eventos son lanzados desde el servidor, que está en otra clase, y peor aún, corre en otro hilo.
Hacer una llamada como: ServerConsole.append(Html.fromHtml("Buen día señor sol")); desde cualquier otra clase que no sea la de la actividad en cuestión resultará en una excepción:
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Para nuestra suerte, existe una solución y es haciendo uso del método onProgressUpdate, con el cual podemos modificar cosas que estén corriendo en el hilo de UI
//Fragmento de código del archivo WebServerActivity.java
@Override
protected void onProgressUpdate(String... values) {
      /* logView es un método implementado en la clase WebServerActivity, en donde fueron creados los controles de la interfas. Se encarga de añadir el texto al control ServerConsole */
      logView((values[0]), level);
}
Contrario a lo que pareciera uno no debe llamar directamente a este método. Si no hacer uso del método (también proporcionado por la clase AsyncTask) publishProgress el cual, a cada llamada, provocará que el método onProgressUpdate se ejecute.
Esa es la manera en la que estaremos modificando la interfaz desde el hilo en el que correrá nuestro servidor.

Apagando y encendiendo el servidor

Para terminar la ejecución del código existente dentro de una AsyncTask tenemos a nuestra disposición el método cancel, del cual nos vamos a valer para apagar el servidor si es que no queremos que siga procesando solicitudes, dada la arquitectura de nuestro servidor, también es necesario cerrar el socket, ya que al estar esperando un cliente, no se apaga de inmediato.
//Fragmento de código del archivo WebServerActivity.java
servidorWeb.cancel(true);
servidorWeb.closeSocket();

Aceptando, leyendo y respondiendo la petición

Una vez que todo el esqueleto esté armado, es necesario adentrarnos a un nivel inferior, al núcleo del servidor, la forma en la que se hace una petición web es aceptando la conexión con un socket creado internamente, a través del cual podemos leer y escribir información al cliente que lo solicite.
Para realizar esto tenemos el siguiente código dentro de doInBackground:
//Fragmento de código del archivo WebServerActivity.java
///Mientras la tarea no haya sido cancelada
while (!isCancelled()) {
      ///Quedamos a la espera de un cliente nuevo
      Socket entrante = ss.accept();
///Actualizamos la “consola” con la información del cliente
///aceptado
      publishProgress("Cliente aceptado: <b>"                                     + entrante.getInetAddress().getHostName() + "</b>",
            "1");
///Usaremos la clase auxiliar PeticionWeb para procesar
      PeticionWeb pw = new PeticionWeb(entrante, this);
///Dado que PeticionWeb extiende de Thread, lo iniciamos
      pw.start();
}

La clase PeticionWeb


//Fragmento de código del archivo PeticionWeb.java
public class PeticionWeb extends Thread {
      private ServidorWeb web;
      private Socket scliente = null;
Esta clase será el auxiliar encargado de procesar la petición, extiende de Thread para que corra de manera que no bloquee a las demás peticiones entrantes. En ella se obtienen los flujos de entrada y de salida, que son los canales de comunicación con el cliente que está interesado en consumir de nuestro servidor.
El siguiente código es la implementación sobreescrita de run adaptada a nuestras necesidades.

//Fragmento de código del archivo PeticionWeb.java
@Override
public void run() {
      try {
            /* Obtenemos los flujos de entrada y salida, así como un objeto instancia de PrintWriter para escribir en el cliente */
            BufferedReader in = new BufferedReader(new InputStreamReader(
                        scliente.getInputStream()));
            OutputStream outputStream = scliente.getOutputStream();
            PrintWriter out = new PrintWriter(new OutputStreamWriter(
                        outputStream), true);
            out.flush();

            String cadena = "";
            /* ProcesaPeticion es la clase que usaremos para recuperar el contenido de los archivos almacenados en las carpetas del servidor */
            ProcesaPeticion pp = null;
            /* Leemos del flujo de entrada, mientras la lectura no sea nula continuaremos */
            while ((cadena = in.readLine()) != null) {

                  if (cadena != null ///es
&& !"".equals(cadena)) {
/* La lectura de una línea en blanco marca el final de la petición HTTP */
                        if (cadena.startsWith("GET")) {
                        /* Revisamos el contenido de la cadena que comience con GET, ya que es ahí en donde viene la información del archivo que se desea consultar */
                             cadena = cadena.substring(4);
                  cadena = cadena.substring(0,  
cadena.indexOf(" "));
                             web.log("Petición: " + cadena);
/* Dejamos que la clase auxiliar trate de buscar el archivo y devolver una respuesta */
                             pp = new ProcesaPeticion(cadena);
                        }
                  } else
                        break;
            }
/* Escribimos las cabeceras de la respuesta y el contenido solicitado, el cual nos lo devolverá el método getResponse */
            out.println("HTTP/1.0 200 OK");
            out.println("Server: " + web.getIpAddr());
            out.println("Date: " + new Date());
            out.println("Content-Type: text/html");
            out.println("");
            if (pp != null) {
                  out.println(pp.getResponse());
            }
            out.flush();
            out.close();
      } catch (Exception e) {
            web.log("Error en servidor: " + e.toString(), "2");
      }
}

La clase ProcesaPeticion

Esta clase es el puente entre los archivos físicos en el servidor y la respuesta que el cliente espera recibir del servidor, no olvidar que, como estamos guardando los archivos del servidor en la memoria externa del dispositivo necesitamos añadir otro permiso al manifiesto de la app.
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
El código:
public class ProcesaPeticion {
      String path;
      String response;

      public ProcesaPeticion(String path) throws Exception {
            this.path = path;
            // Checamos si está montada la SD:
            String state = Environment.getExternalStorageState();
            if (Environment.MEDIA_MOUNTED.equals(state)) {
                  // Variables File que nos ayudarán más adelante
                  File sdCard = Environment.getExternalStorageDirectory()
,peticion
,contenedor;
                  // Los documentos deberán existir en la carpeta
                  // "AndroidWebServer/wwwroot"
                  contenedor = new File(sdCard.getAbsolutePath()
                             + "/AndroidWebServer/wwwroot");
                  contenedor.mkdirs();
                  File request = new File(contenedor.getAbsolutePath() + path);
                  StringBuilder strLine = new StringBuilder();
                  if (request.exists() && request.isFile()) {
                        try {
                             // Lectura del archivo
                        } catch (Exception e) {
                        }
                  } else if(request.exists() && request.isDirectory()){
                        // Aquí manejaremos cuando la solicitud sea a un // directorio
                  } else {
                        // La ruta que solicitó no existe
                  }
                  response = strLine.toString();
            } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            } else {
                  throw new Exception("No hay una memoria SD en el dispositivo");
            }
      }
     
}



Referencias

Wikipedia
Documentación de Android Developers
… y mucho de http://stackoverflow.com



Contacto

Si tienes alguna duda con respecto a la aplicación puedes dejar un comentario en la siguiente liga
Enviarme un correo a antonio.feregrino@gmail.com
Mandarme un tweet a @fferegrino o @IO_Exception
No olvides que la aplicación está disponible en Google Play, la tienda de aplicaciones para Android en la siguiente liga:


¡Saludos!
@fferegrino :)

2 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete

¡Hey, gracias por tu comentario! No seas anónimo, inicia sesión para que te responda más fácilmente.