Está en la página 1de 12

Prctica 5: Servidor web concurrente en Java

Esta prctica pretende familiarizar al alumno con la programacin de servidores que emplean sockets TCP. Para ello partiremos del servidor web bsico visto como ejemplo en las clases de teora (incluido como anexo al final del captulo), y lo modificaremos para obtener un servidor web concurrente, capaz de admitir conexiones simultneamente con varios clientes. Nuestro servidor seguir la versin 1.0 del protocolo HTTP definida en el RFC 1945 (http://www.ietf.org/rfc/rfc1945.txt), aunque no permitir todas sus posibilidades de intercambio de informacin, sino slo un subconjunto reducido de las mismas. Como ya hemos estudiado, esta versin emplea conexiones no persistentes, es decir, el cliente realiza una conexin TCP distinta para cada uno de los objetos que componen la pgina web. Por lo tanto, cada peticin se tratar de forma independiente y no es necesario mantener informacin de estado de una peticin a la siguiente, lo que simplifica la implementacin del servidor. Por otra parte, nuestro servidor debe ser capaz de atender varias peticiones concurrentemente, por lo que debe ser multihilo. Esto nos permitir estudiar un ejemplo sencillo de servidor concurrente y ver las diferencias en la estructura con respecto al caso iterativo.

P5-2

Prcticas de Redes de Computadores

En los siguientes apartados abordaremos diferentes aspectos que deben resolverse para la correcta implementacin del servidor propuesto.

1. Clases para comunicaciones


Como sabemos el servicio de web utiliza para transferir sus mensajes el protocolo de nivel de transporte TCP. Por ello, en este apartado veremos las clases bsicas para comunicar un cliente y un servidor mediante sockets TCP. La informacin que proporcionamos sobre las clases java que hay que emplear y sus mtodos es bastante limitada. Para ver los detalles es aconsejable consultar la pgina web de Sun con informacin sobre java (http://java.sun.com/javase/6/docs/api). Tambin puede ser de inters consultar el documento:
http://www.ulpgc.es/otros/tutoriales/java.

Como mnimo necesitaremos dos clases pertenecientes al paquete java.net.*, las clases Socket y ServerSocket. 1. Socket permite establecer una conexin entre un cliente y un servidor TCP. El cliente crea un socket e inicia la conexin con el servidor. Una vez conectado al otro extremo utiliza este socket para el envo y la recepcin de informacin. 2. ServerSocket permite a un servidor escuchar en un puerto a la espera de una conexin TCP. Al establecerse sta, en el programa servidor se crea un objeto de la clase Socket que se emplea para el intercambio de informacin posterior entre ambos extremos. Nuestro servidor esperar las conexiones de los clientes escuchando en un puerto previamente determinado. 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. Para esperar la conexiones hay que invocar el mtodo accept() de la clase ServerSocket. Este mtodo bloquea la ejecucin del programa a la espera de una conexin. Al recibir una peticin, accept() crea un nuevo objeto de la clase Socket, que se usar durante el resto de la comunicacin con el cliente. El empleo del mtodo accept() puede generar una excepcin IOException, por lo que, o bien habr que capturarla mediante una clusula try, tal y como se hizo en la prctica anterior, o bien puede lanzarse (throws) al programa o funcin que llam al mtodo que genera la

Servidor web concurrente en Java

P5-3

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, esta excepcin se lanzar hacia ella.

2. Gestin de la entrada/salida
Una vez conectado con un cliente, con el fin de comprobar y depurar el funcionamiento del servidor, ste VISUALIZAR POR PANTALLA LAS CADENAS DE CARACTERES que reciba del cliente. Para manejar la transferencia de informacin a travs del socket, la clase Socket dispone de dos mtodos: getInputStream() y getOutputStream(), que proporcionan un flujo de entrada y uno de salida, respectivamente. Como ya vimos en una prctica anterior, es mejor no manejar estos flujos directamente, sino anidados a travs de otras clases como Scanner y PrintWriter y, al igual que hicimos all, sern las que empleemos. A continuacin se muestra, de forma breve, un ejemplo de uso de estas clases y algunos de sus mtodos:
Scanner recibe = new Scanner (cliente.getInputStream()); System.out.println(recibe.nextLine()); PrintWriter envia = new PrintWriter(cliente.getOutputStream()); envia.println(GET / HTTP/1.0);

donde cliente es un objeto de la clase Socket conectado con un cliente.

3. Descripcin del protocolo e implementacin del servidor


El servidor que vamos a realizar sigue la versin 1.0 del protocolo HTTP. No obstante, nuestro servidor no implementar todo el protocolo, sino nicamente un subconjunto de ste. En este apartado vamos a explicar qu parte implementaremos. Para ello necesitamos conocer, qu peticiones debe ser capaz de servir y cmo responder a esas peticiones. HTTP emplea dos tipos de mensajes: peticiones de los clientes a los servidores y respuestas de los servidores a los clientes. Ambos poseen una estructura similar formada por tres campos: una lnea inicial que se incluye siempre, a continuacin puede haber una o ms cabeceras (en la versin 1.0 ninguna es obligatoria), y por ltimo, un cuerpo que no siempre estar presente. Tras finalizar las cabeceras (o despus de la lnea inicial, en el

P5-4

Prcticas de Redes de Computadores

caso de que no haya ninguna cabecera) debe haber una lnea en blanco, formada por los caracteres ASCII: CR y LF (retorno de carro y salto de lnea, respectivamente).

Mensaje de peticin del cliente


En los mensajes de peticin la lnea inicial contiene la orden deseada por el cliente. Aunque el estndar especifica varias rdenes posibles, la ms habitual es la orden GET, que ser la nica que implementaremos. Esta orden permite al cliente solicitar objetos almacenados en el servidor. Comienza con la palabra reservada GET, le sigue el objeto solicitado por el cliente y finaliza con la versin del protocolo empleado. En el caso de la versin 1.0 tiene la forma:
GET objeto HTTP/1.0 CR LF CR LF

Donde, como se ha indicado, CR LF son los caracteres de retorno de carro y salto de lnea, respectivamente. Por ejemplo, para poder obtener la pgina web correspondiente a http://www.upv.es/index.html, un navegador se conectar al puerto 80 del servidor www.upv.es y enviar la orden:
GET /index.html HTTP/1.0

lnea en blanco enviada tambin por el navegador. Nuestro servidor slo analizar la lnea inicial de los mensajes de peticin y no se preocupar del resto. Para interpretar la lnea de peticin, el servidor necesita analizar cada una de las palabras que la componen. Esta tarea podemos resolverla fcilmente mediante la clase java Scanner que nos permite leer el siguiente elemento (String) de un flujo de entrada, mediante el mtodo next(). Como ya hemos mencionado, la descripcin de dicha clase se puede consultar en la pgina web de Sun. Como nosotros slo vamos a implementar la orden GET, nuestro servidor slo tiene que comprobar si la primera palabra de la cadena es GET, y en caso afirmativo identificar qu fichero ha solicitado el cliente. Para comprobar que la peticin es un GET podemos emplear cualquiera de los mtodos que tiene la clase String para comparar dos objetos de ese tipo, como equals, equalsIgnoreCase o startsWith. Si el servidor tiene el fichero solicitado (y el cliente dispone de los permisos necesarios para acceder a l), el servidor debe envirselo al cliente, y en caso contrario responder con un mensaje de error.

Servidor web concurrente en Java

P5-5

Mensaje de respuesta del servidor


La respuesta a una peticin GET incluye los tres campos que hemos mencionado antes, y es necesario que nuestro servidor los emplee todos. Veamos que informacin debemos poner en cada uno de ellos: Campo 1: la lnea inicial En el caso de la respuesta de un servidor esta lnea se denomina lnea de estado y emplea el formato siguiente:
versin num cadena

donde versin es la versin del protocolo utilizada, num es un cdigo numrico de tres cifras que expresa el resultado de la peticin realizada por el cliente, y por ltimo, cadena es una cadena de caracteres que tambin indica el resultado de la operacin pero ahora en un formato fcilmente comprensible por los programadores. Algunos de los valores de los ltimos dos campos son:
Num
200 304 400 401 404 501 OK Not Modified Bad Request Unauthorized Not Found Not Implemented

Cadena

La lnea de estado tpica de la versin 1.0 para indicar que se enva el objeto pedido por el cliente es:
HTTP/1.0 200 OK

Si, por el contrario, el servidor no dispone del objeto solicitado responder con:
HTTP/1.0 404 Not Found

Estas son las dos lneas de estado que nuestro servidor debe implementar. Campo 2: las lneas de cabecera Aunque la versin 1.0 no establece ninguna cabecera obligatoria lo habitual es que los servidores enven varias lneas de cabecera en sus respuestas. Estas lneas permiten a los servidores incluir informacin sobre

P5-6

Prcticas de Redes de Computadores

el objeto que van a enviar en el cuerpo del mensaje, as como avisar al cliente en el caso de vayan a cerrar la conexin una vez finalizado el envo del mensaje al cliente. El orden de las distintas cabeceras es irrelevante. El formato de las lneas de cabecera es:
Etiqueta: valor

donde la Etiqueta indica el tipo de informacin que se enva y valor su valor asociado. Las lneas siguientes muestran algunas lneas tpicas de cabecera:
Content-Type: text/html Content-Length: 3453 Content-Encoding: x-gzip Date: Tue, 15 Nov 2004 08:12:31 GMT Expires: Tue, 12 Oct 2005 06:22:41 GMT

Nuestro servidor deber enviar, al menos, las lneas de cabecera del Content-Type y el Content-Length. La primera de ellas indica el tipo MIME del objeto 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 del fichero
.htm .html .txt .gif .jpg .jpeg .pdf *

Tipo MIME
text/html text/plain image/gif image/jpeg application/pdf application/octet-stream

La clase String posee el mtodo endsWith(String) que podemos utilizar para comprobar la extensin del fichero. En el caso en el que el servidor no pueda asociar la extensin del fichero con una de las especficas que tiene definidas, emplear como tipo por defecto el ltimo mostrado en la tabla: application/octet-stream. En nuestro caso, hay que implementar como mnimo los tipos: text/html, image/gif y application/octet-stream. Por otra parte, la etiqueta Content-Length hace referencia al tamao del objeto que se va a enviar. Para calcular ese tamao se puede utilizar la

Servidor web concurrente en Java

P5-7

clase File (perteneciente al paquete java.io.*) ya que uno de sus mtodos (length()) permite obtener este dato. Por ltimo, las lneas de la cabecera y el cuerpo estn separados mediante una lnea en blanco, que en nuestro caso se puede generar con el mtodo println() de la clase PrintWriter. Campo 3: el cuerpo de mensaje El cuerpo es el objeto que haba solicitado el cliente. El servidor tiene que buscarlo en su sistema de ficheros local, para lo que puede utilizar la clase FileInputStream. Al constructor de esta clase se le pasa como parmetro el nombre del fichero (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. Como lo habitual es que las peticiones vengan de un navegador, ste intentar visualizar cualquier respuesta del servidor. Por ello, incluso aunque no haya encontrado el objeto solicitado y devuelva un mensaje de error, el servidor suele incluir una pgina html informando del suceso. Nuestro servidor tambin debe seguir ese comportamiento habitual, y aunque el fichero no exista devolver una respuesta completa: lnea de estado, cabecera y cuerpo. En este caso la cabecera identificar al cuerpo como text/html y el cuerpo ser 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 HTTP/1.0 501 Not Implemented. Por lo tanto, los cambios que hay que realizar con respecto al servidor del anexo son: 1. Comprobar que la peticin es un GET. 2. Aadir la lnea de estado (HTTP/1.0 ). 3. Comprobar la extensin del fichero y aadir las cabeceras correspondientes, relacionadas con su extensin y su longitud (Content-Length y ContentType).

P5-8

Prcticas de Redes de Computadores

Recuerda que el servidor debe visualizar por pantalla todo lo que reciba del cliente.

4. 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 la web de la asignatura de redes, por ejemplo. La copiamos a nuestro ordenador local mediante la instruccin
wget r nH http://www.redes.upv.es/redes/

desde el directorio donde est nuestro programa. A continuacin, lanzaremos el servidor en nuestro ordenador local y nos conectaremos a l mediante el navegador, usando el URL siguiente:
http://localhost:8000/redes/index.html

Se supone que, como se ha recomendado en el apartado 1, el servidor escucha en el puerto 8000. En otro caso, habra que sustituir el 8000 que aparece en el URL por el nmero de puerto correspondiente. Si el proceso servidor pudiera lanzarse con privilegios de administrador y escuchar en el puerto 80 (puerto estndar para el servicio web) no sera necesario indicar el nmero de puerto en el URL del navegador. Si nuestro servidor no funciona correctamente al primer intento, puede resultar til para realizar la depuracin emplear el programa telnet. Si tecleamos:
telnet localhost 8000 GET /redes/index.html http/1.0 CR LF

Veremos en pantalla, a continuacin, la respuesta del servidor y podremos comprobar si es la esperada.

5. 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

Servidor web concurrente en Java

P5-9

reciba una peticin de conexin TCP, el servidor aceptar la conexin y se la pasar a un 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) { //Espero un cliente Socket cliente=servidor.accept(); // Cdigo para lanzar un hilo que atienda la peticin // Hay que pasarle el socket cliente al hilo } } }

Java ofrece varias posibilidades para crear un hilo de ejecucin. La ms sencilla es declarar una subclase de la clase Thread. Cada objeto de esta subclase permite lanzar un nuevo hilo de ejecucin. La sintaxis para crear una nueva subclase a partir de la clase Thread es:
class PeticionHTTP extends Thread

La clase Thread (y por extensin la subclase creada a partir de ella) siempre incluye un mtodo run() que contiene el cdigo que debe ejecutarse concurrentemente con el resto del programa. 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

P5-10
} }

Prcticas de Redes de Computadores

Despus de haber visto cmo se declara una subclase de la clase


Thread, vamos a ver cmo utilizarla. El primer paso es crear un objeto de la subclase nueva, en nuestro ejemplo, la clase PeticionHTTP. A conti-

nuacin hay que arrancar el hilo. Esto se hace invocando el mtodo start(), que a su vez llamar al mtodo run().
PeticionHTTP pethttp=new PeticionHTTP(); pethttp.start();

En nuestro caso, cada objeto de la clase PeticionHTTP deber atender una peticin HTTP a travs de la conexin TCP que se acaba de realizar tras la ejecucin del mtodo accept(). El socket devuelto por el mtodo accept()se crea en el hilo principal, pero debe ser accesible desde los nuevos hilos que se van lanzando. Esto se puede hacer a travs del constructor de la clase PeticionHTTP, pasndole como parmetro el socket conectado con el cliente. El hilo principal crear un nuevo objeto de la clase PeticionHTTP, le pasar como parmetro el socket conectado con el cliente, y despus invocar el mtodo start():
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.

6. Anexo
En este anexo se incluye el cdigo del micro-servidor web iterativo visto en clase. Las principales diferencias entre este servidor y el que hay que realizar en esta prctica son dos. La primera es que el servidor mostrado como ejemplo ms abajo no sigue el estndar HTTP/1.0, ya que no devuelve siempre la lnea de estado ni la cabecera de respuesta. La segunda diferencia es que no es concurrente.
import java.net.*; import java.util.Scanner;

Servidor web concurrente en Java


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) { // espero a que venga un cliente Socket cliente=servidor.accept();

P5-11

// nos aseguramos de que el fin de lnea se ajuste al estndar System.setProperty("line.separator","\r\n"); Scanner lee=new Scanner (cliente.getInputStream()); PrintWriter escribe=new PrintWriter(cliente.getOutputStream(),true); // esto debe ser el lee.next(); "GET"

// esto es el fichero String fichero = "." + lee.next(); // comprobamos si existe FileInputStream fis = null; boolean existe = true; try { fis = new FileInputStream(fichero); } catch (FileNotFoundException e) { existe = false; } if (existe && fichero.length()>2) while((bytes = fis.read(buffer)) != -1 ) // enviar fichero cliente.getOutputStream().write(buffer, 0, bytes); else {escribe.println("HTTP/1.0 404 Not Found"); escribe.println(); } cliente.close(); } } }

P5-12

Prcticas de Redes de Computadores