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 :)
good job bro!!
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete