2. Procesamiento de peticiones

Un servlet maneja peticiones de los clientes a través de su método service. Con él se pueden manejar peticiones HTTP (entre otras), reenviando las peticiones a los métodos apropiados que las manejan. Por ejemplo, una petición GET puede redirigirse a un método doGet. Veremos ahora los elementos principales que intervienen en una interacción vía HTTP.

2.1. Petición y respuesta HTTP

2.1.1. Peticiones del cliente

En el protocolo HTTP el cliente realiza una petición que se descompone en:

  • Un comando HTTP, seguido de una dirección de documento o URI (Uniform Resource Identifier), y un número de versión HTTP, de forma que se tiene una línea con el formato:

    Comando    URI    Protocolo

    Por ejemplo:

    GET   /index.html  HTTP/1.1
  • Tras la petición, el cliente puede enviar información adicional de cabeceras (headers) con las que se da al servidor más información sobre la petición (tipo de software que ejecuta el cliente, tipo de contenido (content-type) que entiende el cliente, etc). Esta información puede utilizarla el servidor para generar la respuesta apropiada. Las cabeceras se envían una por línea, donde cada una tiene el formato:

    Clave: Valor

    Por ejemplo:

    Accept-Encoding: gzip, deflate
    User-Agent: Mozilla/4.0 (compatible;MSIE5.0;Windows 98)

Tras las cabeceras, el cliente envía una línea en blanco (\r\n\r\n) para indicar el final de la sección de cabeceras.

  • Finalmente, de forma opcional, se pueden enviar datos adicionales si el comando HTTP solicitado lo requiere (por ejemplo, el método POST que veremos a continuación).

METODO GET

El comando GET permitía al principio solicitar al servidor un documento estático, existente en su espacio de direcciones. Luego se vio que esto no era suficiente, y se introdujo la posibilidad de solicitar búsquedas al servidor, de forma que el documento no tuviera que ser necesariamente estático, sino que la búsqueda estuviera condicionada por unos determinados parámetros. Así, el comando GET tiene la forma:

GET   direccion ? parametros   version HTTP

Por ejemplo:

GET   /cgi-bin/pagina.cgi?IDIOMA=C&MODELO=a+b  HTTP/1.1

Los parámetros se indican con pares nombre=valor, separados por '&', y reciben el nombre de datos de formulario. El URI no puede contener espacios ni algunos caracteres, por lo que se utilizan códigos especiales, como el + para indicar espacio en blanco, u otros códigos %XX para representar otros caracteres. Uno de los trabajos más duros de los programas CGI es procesar esta cadena de parámetros para extraer la información necesaria.

OTROS METODOS

En la versión 1.1 de HTTP se definen otros métodos además de GET:

  • OPTIONS: para consultar al servidor acerca de las funcionalidades que proporciona

  • HEAD: el servidor responde de forma idéntica a un comando GET, pero no devuelve el cuerpo del documento respuesta, sólo las cabeceras. Suele emplearse para comprobar características del documento.

  • POST: se emplea para enviar al servidor un bloque de datos en el cuerpo de la petición

  • PUT: solicita que el cuerpo de la petición que envía se almacene en el espacio de direcciones del servidor, con el identificador URI solicitado (guarda un documento en el servidor)

  • DELETE: solicita borrar un documento específico del servidor

  • TRACE: se utiliza para seguir el camino de la petición por múltiples servidores y proxies (útil para depurar problemas de red).

GET Y POST

Los dos métodos más comúnmente usados son GET y POST. Veremos las diferencias entre uno y otro con un ejemplo:

  • Un ejemplo de petición GET es:

    GET  /dir/cargaPagina.php?id=21&nombre=Pepe  HTTP/1.1
    <cabeceras>
  • Este ejemplo, convertido a petición POST es:

    POST  /dir/cargaPagina.php  HTTP/1.1
    <cabeceras>
    
    id=21&nombre=Pepe

Vemos que los parámetros se pasan en el cuerpo de la petición, fuera de la línea del comando.

Comúnmente existen 3 formas de enviar una petición GET:

  • Teclear la petición directamente en la barra del navegador:

  • Colocar la petición en un enlace y pinchar el enlace para realizarla:

    <a href="http://www.xx.com/pag.html?id=123&nombre=pepe">Pulsa Aqui</a>
  • Enviar la petición tras rellenar un formulario con method="get" (o sin method) con los dos parámetros a enviar:

    <html>
    <body>
      <form action="http://www.xx.com/pag.html">
        <input type="text" name="id" value="123">
        <input type="text" name="nombre" value="pepe">
        <input type="submit" value="Enviar">
      </form>
    </body>
    </html>

    Para enviar una petición POST, normalmente se utiliza un formulario con method="post":

    <html>
    <body>
      <form action="http://www.xx.com/pag.html" METHOD=POST>
        <input type="text" name="id" value="123">
        <input type="text" name="nombre" value="pepe">
        <input type="submit" value="Enviar">
      </form>
    </body>
    </html>

2.1.2. Respuestas del servidor

Las respuestas del servidor también tienen tres partes:

  • Una línea de estado con la versión del protocolo HTTP utilizado en el servidor, un código de estado y una breve descripción del mismo:

    HTTP/1.0  200  OK
  • Información de cabeceras, donde se envía al cliente información sobre el servidor y sobre el documento solicitado. El formato de estas cabeceras es el mismo que el visto para las peticiones del cliente, terminando en una línea en blanco.

  • Finalmente, se envía el documento solicitado. Para marcar el final del mismo se envía también otra línea en blanco.

2.1.3. Cabeceras

Vamos a poder implementar programas que lean las cabeceras que envía un cliente (un navegador, por ejemplo) y que modifiquen el documento servido en función de dichas cabeceras (por ejemplo, enviar una página en función del idioma que se especifique). Por otra parte, podremos utilizar las cabeceras que envíe el servidor como respuesta para obligar al navegador a hacer determinadas acciones, como saltar a otra URL. Veremos a continuación las cabeceras más comunes tanto en las peticiones de los clientes como en las respuestas de los servidores. La RFC donde se especifican estas cabeceras es la 2616.

CABECERAS DE PETICION (HTTP/1.1)

  • Accept: Tipos MIME que puede manejar el cliente

  • Accept-Charset: Conjunto de caracteres que el cliente puede manejar

  • Accept-Encoding: Define si el navegador puede aceptar datos codificados

  • Accept-Language: Idiomas aceptados

  • Authorization: Para identificarse cuando se accede a páginas protegidas

  • Cache-Control: Opciones relacionadas con el servidor proxy. Esta cabecera se llamaba Pragma en HTTP 1.0

  • Connection: Define si el cliente es capaz de realizar conexiones persistentes (keep-alive, valor por defecto), o no (close). Nueva en HTTP 1.1

  • Content-Length: Longitud de los datos enviados. Aplicable a peticiones POST

  • Content-Type: Tipo MIME de los datos enviados. Aplicable a peticiones POST

  • Cookie: Para las cookies que se manejen

  • From: Dirección de correo electrónico responsable de la petición

  • Host: Unica cabecera requerida por HTTP 1.1. Indica el host y el puerto tal y como se especifica en la URL original.

  • If-Modified-Since: El cliente sólo desea el documento si ha sido modificado después de la fecha indicada en esta cabecera.

  • Referer: URL origen de la petición. Si estamos en la página 1 y pinchamos en un enlace a la página 2, la URL de la página 1 se incluye en esta cabecera cuando se realiza la petición de la página 2.

  • User-Agent: Cliente que está realizando la petición (normalmente muestra datos del navegador, como nombre, etc).

CABECERAS DE RESPUESTA

  • Allow: Métodos disponibles (GET, POST, etc) a los que puede responder el recurso que se está solicitando

  • Cache-Control: Dice al cliente en qué circunstancias puede hacer una caché del documento que está sirviendo:

    • public: el documento puede almacenarse en una caché

    • private: el documento es para un solo usuario y sólo puede almacenarse en una caché privada (no compartida)

    • no-cache: el documento nunca debe ser almacenado en caché

    • no-store: el documento no debe almacenarse en caché ni almacenarse localmente de forma temporal en el disco duro

    • must-revalidate: el cliente debe revalidar la copia del documento con el servidor original, no con servidores proxy intermedios, cada vez que se use

    • max-age=xxx: el documento debe considerarse caducado después de xxx segundos.

Esta cabecera se llamaba Pragma en HTTP 1.0

  • Content-Encoding: Tipo de compresión (gzip, etc) en que se devuelve el documento solicitado

  • Content-Language: Idioma en que está escrito el documento. En la RFC 1766 están los idiomas disponibles

  • Content-Length: Número de bytes de la respuesta

  • Content-MD5: Una forma de fijar el checksum (verificación de integridad) del documento enviado

  • Content-Type: Tipo MIME de la respuesta

  • Date: Hora y fecha, en formato GMT, en que la respuesta ha sido generada

  • Expires: Hora y fecha, en formato GMT, en que la respuesta debe considerarse caducada

  • Last-Modified: Fecha en que el documento servido se modificó por última vez. Con esto, el documento se sirve sólo si su Last-Modified es mayor que la fecha indicada en el If-Modified-Since de la cabecera del cliente.

  • Location: Indica la nueva URL donde encontrar el documento. Debe usarse con un código de estado de tipo 300. El navegador se redirigirá automáticamente a la dirección indicada en esta cabecera.

  • Refresh: Indica al cliente que debe recargar la página después de los segundos especificados. También puede indicarse la dirección de la página a cargar después de los segundos indicados:

Refresh: 5; URL=http://www.unapagina.com
  • Set-Cookie: Especifica una cookie asociada a la página

  • WWW-Authenticate: Tipo de autorización y dominio que debería indicar el cliente en su cabecera Authorization.

Para colocar estas cabeceras en un documento se tienen varios métodos, dependiendo de cómo estemos tratando las páginas (mediante servlets, HTML, etc). Por ejemplo, con HTML podemos enviar cabeceras mediante etiquetas META en la cabecera (<HEAD>) de la página HTML:

<META HTTP-EQUIV="Cabecera" CONTENT="Valor">

Por ejemplo:

<META HTTP-EQUIV="Location" CONTENT="http://www.unapagina.com">

2.1.4. Códigos de estado

El código de estado que un servidor devuelve a un cliente en una petición indica el resultado de dicha petición. Se tiene una descripción completa de los mismos en el RFC 2616. Están agrupados en 5 categorías:

  • 100 - 199: códigos de información, indicando que el cliente debe responder con alguna otra acción.

  • 200 - 299: códigos de aceptación de petición. Por ejemplo:

    200

    OK

    Todo está bien

    204

    No Content

    No hay documento nuevo

  • 300 - 399: códigos de redirección. Indican que el documento solicitado ha sido movido a otra URL. Por ejemplo:

    301

    Moved Permanently

    El documento está en otro lugar, indicado en la cabecera Location

    302

    Found

    Como el anterior, pero la nueva URL es temporal, no permanente.

    304

    Not Modified

    El documento pedido no ha sufrido cambios con respecto al actual (para cabeceras If-Modified-Since)

  • 400 - 499: códigos de error del cliente. Por ejemplo:

    400

    Bad Request

    Mala sintaxis en la petición

    401

    Unauthorized

    El cliente no tiene permiso para acceder a la página. Se debería devolver una cabecera WWW-Authenticate para que el usuario introduzca login y password

    403

    Forbidden

    El recurso no está disponible

    404

    Not Found

    No se pudo encontrar el recurso

    408

    Request Timeout

    El cliente tarda demasiado en enviar la petición

  • 500 - 599: códigos de error del servidor. Por ejemplo:

    500

    Internal Server Error

    Error en el servidor

    501

    Not Implemented

    El servidor no soporta la petición realizada

    504

    Gateway Timeout

    Usado por servidores que actúan como proxies o gateways, indica que el servidor no obtuvo una respuesta a tiempo de un servidor remoto

2.1.5. Peticiones: HttpServletRequest

Como hemos visto anteriormente, los objetos ServletRequest se emplean para obtener información sobre la petición de los clientes. Más en concreto, el subtipo HttpServletRequest se utiliza en las peticiones HTTP. Proporciona acceso a los datos de las cabeceras HTTP, cookies, parámetros pasados por el usuario, etc, sin tener que parsear nosotros a mano los datos de formulario de la petición. La clase dispone de muchos métodos, pero destacamos los siguientes:

  • Para obtener los valores de los parámetros pasados por el cliente, se tienen los métodos:

    Enumeration getParameterNames()
    String      getParameter (String nombre)
    String[]    getParameterValues (String nombre)

    Con getParameterNames() se obtiene una lista con los nombres de los parámetros enviados por el cliente. Con getParameter() se obtiene el valor del parámetro de nombre nombre. Si un parámetro tiene varios valores (por ejemplo, si tenemos un array de cuadros de texto con el mismo nombre en un formulario), se pueden obtener todos separados con getParameterValues(). Los nombres de los parámetros normalmente sí distinguen mayúsculas de minúsculas, deberemos tener cuidado al indicarlos.

  • Para obtener la cadena de una petición GET, se tiene el método:

    String getQueryString()

    que devuelve todos los parámetros de la petición en una cadena, que deberemos parsear nosotros como nos convenga.

  • Para obtener datos de peticiones POST, PUT o DELETE, se tienen los métodos:

    BufferedReader     getReader()
    ServletInputStream getInputStream()

    Con getReader() se obtiene un BufferedReader para peticiones donde esperemos recibir texto. Si esperamos recibir datos binarios, se debe emplear getInputStream(). Si lo que esperamos recibir son parámetros por POST igual que se haría por GET, es mejor utilizar los métodos getParameterXXXX(…​) vistos antes.

  • Para obtener información sobre la línea de petición, se tienen los métodos:

    String getMethod()
    String getRequestURI()
    String getProtocol()

    Con getMethod() obtenemos el comando HTTP solicitado (GET, POST, PUT, etc), con getRequestURI() obtenemos la parte de la URL de petición que está detrás del host y el puerto, pero antes de los datos del formulario. Con getProtocol() obtenemos el protocolo empleado (HTTP/1.1, HTTP/1.0, etc).

2.1.6. Respuestas: HttpServletResponse

Los objetos ServletResponse se emplean para enviar el resultado de procesar una petición a un cliente. El subtipo HttpServletResponse se utiliza en las peticiones HTTP. Proporciona acceso al canal de salida por donde enviar la respuesta al cliente.

La clase dispone de muchos métodos, pero destacamos:

Writer              getWriter()
ServletOutputStream getOutputStream()

Con getWriter() se obtiene un Writer para enviar texto al cliente. Si queremos enviar datos binarios, se debe emplear getOutputStream(). Si queremos especificar información de cabecera, debemos establecerla ANTES de obtener el Writer o el ServletOutputStream. Hemos visto en algún ejemplo el método setContentType() para indicar el tipo de contenido. Veremos las cabeceras con más detenimiento más adelante.

2.2. Procesamiento de peticiones GET y POST

Como se ha visto anteriormente, el método doGet() se emplea para procesar peticiones GET. Para realizar nuestro propio procesamiento de petición, simplemente sobreescribimos este método en el servlet:

public void doGet(HttpServletRequest request,
                  HttpServletResponse response)
               throws ServletException, IOException {
  // ... codigo para una peticion GET
}

Podemos utilizar los métodos del objeto HttpServletRequest vistos antes. Así podremos, entre otras cosas:

  • Acceder a elementos de la petición, como valores de parámetros:

    String nombreUsuario = request.getParameter("nombre");
  • Acceder a los parámetros en la cadena de la petición y procesarlos como queramos:

    String query = request.getQueryString();
    ...
  • Obtener un canal de entrada (Reader o InputStream) con que leer los datos de la petición:

    BufferedReader r = request.getReader();
    ...

    Esta, sin embargo, no es una buena idea para tomar parámetros de peticiones u otras cosas. Se suele emplear sobre todo para transferencias de ficheros, pero hay que tener en cuenta que si obtenemos un canal de entrada, luego no podremos obtener parámetros u otros valores con métodos getParameter() y similares.

  • etc.

También podemos utilizar los métodos del objeto HttpServletResponse para, entre otras cosas:

  • Establecer valores de la cabecera (antes que cualquier otra acción sobre la respuesta):

    response.setContentType("text/html");
  • Obtener el canal de salida por el que enviar la respuesta:

    PrintWriter out = response.getWriter();
    out.println ("Enviando al cliente");
  • Redirigir a otra página:

    response.sendRedirect("http://localhost:8080/pag.html");
  • etc.

De forma similar, el método doPost(), se emplea para procesar peticiones POST. Igual que antes, debemos sobreescribir este método para definir nuestro propio procesamiento de la petición:

public void doPost(HttpServletRequest request,
                   HttpServletResponse response)
               throws ServletException, IOException {
  // ... codigo para una peticion POST
}

Las posibilidades de los parámetros HttpServletRequest y HttpServletResponse son las mismas que para GET. Normalmente muchos servlets definen el mismo código para uno y otro método (hacen que doPost() llame a doGet() y definen allí el código, o al revés), pero conviene tenerlos separados para poder tratar independientemente uno y otro tipo de peticiones si se quiere.

2.2.1. Procesamiento secuencial de peticiones

Los servlets pueden gestionar múltiples peticiones de clientes concurrentemente. Por cada petición se crea un hilo que se ejecuta sobre el código del servlet al que se ha accedido. Si existen varias peticiones concurrentes tendremos varios hilos ejecutándose sobre un mismo objeto servlet. Esto podría ocasionar problemas de concurrencia por ejemplo si estuviésemos utilizando variables de instancia de la clase del servlet. Por este motivo deberemos evitar esta práctica cuando implementemos servlets.

Pero puede suceder que en determinado momento necesitemos acceder a cierto recurso compartido, y no nos interese que varios clientes accedan a dicho recurso simultáneamente. Para solucionar este problema, podemos definir bloques de código synchronized, o bien hacer que el servlet sólo atienda una petición cada vez.

Para esto último lo único que hay que hacer es que el servlet, además de heredar de HttpServlet, implemente la interfaz SingleThreadModel. Esto no supone definir más métodos, simplemente añadimos el implements necesario al definir la clase Servlet:

public class MiServlet extends HttpServlet
                       implements SingleThreadModel {
  ...
}

Esta es una práctica que deberemos evitar siempre que sea posible, ya que genera un cuello de botella en nuestra aplicación y no siempre evita los problemas de concurrencia (por ejemplo si accedemos a recursos compartidos por varios servlets). Por este motivo este mecanismo ha quedado desaprobado y en su lugar se recomienda evitar el uso de variables de instancia en servlets, y sincronizar los bloques concretos de código que pudiesen provocar problemas de concurrencia.

Lo más importante es ser consciente de que varios hilos ejecutarán de forma concurrente el código de nuestros servlets y tomar las precauciones oportunas.

2.2.2. Manejo de formularios

Los datos que se envían como parámetros en una petición (tras el interrogante si es una petición GET, o por otro lado si es POST) se llaman datos de formulario. Una vez enviados estos datos como petición, ¿cómo se extraen en el servidor?

Si trabajáramos con CGI, los datos se tomarían de forma distinta si fuese una petición GET o una POST. Para una GET, por ejemplo, tendríamos que tomar la cadena tras la interrogación, y parsearla convenientemente, separando los bloques entre &, y luego separando el nombre del parámetro de su valor a partir del =. También hay que descodificar los valores: los alfanuméricos no cambian, pero los espacios se han convertido previamente en +, y otros caracteres se convierten en %XX%.

Con servlets todo este análisis se realiza de forma automática. La clase HttpServletRequest dispone de métodos que devuelven la información que nos interesa ya procesada, e independientemente de si es una petición GET o POST. Hemos visto antes los métodos:

Enumeration getParameterNames()
String      getParameter (String nombre)
String[]    getParameterValues (String nombre)

2.2.3. Ejemplo

Veamos un ejemplo, supongamos que tenemos este formulario:

<html>
<body>
<form action="/appforms/servlet/ejemplos.ServletForm">
  Valor 1: <input type="text" name="texto1">
  <br>
  Valor2:
  <select name="lista">
  <option name="lista" value="Opcion 1">Opcion 1</option>
  <option name="lista" value="Opcion 2">Opcion 2</option>
  <option name="lista" value="Opcion 3">Opcion 3</option>
  </select>
  <br>
  Valores 3:
  <br>
  <input type="text" name="texto2">
  <input type="text" name="texto2">
  <input type="text" name="texto2">

  <input type="submit" value="Enviar">
</form>
</body>
</html>

Al validarlo se llama al servlet ServletForm, que muestra una página HTML con los valores introducidos en los parámetros del formulario:

package ejemplos;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ServletForm extends HttpServlet
{
  // Metodo para GET

  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException {
    response.setContentType("text/html");

    PrintWriter out = response.getWriter();

    // Mostramos los datos del formulario

    out.println ("<HTML>");
    out.println ("<BODY>");
    out.println ("<H1>Datos del formulario</H1>");
    out.println ("<BR>");

    String valor1 =
      request.getParameter("texto1");
    String valor2 =
      request.getParameter("lista");
    String[] valor3 =
      request.getParameterValues("texto2");

    out.println ("Valor 1:" + valor1);
    out.println ("<BR>");
    out.println ("Valor 2:" + valor2);
    out.println ("<BR>");
    out.println ("Valor 3:");
    out.println ("<BR>");
    if (valor3 != null)
      for (int i = 0; i < valor3.length; i++)
      {
        out.println (valor3[i]);
        out.println ("<BR>");
      }

    out.println ("</BODY>");
    out.println ("</HTML>");
  }

  // Metodo para POST

  public void doPost(HttpServletRequest request,
                     HttpServletResponse response)
                  throws ServletException, IOException {
    doGet(request, response);
  }
}

Para probar el ejemplo que viene en las plantillas, cargamos la URL:

2.3. Cabeceras y códigos

Veremos a continuación cómo tratar las cabeceras HTTP de una petición y de una respuesta, así como los códigos de estado que emite un servidor Web ante una petición, y las variables CGI a las que podemos acceder.

2.3.1. Cabeceras de petición

Cuando se envía una petición HTTP, se pueden enviar, entre otras cosas, unas cabeceras con información sobre el navegador. Para leer estas cabeceras de una petición desde un servlet, se utiliza el método getHeader() del objeto HttpServletRequest.

String getHeader(String nombre)

El parámetro indica el nombre de la cabecera cuyo valor se quiere obtener. Devuelve el valor de la cabecera, o null si la cabecera no ha sido enviada en la petición. Se tienen otros métodos, como:

Enumeration getHeaderNames()
Enumeration getHeaders(String nombre)
int getIntHeader(String nombre)
...

Con getHeaderNames() obtendremos todos los nombres de las cabeceras enviadas. Con getHeaders() obtendremos todos los valores de la cabecera de nombre dado. También hay métodos como getIntHeader() que devuelve el valor de una cabecera con un tipo de dato específico (entero, en este caso). Los nombres de las cabeceras normalmente no distinguen mayúsculas de minúsculas. Algunas cabeceras son de uso común, y tienen métodos específicos para obtener sus valores, como:

Cookie[] getCookies()
String getContentLength()
String getContentType()
...

Con getCookies() obtendremos todas las cookies de la petición (veremos las cookies con más detalle en otro tema). Con getContentLength() obtenemos el valor de la cabecera Content-Length, y con getContentType() el de la cabecera Content-Type.

2.3.2. Cabeceras de respuesta

En la respuesta de un servidor web a una petición también pueden aparecer cabeceras que informan sobre el documento servido o sobre el propio servidor. Podemos definir cabeceras de respuesta para enviar cookies, indicar la fecha de modificación, etc. Estas cabeceras deben establecerse ANTES de enviar cualquier documento, o antes de obtener el PrintWriter si es el caso. Para enviar cabeceras, el método más general es setHeader() del objeto HttpServletResponse.

void setHeader(String nombre, String valor)

Al que se le pasan el nombre de la cabecera y el valor. Hay otros métodos útiles:

void setIntHeader(String nombre, int valor)
void addHeader(String nombre, String valor)
void addIntHeader(String nombre, int valor)
...

setIntHeader() o setDateHeader() se utilizan para enviar cabeceras de tipo entero o fecha. Los métodos add…​() se emplean para añadir múltiples valores a una cabecera con el mismo nombre.

Algunas cabeceras tienen métodos específicos de envío, como:

void setContentType(String tipo)
void setContentLength(int tamaño)
void sendRedirect(String url)
void addCookie(Cookie cookie)

Con setContentType() se establece la cabecera Content-Type con el tipo MIME del documento. Con setContentLength() se indican los bytes enviados. Con sendRedirect() se selecciona la cabecera Location, y con ella se redirige a la página que le digamos. Finalmente, con addCookie() se establecen cookies (esto último ya lo veremos con más detalle en otro tema). Es recomendable utilizar estos métodos en lugar del método setHeader() para la cabecera en cuestión.

2.3.3. Variables CGI

Las variables CGI son una forma de recoger información sobre una petición. Algunas se derivan de la línea de petición HTTP y de las cabeceras, otras del propio socket (como el nombre o la IP de quien solicita la petición), y otras de los parámetros de instalación del servidor (como el mapeo de URLs a los paths actuales). Mostramos a continuación una tabla con las variables CGI, y cómo acceder a ellas desde servlets:

VARIABLE CGI SIGNIFICADO ACCESO DESDE SERVLETS

AUTH_TYPE

Tipo de cabecera Authorization (basic o digest)

request. getAuthType()

CONTENT_LENGTH

Número de bytes enviados en peticiones POST

request. getContentLength()

CONTENT_TYPE

Tipo MIME de los datos adjuntos

request. getContentType()

DOCUMENT_ROOT

Path del directorio raíz del servidor web

getServletContext(). getRealPath("/")

HTTP_XXX_YYY

Acceso a cabeceras arbitrarias HTTP

request. getHeader("Xxx-Yyy")

PATH_INFO

Información de path adjunto a la URL

request. getPathInfo()

PATH_TRANSLATED

Path mapeado al path real del servidor

request. getPathTranslated()

QUERY_STRING

Datos adjuntos para peticiones GET

request. getQueryString()

REMOTE_ADDR

IP del cliente que hizo la petición

request. getRemoteAddr()

REMOTE_HOST

Nombre del dominio del cliente que hizo la petición (o IP si no se puede determinar)

request. getRemoteHost()

REMOTE_USER

Parte del usuario en la cabecera Authorization (si se suministró)

request. getRemoteUser()

REQUEST_METHOD

Tipo de petición (GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE)

request. getMethod()

SCRIPT_NAME

Path del servlet

request. getServletPath()

SERVER_NAME

Nombre del servidor web

request. getServerName()

SERVER_PORT

Puerto por el que escucha el servidor

request. getServerPort()

SERVER_PROTOCOL

Nombre y versión usada en la línea de petición (HTTP/1.0, HTTP/1.1 …​)

request. getServerProtocol()

SERVER_SOFTWARE

Información del servidor web

getServletContext(). getServerInfo()

request se asume que es un objeto de tipo HttpServletRequest. Para obtener cualquiera de las variables antes mencionadas, sólo hay que llamar al método apropiado desde doGet() o doPost().

2.3.4. Códigos de estado HTTP

Cuando un servidor web responde a una petición, en la respuesta aparece, entre otras cosas, un código de estado que indica el resultado de la petición, y un mensaje corto descriptivo de dicho código. El envío de cabeceras de respuesta normalmente se planifica junto con el envío de códigos de estado, ya que muchos de los códigos de estado necesitan tener una cabecera definida. Podemos hacer varias cosas con los servlets manipulando las líneas de estado y las cabeceras de respuesta, como por ejemplo reenviar al usuario a otros lugares, indicar que se requiere un password para acceder a un determinado sitio web, etc. Para enviar códigos de estado se emplea el método setStatus() de HttpServletResponse:

void setStatus(int estado)

Donde se le pasa como parámetro el código del estado. En la clase HttpServletResponse tenemos una serie de constantes para referenciar a cada código de estado. Por ejemplo, la constante:

HttpServletResponse.SC_NOT_FOUND

se corresponde con el código 404, e indica que el documento solicitado no se ha encontrado. Existen otros métodos para gestión de mensajes de error:

void sendError(int codigo, String mensaje)
void sendRedirect(String url)

sendError() genera una página de error, con código de error igual a codigo, y con mensaje de error igual a mensaje. Se suele utilizar este método para códigos de error, y setStatus() para códigos normales. sendRedirect() genera un error de tipo 302, envía una cabecera Location y redirige a la página indicada en url. Es mejor que enviar directamente el código, o hacer un response.setHeader("Location", "http…​"), porque es más cómodo, y porque el servlet genera así una página con el enlace a la nueva dirección, para navegadores que no soporten redirección automática Si queremos enviar un código en la respuesta, se tiene que especificar antes de obtener el objeto PrintWriter.

2.4. Procesamiento asíncrono

Hemos visto anteriormente que por cada petición que se realiza a un servlet se crea un hilo de ejecución. Dado que los recursos del servidor son limitados, normalmente tenemos un número máximo de hilos que pueden atender a los clientes (tenemos un pool de hilos). Cuando los hilos se agoten no se podrán atender más peticiones.

Si realizamos operaciones de larga duración estos hilos quedarán ocupados durante más tiempo y será más fácil agotar los recursos disponibles. Podemos mejorar la escalabilidad del sistema utilizando procesamiento asíncrono. Este tipo de procesamiento nos permite liberar el hilo de la petición mientras se realiza una operación larga en segundo plano, de forma que otro cliente pueda utilizarlo mientras tanto. Estas operaciones podrían ser por ejemplo:

  • Consultas a base de datos

  • Acceso a servicios web remotos

  • Operaciones que dependan del suceso de algún evento o de la interacción del usuario

Para habilitar el procesamiento asíncrono en un servlet debemos añadir el atributo asyncSupported a la anotación WebServlet:

@WebServlet(urlPatterns={"/ServletAsincrono"}, asyncSupported=true)
public class AsincronoServlet extends HttpServlet {
    ...
}

A continuación veremos como liberar el hilo de la petición del procesamiento de operaciones largas o del bloqueo de la entrada/salida utilizando el procesamiento asíncrono.

2.4.1. Procesamiento de operaciones de larga duración

Una vez habilitado el soporte para procesamiento asíncrono, podremos poner la petición en modo de procesamiento asíncrono mediante el método startAsync:

public void doGet(HttpServletRequest request, HttpServletResponse response) {
  ...
  final AsyncContext ac = request.startAsync();
  ...
}

Al llamar a este método obtenemos un objeto AsyncContext que representa el contexto de ejecución asíncrono y que será necesario para procesar la petición una vez hemos pasado a este modo. Esto nos permitirá pasar el procesamiento de la operación a otro hilo, liberando así al hilo de la petición para que pueda atender a otro cliente.

El procesamiento asíncrono de la operación se realizará como se muestra a continuación:

ac.start(new Runnable() { (1)
  public void run() {
      HttpServletRequest response = ac.getRequest(); (2)
      // Procesa la petición
      ...
      HttpServletResponse response = ac.getResponse(); (3)
      // Escribe la respuesta
      ...
      ac.complete(); (4)
  }
}
1 Con start se crea un nuevo hilo dentro del cual realizaremos el procesamiento, liberando así el hilo de la petición.
2 Podemos obtener el objeto de la petición a partir del AsyncContext.
3 También podemos obtener el objeto de la respuesta a partir del AsyncContext, con el cual podremos devolver el resultado al cliente una vez finalizado el procesamiento.
4 Es importante llamar a complete para que termine la operación asíncrona, cierre la respuesta y se la envíe el cliente. Siempre deberemos llamar a este método tras escribir la respuesta.

2.4.2. Entrada y salida no bloqueante

En el apartado anterior hemos visto cómo realizar una operación de larga duración de forma asíncrona. Sin embargo, a veces gran parte del tiempo que se mantiene ocupado el hilo de la petición es debido a la entrada y salida, normalmente debido a mensajes de petición extensos.

Podemos utilizar el soporte asíncrono también para leer la petición fuera del hilo de la petición, y así evitar que quede bloqueado más tiempo del necesario. Para hacer esto definiremos un ReadListener sobre el InputStream de la petición, al que le irá llegando el mensaje conforme se reciba:

final AsyncContext ac = request.startAsync();
final ServletInputStream sis = request.getInputStream();

sis.setReadListener(new ReadListener() { (1)
  byte buffer[] = new byte[4*1024];
  StringBuilder sb = new StringBuilder();

  @Override
  public void onDataAvailable() { (2)
      try {
        do {
            int length = sis.read(buffer);
            sb.append(new String(buffer, 0, length));
        } while(sis.isReady());
      } catch (IOException ex) {  }
  }

  @Override
  public void onAllDataRead() { (3)
      try {
        // Procesar peticion
        ...
        PrintWriter pw = ac.getResponse().getWriter();
        // Escribir respuesta
        ...
      } catch (IOException ex) {  }
      ac.complete();
  }

  @Override
  public void onError(Throwable t) {  }
});
1 Definimos un objeto ReadListener y se lo asignamos al InputStream de la petición.
2 Cada vez que se reciba un fragmento del mensaje se llamará a onDataAvailable. Deberemos leer todos los datos disponibles hasta el momento, y cuando no hayan llegado más saldremos del método para evitar bloquear el hilo. Este método se volverá a llamar cuando haya un nuevo fragmento disponible.
3 Cuando todo el mensaje haya llegado se llama a onAllDataRead. Aquí podemos ya ver todo el contenido que hemos recibido con la petición, y podemos procesarlo y devolver una respuesta.

2.5. Ejemplos

2.5.1. Ejemplo de cabeceras de petición

El siguiente servlet muestra los valores de todas las cabeceras HTTP enviadas en la petición. Recorre las cabeceras enviadas y muestra su nombre y valor:

package ejemplos;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ServletCabecerasPeticion extends HttpServlet {

    // Metodo para GET
    public void doGet(HttpServletRequest request,
                     HttpServletResponse response)
                   throws ServletException, IOException {
        PrintWriter out = response.getWriter();

        // Mostramos las cabeceras enviadas
        // en la peticion

        out.println ("<HTML>");
        out.println ("<BODY>");
        out.println ("<H1>Cabeceras</H1>");
        out.println ("<BR>");

        Enumeration cabeceras = request.getHeaderNames();

        while (cabeceras.hasMoreElements())
        {
            String nombre = (String)(cabeceras.nextElement());
            out.println ("Nombre: " + nombre +
               ", Valor: " + request.getHeader(nombre));
            out.println ("<BR><BR>");
        }

        out.println ("</BODY>");
        out.println ("</HTML>");
    }

    // Metodo para POST
    public void doPost(HttpServletRequest request,
                      HttpServletResponse response)
                   throws ServletException, IOException {
        doGet(request, response);
    }
}

Se puede probar con este formulario, pinchando el botón:

<html>
<body>
<form action=
 "/appcab/servlet/ejemplos.ServletCabecerasPeticion">
  <input type="submit" value="Pulsa aqui">
</form>
</body>
</html>

2.5.2. Ejemplo de cabeceras de respuesta

El siguiente servlet espera un parámetro accion que puede tomar 4 valores:

  • primos: El servlet tiene un hilo que está constantemente calculando números primos. Al elegir esta opción se envía una cabecera Refresh y recarga el servlet cada 10 segundos, mostrando el último número primo que ha encontrado.

  • redirect: Utiliza un sendRedirect() para cargar la página que se indique como parámetro

  • error: Utiliza un sendError() para mostrar una página de error, con un mensaje de error definido por el usuario, y un código de error a elegir de una lista.

  • codigo: Envía un código de estado HTTP (con setStatus()), a elegir de entre una lista.

package ejemplos;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ServletCabecerasRespuesta extends HttpServlet
                                       implements Runnable {
    // Ultimo numero primo descubierto
    long primo = 1;

    // Hilo para calcular numeros primos
    Thread t = new Thread(this);

    // Metodo de inicializacion
    public void init()
    {
        t.start();
    }

    // Metodo para GET
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
                    throws ServletException, IOException {
        String accion = request.getParameter("accion");

        if (accion.equals("primo"))
        {
            // Buscar el ultimo numero
            // primo y enviarlo

            response.setContentType("text/html");
            response.setHeader("Refresh", "10");
            PrintWriter out = response.getWriter();
            out.println ("<HTML><BODY>");
            out.println ("Primo: " + primo);
            out.println ("</BODY></HTML>");

        } else if (accion.equals("redirect")) {

            // Redirigir a otra pagina

            String url = request.getParameter("url");
            if (url == null)
                url = "http://www.ua.es";
            response.sendRedirect(url);

        } else if (accion.equals("error")) {

            // Enviar error con sendError()

            int codigo = response.SC_NOT_FOUND;
            try {
                codigo = Integer.parseInt
               (request.getParameter("codigoMensaje"));
            } catch (Exception ex) {
                codigo = response.SC_NOT_FOUND;
            }

            String mensaje = request.getParameter("mensaje");
            if (mensaje == null)
                mensaje = "Error generado";
            response.sendError(codigo, mensaje);

        } else if (accion.equals("codigo")) {

            // Enviar un codigo de error

            int codigo = response.SC_NOT_FOUND;
            try {
                codigo = Integer.parseInt
                  (request.getParameter("codigo"));
            } catch (Exception ex) {
                codigo = response.SC_NOT_FOUND;
            }

            response.setStatus(codigo);
        }
    }

    // Metodo para POST
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
                    throws ServletException, IOException {
        doGet(request, response);
    }

    ... el resto del codigo es para el hilo,
    para calcular numeros primos
    Puede consultarse en el fichero fuente,
    aqui se quita por simplificar
}

Se puede probar con este formulario, eligiendo la acción a realizar, introduciendo los parámetros necesarios en el formulario y pinchando el botón de Enviar Datos:

<html>
<body>
<form action=
"/appcab/servlet/ejemplos.ServletCabecerasRespuesta">

<table border="0">

<tr>
<td>
<input type="radio" name="accion" value="primo" selected>
Obtener ultimo numero primo
</td>
<td></td>
<td></td>
</tr>

<tr>
<td>
<input type="radio" name="accion" value="redirect">
Redirigir a una pagina
</td>
<td>
URL:
<input type="text" name="url" value="http://www.ua.es">
</td>
<td></td>
</tr>

<tr>
<td>
<input type="radio" name="accion" value="error">
Mostrar pagina de error
</td>
<td>
Mensaje:
<input type="text" name="mensaje"
value="Error generado por el usuario">
</td>
<td>
Codigo:
<select name="codigoMensaje">
<option name="codigoMensaje" value="400">400</option>
<option name="codigoMensaje" value="401">401</option>
<option name="codigoMensaje" value="403">403</option>
<option name="codigoMensaje" value="404" selected>404
</option>
</select>
</td>
</tr>

<tr>
<td>
<input type="radio" name="accion" value="codigo">
Enviar codigo de error
</td>
<td>
Codigo:
<select name="codigo">
<option name="codigo" value="200">200</option>
<option name="codigo" value="204">204</option>
<option name="codigo" value="404" selected>404</option>
</select>
</td>
<td></td>
</tr>

</table>

<input type="submit" value="Enviar Datos">

</form>
</body>
</html>

2.5.3. Ejemplo de autentificación

El siguiente servlet emplea las cabeceras de autentificación: envía una cabecera de autentificación si no ha recibido ninguna, o si la que ha recibido no está dentro de un conjunto de Properties predefinido, con logins y passwords válidos. En el caso de introducir un login o password válidos, muestra un mensaje de bienvenida. Los logins y passwords están en un objeto Properties, definido en el método init(). Podríamos leer estos datos de un fichero, aunque por simplicidad aquí se definen como constantes de cadena. Los datos de autentificación se envían codificados, y se emplea un objeto sun.misc.BASE64Decoder para descodificarlos y sacar el login y password.

package ejemplos;

import java.io.*;
import java.util.*;
import sun.misc.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ServletPassword extends HttpServlet
{
    // Conjunto de logins y passwords permitidos
    Properties datos = new Properties();

    // Metodo de inicializacion

    public void init()
    {
        datos.setProperty("usuario1", "password1");
        datos.setProperty("usuario2", "password2");
    }

    // Metodo para GET

    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
                    throws ServletException, IOException {
        response.setContentType("text/html");

        // Comprobamos si hay cabecera
        // de autorizacion

        String autorizacion = request.getHeader("Authorization");

        if (autorizacion == null) {
            // Enviamos el codigo 401 y
            // la cabecera para autentificacion

            response.setStatus(response.SC_UNAUTHORIZED);
            response.setHeader("WWW-Authenticate",
                               "BASIC realm=\"privileged-few\"");
        } else {
            // Obtenemos los datos del usuario
            // y comparamos con los almacenados

            // Quitamos los 6 primeros caracteres
            // que indican tipo de autentificación
            // (BASIC)

            String datosUsuario =
                autorizacion.substring(6).trim();

            BASE64Decoder dec = new BASE64Decoder();

            String usuarioPassword = new String
                (dec.decodeBuffer(datosUsuario));

            int indice = usuarioPassword.indexOf(":");

            String usuario =
                usuarioPassword.substring(0, indice);

            String password =
                usuarioPassword.substring(indice + 1);

            String passwordReal =
                datos.getProperty(usuario);

            if (passwordReal != null &&
                passwordReal.equals(password)) {

                // Mensaje de bienvenida

                PrintWriter out = response.getWriter();
                out.println ("<HTML><BODY>");
                out.println ("OK");
                out.println ("</BODY></HTML>");
            } else {

                // Pedir autentificacion

                response.setStatus(response.SC_UNAUTHORIZED);
                response.setHeader("WWW-Authenticate",
                           "BASIC realm=\"privileged-few\"");
            }
        }
    }

    // Metodo para POST

    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
                     throws ServletException, IOException {
        doGet(request, response);
    }
}

Se puede probar cada ejemplo, respectivamente, con:

Un ejemplo de login y password válidos para el tercer ejemplo es: login=usuario1, password=password1.

2.6. Ejercicios

2.6.1. Recogida de parámetros del usuario (0,3 puntos)

La aplicación cweb-peticiones contiene un formulario form_datos.html con una serie de campos (tipo texto, listas, checkboxes…​). Se pide que dicho formulario, en su action, llame al servlet org.expertojava.cweb.ejercicios.DatosServlet que deberéis crear e implementar. El servlet recogerá todos los parámetros del formulario y los mostrará en una tabla de dos columnas (una con el nombre del parámetro y otra con el valor).

2.6.2. Trabajando con redirecciones (0,4 puntos)

Sobre la aplicación anterior, tenemos otro formulario form_datos2.html idéntico al del ejercicio anterior, que deberá llamar al servlet org.expertojava.cweb.ejercicios.DatosServlet2. Crear este servlet y hacer que redirija a la página bienvenida.html con un mensaje de bienvenida, si los datos introducidos en el formulario son correctos, y a la misma página form_datos2.html si hay algún dato incorrecto. Entenderemos por dato incorrecto que alguno de los campos de texto se quede vacío. Utilizad el método sendRedirect de la respuesta para las redirecciones.

2.6.3. Un buscador sencillo (0,4 puntos)

En el fichero libros.txt hay un listado de libros, indicando su ISBN, título y autor. Cread e implementad un servlet org.expertojava.cweb.ejercicios.LibroServlet que lea dicho fichero, guarde los libros en un ArrayList, y reciba un parámetro cadena. Como resultado, sacará todos los libros de la lista que contengan dicha cadena (en el título, en el autor, o en cualquier parte de la cadena). Podéis hacer también una página HTML libros.html con el formulario de búsqueda que llame al servlet, para poderlo probar.

Para leer los libros, en la inicialización del servlet podemos utilizar un código como el siguiente:

ArrayList<String> libros = new ArrayList<String>();
...
InputStream is = getClass().getResourceAsStream("/libros.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String libro;
try {
  while( (libro=br.readLine())!=null ) {
    libros.add(libro);
  }
} catch (IOException e) { }

2.6.4. Distinguir el navegador (0,3 puntos)

Muchas veces, cuando escribamos una aplicación Web, nos va a interesar poder distinguir el tipo de navegador que está utilizando el cliente, porque en función del mismo se podrán hacer o no determinadas acciones. Por ejemplo, el código Javascript que se emplea en un navegador Internet Explorer es diferente a veces del que se emplea en uno Mozilla. Para probar a distinguir entre navegadores, vamos a crear el servlet org.expertojava.cweb.ejercicios.NavServlet para que identifique si el cliente accede desde un tipo de navegador u otro. Para ello leemos la cabecera User-Agent con el método getHeader(…​) de la petición, y comprobamos su valor. Mostrad la cadena en una página, y cargad dicha página desde dos o tres navegadores diferentes. Cada uno mostrará algún rasgo distintivo en dicha cadena, que lo identifique de los demás. Una vez tengáis una parte de texto que los diferencia (por ejemplo, en Firefox el User-Agent tiene la cadena "Firefox", y en el otro navegador (Mozilla, por ejemplo), no la tiene) haríamos con algo como:

public void doGet(HttpServletRequest req, ...) throws ...
{
  String nav = req.getHeader("User-Agent");
  // Cambiar "Firefox" por el texto que sea
  if (nav.indexOf("Firefox") != -1)
    ... // Firefox
  else
    ... // Otro navegador (Mozilla)
  ...
}

Una vez distinguido el navegador, ya se podría hacer algo que sólo sirviera para ese navegador. En este caso, por simplificar, vamos a cargar como imagen el logo del navegador que hayamos detectado. Tenéis en la plantilla las imágenes correspondientes diferentes navegadores (directorio webapp/logos). Colocad como imagen de la página la del navegador que hayáis detectado (con una etiqueta <img> de HTML).

2.6.5. Redirecciones con retardo (0,3 puntos)

El formulario form_datos3.html se envía al servlet org.expertojava.cweb.ejercicios.CompruebaServlet. Se pide crear e implementar dicho servlet para comprobar que los datos sean correctos (que no haya ningún campo de texto vacío). En el caso de que no haya errores el servlet simplemente mostrará un mensaje indicando que todo ha ido bien. Si hay algún error, el servlet debe redirigir al servlet org.expertojava.cweb.ejercicios.ErrorCompruebaServlet, que deberéis crear para que muestre un mensaje con el error producido (indicando qué campo está incompleto), y a los 5 segundos redirija al formulario anterior

Se puede utilizar la cabecera de respuesta Refresh con valor 5; url=form_datos3.html

2.6.6. Loggear variables CGI (0,3 puntos)

Sobre el servlet org.expertojava.cweb.ejercicios.CompruebaServlet anterior vamos a añadir mensajes de log de tipo INFO, para que:

  • Tras cada petición (por doGet o doPost), genere un mensaje de tipo INFO que indique la IP del cliente que solicitó la petición (variable CGI REMOTE_ADDR), el tipo de petición (variable CGI REQUEST_METHOD), y el tipo de navegador (cabecera User-Agent).

  • El mensaje en conjunto deberá tener el formato siguiente (se incluyen en las plantillas los ficheros de configuración para obtener este formato):

    día/mes/año hora:minuto:segundo - texto del mensaje - nueva línea