Redes e Hilos en java: un servidor web

Bueno para que se vea que eso de los hilos sirve para algo vamos con una aplicación algo más práctica. En este caso vamos  a implementar un servidor web y como queremos que pueda procesar varias peticiones a la vez cada una de ellas la tratará con un hilo propio.

Con el paquete java.net.* podemos crear Sockets y por suerte en java no son tan engorrosos como en c. Con un par de ordenes tenemos creado un servidor TCP, sacamos de cada petición el stream de salida y de entrada y a trabajar.

Para este ejemplo, y por probar cosas, he creado un par de clases auxiliares, Log y Propiedades. La primera es para sacar todos los mensajes por pantalla y la otra para cargar las propiedades del servidor web un fichero properties. Eso nos permite cambiar la config del servidor sin tener que recompilar nada. Las dos clases están hechas de una manera peculiar. Recuerdo cuando me iban a hacer una entrevista de trabajo y me dijeron "te van a preguntar qué es el patrón Singleton".

Bueno, el caso es que para asegurarme de que solo tengo una instancia de esas clases meto ese patrón. Y sí, en aquella entrevista finalmente me lo preguntaron.

Clase para Log

/**
 * Log
 * Clasecilla para hacer log
 * @author Pello Xabier Altadill
 *
 */
public class Log {
        private static Log esteLog;
       

// thnx eug.

private Log () {}


        public static Log getLog () {
                if (esteLog == null) {
                        esteLog = new Log();
                }
                return esteLog;
        }
       
        /**
        * log
        * @param msg t
        */
        public void log (String msg) {
                System.out.println(msg);
        }
}


Clase para cargar Properties

import java.io.FileInputStream;
import java.util.Properties;



/**
 * Propiedades
 * Para recuperar properties de un fichero
 * @author Pello Xabier Altadill
 *
 */
public class Propiedades {

        private static Propiedades propiedades = null;
        private String nombreFicheroProperties = "servidorweb.properties";

        private Propiedades () {}       
        /**
        * retorna el objeto único para las propiedades
        * @return
        */
        public static Propiedades getPropiedades () {
                if (propiedades == null) {
                        propiedades = new Propiedades();
                }
                return propiedades;
        }
       
        /**
        * leerPropiedad
        * @param propiedad la propiedad que queremos leer
        * @return el valor correspondiente
        */
        public String leerPropiedad (String propiedad) {
                String resultado = "";
                try {
                FileInputStream ficheroProperties = new FileInputStream(nombreFicheroProperties);
               
                // Creamso un objeto properties
                Properties p = new Properties();
               
                // Cargamos las properties
                p.load(ficheroProperties);

                resultado = p.getProperty(propiedad);
                } catch (Exception e) {
                        System.err.println("La defequé al leer property: " + e.getMessage());
                }
                return resultado;
        }
       
       

}


Clase para el servidor web

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;


/**
 * @author Pello Xabier Altadill
 * greetz: si pongo mal. si no pongo mal. xDDDD
 */
public class ServidorWeb {
        private final static int PUERTO_POR_DEFECTO = 10666;
        private final static boolean JAVA_ES_LENTO = true;
       
        /**
        * constructor por defecto
        */
        ServidorWeb () {
                dale();
        }
       
        private void dale () {
                int puerto = PUERTO_POR_DEFECTO;
                ServerSocket socketServidor;
                Socket socketCliente = null;
               
                Log.getLog().log("Vamos dale");
                Log.getLog().log("Servidor " + Propiedades.getPropiedades().leerPropiedad("nombre"));
               
                if (!Propiedades.getPropiedades().leerPropiedad("puerto").isEmpty()) {
                        puerto = Integer.parseInt(Propiedades.getPropiedades().leerPropiedad("puerto"));
                }
                try {
                        // Creamos el server socket
                        socketServidor = new ServerSocket(puerto);
                        Log.getLog().log("OK, servidor a la escucha en " + puerto);
                       
                        // Bucle infinito con mensajejeje
                        while (JAVA_ES_LENTO) {
                               
                                socketCliente = socketServidor.accept();
                                Log.getLog().log("Nueva petición desde: " + socketCliente.getInetAddress().getHostAddress());

                                // Creamos una nueva petición, que es un hilo, y este se encarga de todo
                                new Peticion(new BufferedReader(new InputStreamReader(socketCliente.getInputStream())),
                                        new PrintWriter(socketCliente.getOutputStream(), true));
                        }
                       
                } catch (Exception e) {
                        Log.getLog().log("La eyecté en el server socket. " + e.getMessage());
                }
        }
       
        /**
        *
        * @param args
        */
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                ServidorWeb miServidorWeb = new ServidorWeb();

        }

}


Clase para procesar cada petición

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.PrintWriter;


/**
 * Hilo que procesa una petición enviada a nuestro servidor
 * web. Lo único que hace es tratar de averiguar el fichero
 * que se solicita, se abre, se lee y se escupe.
 * @author Pello Xabier Altadill Izura
 *
 */
public class Peticion implements Runnable {

        private Thread peticion;
        private BufferedReader entrada;
        private PrintWriter salida;
        private String directorioPublico;
       
        /**
        *
        */
        public Peticion(BufferedReader entrada, PrintWriter salida) {
                this.entrada = entrada;
                this.salida = salida;
                directorioPublico = Propiedades.getPropiedades().leerPropiedad("public");
                peticion = new Thread(this);
                peticion.start();
        }
       
       
        /**
        * aquí se mete al hacer start en el constructor
        */
        public void run () {
                // Pin pan pun, bocadillo de atún
                enviarRespuesta(procesarPeticion());
                cerrarChiringuito();
        }
       
        /**
        * procesarPeticion
        * procesa la petición al servidor web
        * y saca el fichero que se está solicitando
        * @return
        */
        private String procesarPeticion () {
                String fichero = "/";
                String linea = "";
                String lineaPeticion[];
                try {
                        // Qué pinta tiene una petición? Desde chrome algo así:
                        /* GET / HTTP/1.1
                        Host: localhost:10666
                        Connection: keep-alive
                        Cache-Control: max-age=0
                        Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,;q=0.5
                        User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3
                        Accept-Encoding: gzip,deflate,sdch
                        Accept-Language: en-US,en;q=0.8
                        Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 */

                        // Nos quedaremos con la 1º línea, si no tiene GET o POST
                        // pasamos millas
                        while ((linea = entrada.readLine()) != null) {
                                // Nos quedamos con la primera línea de la petición
                                if (linea.startsWith("GET") || linea.startsWith("POST")) {
                                        lineaPeticion = linea.split(" ");
                                        return lineaPeticion[1];
                                } else {
                                        return fichero;
                                }
                        }
                } catch (Exception e) {
                        Log.getLog().log("La deposité al leer petición");
                }
                return fichero;
        }
       
        /**
        * enviarRespuesta
        * @param fichero
        */
        private void enviarRespuesta (String fichero) {
               
                if (fichero.equals("/")) {
                        fichero = "/" + Propiedades.getPropiedades().leerPropiedad("default");
                }

                // The mother of the lamb. Abrimos el fichero que nos piden
                // lo leemos y lo servimos
                try {
                        String linea = "";
                        BufferedReader inputStream =
                                        new BufferedReader(new FileReader(directorioPublico + fichero));
                       
                        Log.getLog().log("Abriendo fichero: " + fichero);
                       
                        // Leer linea y mandar de vuelta...
                        while ( (linea = inputStream.readLine() ) != null)
                                       salida.write(linea);
                       
                } catch (Exception e) {
                        salida.write("No encontré el fichero: " + fichero + ":");
                        Log.getLog().log("La cagué al leer contenidos " + e.getMessage());
                }
               
                // Tiramos de la cadena, ay que me lol...
                salida.flush();
        }
       
        /**
        * cerrarChiringuito
        * Cerramos los streams y se termina la fiesta
        */
        private void cerrarChiringuito () {
                try {
                        salida.close();
                        entrada.close();
                } catch (Exception e) {
                        Log.getLog().log("La deyecté al cerrar ficheros " + e.getMessage());
                }
               
        }

}


La cosa parece funcionar porque al probarlo desde Chrome en el log veo lo siguiente:
Vamos dale
Servidor ServidorWeb de palo
OK, servidor a la escucha en 10666
Nueva petición desde: 127.0.0.1
Abriendo fichero: /index.html
Nueva petición desde: 127.0.0.1
La cagué al leer contenidos /tmp/favicon.ico (No existe el fichero o el directorio)

Es decir, Chrome me lanza mi petición, el servidor la sirve bien y Chrome a su bola
lanza una petición a favicon.ico. El servidor tb procesa esa petición, pero claro
el fichero no existe.

Este servidor es una mierda que solo va a servir bien texto. Habría que meterse
en el protocolo HTTP y dar respuesta a los mimetypes, juego de caracteres, etc..

Further reading
Tal y como esté hecho este servidor por cada petición va a crear un hilo, sin límite alguno.
Java incorpora un mecanismo de pool de hilos con el que podriamos controlar el número
de peticiones que se procesan, que es lo que hacen los servidores serios: limitar el número de hijos.
Echar un ojo al newFixedThreadPool de la clase java.util.concurrent.Executors

by Pello Altadill 12/28/2012 14:05:20 - 1182 hits