cap15 menu+linea


SERVIDOR SIMPLE DE HTTP

Vamos a implementar un servidor de HTTP básico, sólo le permitiremos admitir operaciones GET y un rango limitado de tipos MIME codificados. Los tipos MIME son los descriptores de tipo para contenido multimedia. Esperamos que este ejemplo sirva como base para un entretenido ejercicio de ampliación y exploración porque, desde luego, lo que no pretendemos es inquietar a los Consejos de Dirección de Microsoft o Netscape.

La aplicación va a crear un ServerSocket conectado al puerto 80, que en caso de no tener privilegios para su uso, podemos cambiar, por ejemplo al 8080; y después entra en un bucle infinito. Dentro del bucle, espera dentro del método accept() del ServerSocket hasta que se establece una conexión cliente. Después asigna un flujo de entrada y salida al socket. A continuación lee la solicitud del cliente utilizando el método getRawRequest(), que devolverá un null si hay un error de entrada/salida o el cliente corta la conexión. Luego se identifica el tipo de solicitud y se gestiona mediante el método handlget() o handleUnsup(). Finalmente se cierran los sockets y se comienza de nuevo.

Cuando se ejecuta el programa completo, se escribe en pantalla lo que el navegador cliente envía al servidor. Aunque se capturan varias condiciones de error, en la práctica no aparecen. El ampliar este servidor para que soporte una carga de millones de visitas al día requiere bastante trabajo; no obstante, en el ordenador en que estoy escribiendo esto, no se enlenteció demasiado con una carga de hasta diez entradas por segundo, lo que permitiría alrededor de un millón de visitas al día. Se podría mejorar mediante el uso de threads y control de la memoria caché para gestionar esas visitas, pero eso ya forma parte del ejercicio sobre el que se puede trabajar.

El código fuente de nuestro mini servidor de HTTP se encuentra en el fichero TutHttp.java, que reproducimos a continuación:

import java.net.*;
import java.io.*;
import java.util.*;

// Clase de utilidades donde declaramos los tipos MIME y algunos gestores
// de los errores que se pueden generar en HTML
class HttpUtilidades {
    final static String version = "1.0";
    final static String mime_text_plain = "text/plain";
    final static String mime_text_html = "text/html";
    final static String mime_image_gif = "image/gif";
    final static String mime_image_jpg = "image/jpg";
    final static String mime_app_os = "application/octet-stream";
    final static String CRLF = "\r\n";

    // Método que convierte un objeto String en una matriz de bytes. 
    // Java gestiona las cadenas como objetos, por lo que es necesario
    // convertir las matrices de bytes que se obtienen a Strings y
    // viceversa
    public static byte aBytes( String s )[] {
        byte b[] = new byte[ s.length() ];
        s.getBytes( 0,b.length,b,0 );
        return( b );
        }

    // Este método concatena dos matrices de bytes. El método
    // arraycopy() asombra por su rapidez
    public static byte concatenarBytes( byte a[],byte b[] )[] {
        byte ret[] = new byte[ a.length+b.length ];
        System.arraycopy( a,0,ret,0,a.length );
        System.arraycopy( b,0,ret,a.length,b.length );
        return( ret );
        }

    // Este método toma un tipo de contenido y una longitud, para
    // devolver la matriz de bytes que contiene el mensaje de cabecera
    // MIME con formato
    public static byte cabMime( String ct,int tam )[] {
        return( cabMime( 200,"OK",ct,tam ) );
        }

    // Es el mismo método anterior, pero permite un ajuste más fino
    // del código que se devuelve y el mensaje de error de HTTP
    public static byte cabMime(int codigo,String mensaje,String ct, 
        int tam )[] {
        Date d = new Date();
        return( aBytes( "HTTP/1.0 "+codigo+" "+mensaje+CRLF+
               "Date: "+d.toGMTString()+CRLF+
               "Server: Java/"+version +CRLF+
               "Content-type: "+ct+CRLF+
               ( tam > 0 ? "Content-length: "+tam+CRLF : "" )+CRLF ) );
        }

    // Este método construye un mensaje HTML con un formato decente
    // para presentar una condición de error y lo devuelve como
    // matriz de bytes
    public static byte error( int codigo,String msg,String fname)[] {
        String ret = "<BODY>"+CRLF+"<H1>"+codigo+" "+msg+"</H1>"+CRLF;

        if( fname != null ) 
            ret += "Error al buscar el URL: "+fname+CRLF;
        ret += "</BODY>"+CRLF;
        byte tmp[] = cabMime( codigo,msg,mime_text_html,0 );
        return( concatenarBytes( tmp,aBytes( ret ) ) );
        }

    // Devuelve el tipo MIME que corresponde a un nombre de archivo dado
    public static String mimeTypeString( String fichero ) {
        String tipo;

        if( fichero.endsWith( ".html" ) || fichero.endsWith( ".htm" ) )
            tipo = mime_text_html;
        else if( fichero.endsWith( ".class" ) )
            tipo = mime_app_os;
        else if( fichero.endsWith( ".gif" ) )
            tipo = mime_image_gif;
        else if( fichero.endsWith( ".jpg" ) )
            tipo = mime_image_jpg;
        else
            tipo = mime_text_plain;
        return( tipo );
        }
    }

// Esta clase sirve para que nos enteremos de lo que está haciendo
// nuestro servidor. En una implementación real, todos estos mensajes
// deberían registrarse en algún fichero
class HTTPlog {
    public static void error( String entrada ) {
        System.out.println( "Error: "+entrada );
        }

    public static void peticion( String peticion ) {
        System.out.println( peticion );
        }
    } 

// Esta es la clase principal de nuestro servidor Http
class TutHttp {
    public static final int puerto = 80;
    final static String docRaiz = "/html";
    final static String fichIndice = "index.html";
    final static int buffer = 2048;
    public static final int RT_GET=1;
    public static final int RT_UNSUP=2;
    public static final int RT_END=4;

    // Indica que la petición no está soportada, por ejemplo POST y HEAD
    private static void ctrlNoSop(String peticion,OutputStream sout) {
        HTTPlog.error( "Peticion no soportada: "+peticion );
        }

    // Este método analiza gramaticalmente la solicitud enviada con el
    // GET y la descompone en sus partes para extraer el nombre del
    // archivo que se está solicitando. Entonces lee el fichero que
    // se pide
    private static void ctrlGet( String peticion,OutputStream sout ) {
        int fsp = peticion.indexOf( ' ' );
        int nsp = peticion.indexOf( ' ',fsp+1 );
        String fich = peticion.substring( fsp+1,nsp );

        fich = docRaiz+fich+( fich.endsWith("/") ? fichIndice : "" );
        try {
            File f = new File( fich );
            if( !f.exists() ) 
                {
                sout.write( HttpUtilidades.error( 404,
                    "No Encontrado",fich ) );
                return;
                }

            if( !f.canRead() ) 
                {
                sout.write( HttpUtilidades.error( 404,
                    "Permiso Denegado",fich ) );
                return;
                }

            // Ahora lee el fichero que se ha solicitado
            InputStream sin = new FileInputStream( f );
            String cabmime = HttpUtilidades.mimeTypeString( fich );
            int n = sin.available();
            sout.write( HttpUtilidades.cabMime( cabmime,n ) );
            byte buf[] = new byte[buffer];
            while( ( n = sin.read( buf ) ) >= 0 )
                sout.write( buf,0,n );
                
            sin.close();
        } catch( IOException e ) {
            HTTPlog.error( "Excepcion: "+e );
            }
        }

    // Devuelve la cabecera de la solicitud completa del cliente al
    // método main de nuestro servidor
    private static String getPeticion( InputStream sin ) {
        try {
            byte buf[] = new byte[buffer];
            boolean esCR = false;
            int pos = 0;
            int c;

            while( ( c = sin.read() ) != -1 ) 
                {
                switch( c ) {
                    case '\r':
                        break;
                    case '\n': 
                        if( esCR )
                            return( new String( buf,0,0,pos ) );
                        esCR = true;
                    // Continúa, se ha puesto el primer \n en la cadena
                    default:
                        if( c != '\n' ) 
                            esCR = false;
                        buf[pos++] = (byte)c;
                    }
                }
        } catch( IOException e ) {
            HTTPlog.error( "Error de Recepcion" );
            }
        return( null );
        }

    private static int tipoPeticion( String peticion ) {
        return( peticion.regionMatches( true,0,"get ",0,4 ) ? 
            RT_GET : RT_UNSUP );
        }

    // Función principal de nuestro servidor, que se conecta al socket
    // y se embucla indefinidamente
    public static void main( String args[] ) throws Exception {
        ServerSocket ss = new ServerSocket( puerto );
        while( true ) 
            {
            String peticion;
            Socket s = ss.accept();
            OutputStream sOut = s.getOutputStream();
            InputStream sIn = s.getInputStream();

            if( ( peticion = getPeticion( sIn ) ) != null ) 
                {
                switch( tipoPeticion( peticion ) ) {
                    case RT_GET:
                        ctrlGet( peticion,sOut );
                        break;
                    case RT_UNSUP:
                    default:
                        ctrlNoSop( peticion,sOut );
                        break;
                    }
                HTTPlog.peticion( peticion );
                }
            sIn.close();
            sOut.close();
            s.close();
            }
        }
    }
linea2
menu
Tutorial de Java
[Anterior] [Indice] [Siguiente]
1