Вы находитесь на странице: 1из 9

Prctica 4: Servidor web concurrente en Java

En esta prctica vamos a programar un servidor web concurrente en Java que, por tanto, ser capaz de admitir la conexin simultnea de varios clientes. El servidor tendr la capacidad de servir ficheros mediante el protocolo HTTP en su versin 1.0, tal y como est definido en el RFC 1945 (http://www.ietf.org/rfc/rfc1945.txt). En la versin 1.0 de HTTP el cliente realiza una conexin TCP distinta para cada uno de los ficheros que necesita para visualizar una pgina web. El servidor debe ser capaz de atender varias peticiones concurrentemente, por lo que debe ser multihilo. En el hilo principal, con el que comenzar el programa, el servidor permanecer a la espera de que se conecte algn cliente, escuchando a travs de un socket en un puerto previamente definido. Cuando reciba una peticin de conexin TCP, el servidor aceptar dicha conexin a travs de un nuevo socket ubicado en el mismo puerto y atender la peticin a travs de este nuevo socket usando otro hilo de ejecucin. Toda la informacin sobre las clases de Java puede encontrarse en la pgina web de Sun: http://java.sun.com/j2se/1.4/docs/api, o bien se puede consultar el tutorial de Java: http://www.ulpgc.es/otros/tutoriales/java. Al final de este documento se ha incluido un anexo con un ejemplo de un micro-servidor web iterativo programado en Java, que nos va a servir como punto de partida para realizar el servidor web concurrente en esta prctica. Dado que vamos a abordar la programacin de un servidor capaz de aceptar varias conexiones simultneamente, en los siguientes apartados abordaremos diferentes aspectos que deben resolverse para la correcta implementacin del servidor propuesto.

Prctica 4: Servidor web concurrente en Java

Clases para comunicaciones


Java utiliza tres clases para realizar la comunicacin a travs de sockets. Se aconseja consultar la pgina web de Sun para ms informacin acerca de dichas clases. Socket: utilizada para realizar la comunicacin a travs de una conexin (protocolo TCP). Los clientes crean un socket para conectarse a los servidores, y una vez conectado al otro extremo, servidores y clientes emplearn un objeto de la clase socket para el envo y la recepcin de informacin.

ServerSocket: utilizada por un servidor para ponerse en espera de una

conexin (protocolo TCP). La conexin se establece mediante un objeto de la clase Socket que permite el intercambio de informacin entre el servidor y el cliente.

DatagramSocket: utilizado para la comunicacin sin conexin (protocolo

UDP). Como la aplicacin que vamos a realizar es un servidor de pginas web, la comunicacin se realiza mediante un protocolo orientado a la conexin (TCP), por lo que nuestro servidor utilizar las dos primeras clases de las mencionadas ms arriba. De esta forma, nuestro servidor esperar la conexin de un cliente usando un objeto de la clase ServerSocket, que escuchar en un puerto previamente determinado, superior al 1023. El cdigo de nuestro servidor implementa la espera de un cliente al ejecutar el mtodo accept() de la clase ServerSocket. Este mtodo bloquea la ejecucin del programa a la espera de una conexin. Cuando llega un cliente, el mtodo accept() crear un nuevo objeto de la clase Socket, que se usar durante el resto de la comunicacin con el cliente. Todas estas operaciones pueden generar dos tipos de excepciones: UnknownHostException e IOException, por lo que, o bien habr que capturarlas mediante una clusula try, tal y como se hizo en la prctica anterior, o bien pueden lanzarse (throws) al programa o funcin que llam al mtodo que genera la excepcin. De esta manera no ser necesario programar una clusula try. En nuestro caso, dado que es la mquina virtual Java quien llam al mtodo main, estas excepciones se lanzarn hacia ella.

Gestin de la entrada/salida
Para poder llevar una traza de los comandos que recibimos de los clientes, el servidor ESCRIBIR POR PANTALLA LAS CADENAS DE CARACTERES que le enven los clientes, que recibir usando las clases BufferedReader e InputStreamReader de Java. Consultad la pgina web de Sun para ver cmo se utilizan dichas clases. Tambin pueden consultarse en dicha pgina web los mtodos getInputStream() y getOutputStream() de la

Prctica 4: Servidor web concurrente en Java

clase Socket, y la clase PrintWriter. Esta ltima, a travs de su mtodo println(String), permite enviar una lnea a travs de un OutputStream. A continuacin se muestra, de forma breve, un ejemplo de uso de estas clases y mtodos.
BufferedReader recibe = new BufferedReader(new InputStreamReader (cliente.getInputStream())); System.out.println(recibe.readLine()); PrintWriter envia = new PrintWriter(cliente.getOutputStream()); envia.println(GET / HTTP/1.0);

Implementacin del protocolo


El servidor que vamos a implementar en esta prctica se ajusta a la versin 1.0 del protocolo HTTP. No obstante, nuestro servidor no va a implementar todo el protocolo, sino nicamente un subconjunto de ste. Vamos a detallar el subconjunto a implementar y cmo interpretar las rdenes del protocolo que reciba nuestro servidor. El protocolo HTTP/1.0 admite tres rdenes: GET, HEAD, POST. El primero se utiliza para pedir al servidor un objeto determinado, es decir, es la orden que los navegadores utilizan para obtener los ficheros de los que se compone una pgina web. El segundo se utiliza para recibir informacin sobre un objeto, pero sin recibir el objeto propiamente dicho. El comando POST se usa para enviar informacin desde el navegador al servidor. En esta prctica se implementar slo la orden GET. La sintaxis de la orden GET mnima (sin cabeceras) es: GET objeto HTTP/1.0 CR LF CR LF CR LF son los caracteres de retorno de carro y salto de lnea, por lo que el comando debe terminar con dos saltos de lnea, aunque esto no es necesario que lo compruebe nuestro servidor en esta prctica. Por ejemplo, un navegador desea obtener la siguiente pgina web:
http://www.upv.es/index.html. Lo que har ser conectarse a la mquina www.upv.es por el puerto 80 (puerto estndar para el protocolo HTTP) y enviar

el comando: GET /index.html HTTP/1.0 <- lnea en blanco enviada tambin por el navegador Java implementa la clase StringTokenizer, que podemos utilizar para obtener cada una de las palabras que forman una frase. Para ver el funcionamiento detallado de dicha clase consulta la pgina web de Sun. A esta clase se le pasa como parmetro un String en el constructor, la clase divide dicho String en

Prctica 4: Servidor web concurrente en Java

palabras (o tokens) y cada vez que llamamos al mtodo nextToken() nos devuelve una de las palabras segn su orden de aparicin en el String. Nuestro servidor deber interpretar cada rden que reciba, por lo que tendr que convertir dicha cadena a un objeto de la clase StringTokenizer para interpretarla. Una vez interpretado el texto recibido del cliente, y tras identificar que la orden a ejecutar es GET, e identificado tambin el fichero que hemos de enviar al cliente, el servidor deber contestar a la peticin. La respuesta del servidor, segn el protocolo HTTP/1.0, estar formada por una lnea de estado, una cabecera y un cuerpo de mensaje. La lnea de estado indica si la operacin ha tenido o no xito y, en este caso, cul es la posible causa del error. La cabecera lleva informacin acerca del objeto incluido en la respuesta, identificando el tipo de informacin, el nmero de bytes, etc. El cuerpo contiene el objeto que se est transfiriendo. Veamos cada uno de estos campos con ms detalle. 1) La lnea de estado La lnea de estado tiene el siguiente formato: HTTP/1.0 num cadena, donde num es el identificador de estado: un nmero de tres cifras identificando el resultado de la operacin; cadena puede ser cualquier cadena de caracteres. Algunos de los identificadores de estado son:
Cdigo 200 201 204 301 302 304 400 401 404 500 501 Significado OK Created No Content Moved Permanently Moved Temporarily Not Modified Bad Request Unauthorized Not Found Internal Server Error Not Implemented

Una posible lnea de estado para indicar que toda ha ido bien puede ser: HTTP/1.0 200 OK 2) La cabecera Justo despus de la lnea de estado se ha de enviar la cabecera. La cabecera est formada por varias lneas de texto con informacin sobre los datos que se van a enviar. Los siguientes son ejemplos de algunas de las posibles lneas que pueden

Prctica 4: Servidor web concurrente en Java

tomar parte de una cabecera, en este o en otro orden:


Content-Length: 3453 Content-Type: text/html Content-Encoding: x-gzip Date: Tue, 15 Nov 1995 08:12:31 GMT Expires: Tue, 12 Oct 1999 06:22:41 GMT

La forma de saber cuando termina la cabecera y comienza el cuerpo es mediante una lnea en blanco, que en nuestro caso se puede generar con el mtodo println() de la clase PrintWriter. Nuestro servidor deber enviar al menos como cabecera el ContentType y el Content-Length. El primero de ellos nos indica el tipo MIME del archivo que se va a enviar. Por ejemplo, un fichero *.htm tiene como tipo MIME text/html. Algunos de los tipos MIME ms usados pueden encontrarse en la siguiente tabla:
Extensin archivo .htm .html .txt .gif .jpg .jpeg .mp3 .wav .mid .pdf * Tipo MIME text/html text/plain image/gif image/jpeg audio/x-mp3 audio/x-wav audio/x-midi application/pdf application/octetstream

La clase String posee el mtodo endsWith(String) que podemos utilizar para comprobar la extensin del archivo. En el caso en el que el servidor no sepa el tipo del archivo, el tipo MIME que deber enviar es el ltimo de la tabla. Para comprobar el tamao del archivo se puede utilizar la clase File (perteneciente al paquete java.io.*) ya que como puede verse en la ayuda de Sun, uno de sus mtodos (length()) permite obtener este dato. 3) El cuerpo de mensaje Para enviar el archivo, el servidor tiene que buscarlo en el sistema de archivos local, para lo que se puede utilizar la clase FileInputStream. Al constructor de esta clase se le pasa como parmetro el nombre del archivo (o bien un objeto de la clase File) y, si no existe generar la excepcin FileNotFoundException. Capturando dicha excepcin podemos averiguar si existe o no el fichero.

Prctica 4: Servidor web concurrente en Java

Tanto si el fichero existe como si no, hay que enviar la lnea de estado correspondiente, una cabecera y un cuerpo. En el caso de que el fichero no exista la cabecera identificar al cuerpo como text/html y posteriormente se enviar un mensaje de error en formato html como el siguiente: <html><body><h1>404 Not Found</h1></body></html>

EJERCICIO 1 Completa el servidor web del anexo para que se ajuste a la versin 1.0 del protocolo HTTP conforme a lo descrito en este apartado. Respecto a los tipos MIME, slo es necesario implementar los tipos text/html, image/gif y application/octet-stream. En caso de recibir una orden diferente a GET, el servidor debe contestar con la lnea de estado 501 Not Implemented. Recordad que el servidor debe visualizar por pantalla todo lo que reciba del cliente.

Comprobacin del funcionamiento del servidor


Una vez que hayamos programado el servidor web necesitamos una pgina de muestra para comprobar que nuestro servidor atiende correctamente las peticiones de sus clientes. Para ello podemos utilizar, por ejemplo, la web de la asignatura de redes. La copiamos a nuestro ordenador local usando la instruccin wget r -nH http://www.redes.upv.es/redes/ desde el directorio donde est nuestro programa. Lanzaremos el servidor en nuestro ordenador local. Como nuestro proceso no tiene privilegios de administrador, el puerto con el que trabaje el servidor deber ser uno mayor que el 1023. Por ejemplo, podemos elegir el 8000. Por ltimo, nos conectaremos al servidor, desde el navegador, usando la direccin: http://localhost:8000/redes/index.html

Implementacin de la concurrencia
Dado que el servidor debe ser capaz de atender varias peticiones simultneamente, vamos a convertir el servidor iterativo en un servidor concurrente usando varios hilos de ejecucin. En el hilo principal, el servidor permanecer a la espera de que se conecte algn cliente. Cuando se reciba una peticin de conexin TCP, el servidor aceptar la conexin y se la pasar a un

Prctica 4: Servidor web concurrente en Java

nuevo hilo para ser atendida mientras el hilo principal del servidor queda a la espera de nuevos clientes. El hilo principal tendr un aspecto parecido al siguiente:
public class ServidorWebConcurrente { public static void main(String argv[]) throws UnknownHostException, IOException { int puerto=8000; ServerSocket servidor=new ServerSocket(puerto); while (true) { Socket cliente=servidor.accept(); //Espero un cliente // Cdigo para lanzar un hilo que atienda la peticin // Hay que pasarle el socket cliente al hilo } } }

Para poder lanzar un nuevo hilo de ejecucin, Java permite dos posibilidades distintas: implementar la interfaz Runnable o extender la clase Thread. Nosotros utilizaremos la segunda para atender las peticiones HTTP que van llegando. Para ello es necesario definir, a partir de la clase Thread, una nueva clase que extender la clase Thread. Cada objeto de esta nueva clase podr lanzar un nuevo hilo de ejecucin. La sintaxis para crear una nueva clase a partir de la clase Thread es:
class PeticionHTTP extends Thread

Por otra parte, la clase Thread posee dos mtodos destinados a la puesta en marcha del nuevo hilo de ejecucin. El primero es el mtodo run(). Este mtodo contiene el cdigo que se ejecutar en el nuevo hilo de ejecucin, por lo que habr que programarlo en la nueva clase que estamos creando. El segundo mtodo es el mtodo start(). Cuando este mtodo es invocado, se pone en ejecucin el nuevo hilo, ejecutando el cdigo que haya en el mtodo run(). Poniendo juntos todos estos detalles, nuestra nueva clase tendr un aspecto parecido a:
class PeticionHTTP extends Thread { //atributos... public PeticionHTTP(Socket s) { // Cdigo a ejecutar durante la creacin del hilo } public void run() { // Cdigo del nuevo hilo que atiende al cliente http } }

Prctica 4: Servidor web concurrente en Java

Como ya se ha mencionado, cada objeto de la nueva clase Thread extendida deber atender la peticin HTTP a travs de la conexin TCP que se acaba de realizar tras la ejecucin del mtodo accept() de la clase Serversocket. El socket devuelto por el mtodo accept() ya ha sido creado en el hilo principal, por lo que este hilo deber pasarle a cada nuevo hilo dicho socket. Esto se puede hacer a travs del constructor de la clase Thread, pasndole como parmetro el socket conectado con el cliente. El hilo principal crear un nuevo objeto de la clase PeticionHTTP e invocar el mtodo start() en el punto en el que deba de crear un nuevo hilo de ejecucin:
PeticionHTTP pethttp=new PeticionHTTP(cliente); pethttp.start();

EJERCICIO 2 Copia el servidor web iterativo del ejercicio anterior a un fichero llamado ServidorWebConcurrente.java. Modifica este nuevo programa para convertir el servidor iterativo en servidor concurrente.

Prctica 4: Servidor web concurrente en Java

Anexo
En este anexo se incluye el cdigo del micro-servidor web iterativo visto en clase. Las dos principales diferencias entre este servidor y el que hay que realizar en esta prctica es que el servidor de ms abajo no sigue el estndar HTTP/1.0, ya que no devuelve la lnea de estado ni la cabecera de respuesta. Adems, tampoco es concurrente.
import java.net.*; import java.util.*; import java.io.*; class ServidorWebIterativo { public static void main(String args[]) throws UnknownHostException, IOException { byte[] buffer = new byte[1024]; int bytes; int Puerto=8000; ServerSocket servidor=new ServerSocket(puerto); while(true) { Socket cliente=servidor.accept(); // espero a que venga un cliente // nos aseguramos de que el fin de lnea se ajuste al estndar System.setProperty("line.separator","\r\n"); BufferedReader lee=new BufferedReader(new InputStreamReader(cliente.getInputStream())); PrintWriter escribe=new PrintWriter(cliente.getOutputStream(),true); StringTokenizer tokens = new StringTokenizer(lee.readLine()); tokens.nextToken(); // esto debe ser el "GET" String archivo = "." + tokens.nextToken(); // esto es el archivo FileInputStream fis = null; // comprobamos si existe boolean existe = true; try { fis = new FileInputStream(archivo); } catch (FileNotFoundException e) { existe = false; } if (existe && archivo.length()>2) while((bytes = fis.read(buffer)) != -1 ) // enviar archivo cliente.getOutputStream().write(buffer, 0, bytes); else escribe.println("<html><body><h1>404 Not Found</h1></body></html>"); cliente.close(); } } }

Вам также может понравиться