Skip to content

4. Fundamentos del desarrollo ASP.NET

4.1. El concepto de aplicación web ASP.NET

4.1.1. Introducción

Una aplicación web es una aplicación que reúne diversos documentos (HTML, código .NET, imágenes, sonidos, etc.). Estos documentos deben estar en una misma raíz, que se denomina raíz de la aplicación web. A esta raíz se le asocia una ruta virtual del servidor web. Ya hemos visto el concepto de carpeta virtual para el servidor web Cassini. Este concepto también existe para el servidor web IIS. Una diferencia importante entre ambos servidores es que, en un momento dado, IIS puede tener cualquier número de carpetas virtuales, mientras que el servidor web Cassini solo tiene una, la que se especificó al iniciarlo. Esto significa que el servidor IIS puede servir varias aplicaciones web simultáneamente, mientras que el servidor Cassini solo sirve una a la vez. En los ejemplos anteriores, el servidor Cassini siempre se iniciaba con los parámetros (<webroot>,/aspnet) que asociaban la carpeta virtual /aspnet con la carpeta física <webroot>. Por lo tanto, el servidor web siempre servía la misma aplicación web. Esto no nos impidió escribir y probar páginas diferentes e independientes dentro de esa única aplicación web. Cada aplicación web tiene sus propios recursos, que se encuentran en su raíz física <webroot>:

  • una carpeta [bin] en la que se pueden colocar clases precompiladas
  • un archivo [global.asax] que permite inicializar la aplicación web en su conjunto, así como el entorno de ejecución de cada uno de sus usuarios
  • un archivo [web.config] que permite configurar el funcionamiento de la aplicación
  • un archivo [default.aspx] que actúa como puerta de entrada a la aplicación
  • ...

Tan pronto como una aplicación utiliza uno de estos tres recursos, necesita una ruta física y virtual propias. De hecho, no hay ninguna razón para que dos aplicaciones web diferentes estén configuradas de la misma manera. Todos nuestros ejemplos anteriores pudieron colocarse en la misma aplicación (<webroot>,/aspnet) porque no utilizaban ninguno de los recursos mencionados anteriormente.

Volvamos a la arquitectura MVC recomendada al inicio de este capítulo para el desarrollo de una aplicación web:

Image

La aplicación web está formada por archivos de clase (controlador, clases de negocio, clases de acceso a datos) y archivos de presentación (documentos HTML, imágenes, sonidos, hojas de estilo, etc.). Todos estos archivos se ubicarán en una misma raíz a la que a veces nos referiremos como <application-path>. Esta raíz se asociará a una ruta virtual <application-vpath>. La asociación entre esta ruta virtual y la ruta física se realiza mediante la configuración del servidor web. Hemos visto que, para el servidor Cassini, esta asociación se realiza al iniciar el servidor. Por ejemplo, en una ventana de DOS, se iniciaría Cassini con:

webserver.exe /port:80 /path:<application-path> /vpath:<application-vpath>

En la carpeta <application-path>, encontraremos, según nuestras necesidades:

  • la carpeta [bin] para colocar allí clases precompiladas (dll)
  • el archivo [global.asax] cuando necesitemos realizar inicializaciones, ya sea al iniciar la aplicación o al iniciar una sesión de usuario
  • el archivo [web.config] cuando necesitemos configurar la aplicación
  • el archivo [default.aspx] cuando necesitemos una página predeterminada en la aplicación

Para respetar este concepto de aplicación web, todos los ejemplos que se presentarán a continuación se colocarán en una carpeta <application-path> específica de la aplicación, a la que se asociará una carpeta virtual <application-vpath>, ya que el servidor Cassini se inicia de manera que vincule estos dos parámetros.

4.1.2. Configurar una aplicación web

Si <application-path> es la raíz de una aplicación ASP.NET, se puede utilizar el archivo <application-path>\web.config para configurarla. Este archivo tiene el formato XML. A continuación se muestra un ejemplo:

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
  <appSettings>
    <add key="nom" value="tintin"/>
    <add key="age" value="27"/>
  </appSettings>   
</configuration>

Hay que tener en cuenta que las etiquetas XML distinguen entre mayúsculas y minúsculas. Toda la información de configuración debe estar entre las etiquetas <configuration> y </configuration>. Existen numerosas secciones de configuración que se pueden utilizar. Aquí solo presentamos una, la sección <appSettings>, que permite inicializar datos con la etiqueta <add>. La sintaxis de esta etiqueta es la siguiente:

<add key="identificateur" value="valeur"/>

Cuando el servidor web inicia una aplicación, verifica si en <application-path> hay un archivo llamado web.config. Si es así, lo lee y almacena su información en un objeto de tipo [ConfigurationSettings], el cual estará disponible para todas las páginas de la aplicación mientras esta esté activa. La clase [ConfigurationSettings] tiene un método estático [AppSettings]:

Image

Para obtener el valor de una clave C del archivo de configuración, se escribe ConfigurationSettings.AppSettings("C"). Se obtiene una cadena de caracteres. Para utilizar el archivo de configuración anterior, creemos una página [default.aspx]. El código VB del archivo [default.aspx.vb] será el siguiente:


Imports System.Configuration

Public Class _default
    Inherits System.Web.UI.Page

    Protected nom As String
    Protected age As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'se recupera la información de configuración
        nom = ConfigurationSettings.AppSettings("nom")
        age = ConfigurationSettings.AppSettings("age")
    End Sub

End Class

Se observa que, al cargar la página, se recuperan los valores de los parámetros de configuración [nom] y [age]. Estos serán mostrados por el código de presentación de [default.aspx]:


<%@ Page src="default.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="_default" %>
<html>
    <head>
        <title>Configuration</title>
    </head>
    <body>
        Nom :
        <% =nom %><br/>
        Age :
        <% =age %><br/>
    </body>
</html>

Para la prueba, colocamos los archivos [web.config], [default.aspx] y [default.aspx.vb] en la misma carpeta:

D:\data\devel\aspnet\poly\chap2\config1>dir
30/03/2004  15:06                  418 default.aspx.vb
30/03/2004  14:57                  236 default.aspx
30/03/2004  14:53                  186 web.config

Sea <application-path> la carpeta donde se encuentran los tres archivos de la aplicación. El servidor Cassini se inicia con los parámetros (<application-path>,/aspnet/config1). Solicitamos los archivos URL y [http://localhost/aspnet/config1]. Como [config1] es una carpeta, el servidor web buscará un archivo [default.aspx] en su interior y lo mostrará si lo encuentra. En este caso, lo encontrará:

Image

4.1.3. Aplicación, Sesión, Contexto

4.1.3.1. El archivo global.asax

El código del archivo [global.asax] siempre se ejecuta antes de que se cargue la página solicitada por la solicitud actual. Debe estar ubicado en la raíz <application-path> de la aplicación. Si existe, el servidor web utiliza el archivo [global.asax] en diversos momentos:

  1. cuando la aplicación web se inicia o se cierra
  2. cuando se inicia o finaliza una sesión de usuario
  3. cuando se inicia una solicitud de usuario

Al igual que con las páginas .aspx, el archivo [global.asax] puede escribirse de diferentes maneras y, en particular, separando el código VB en una clase de controlador y el código de presentación. Esta es la opción predeterminada de la herramienta Visual Studio y aquí haremos lo mismo. Normalmente no hay que encargarse de la presentación, ya que esa función recae en las páginas .aspx. El contenido del archivo [global.asax] se reduce entonces a una directiva que hace referencia al archivo que contiene el código del controlador:


<%@ Application src="Global.asax.vb" Inherits="Global" %>

Cabe señalar que la directiva ya no es [Page], sino [Application]. El código del controlador [global.asax.vb] asociado y generado por la herramienta Visual Studio es el siguiente:


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class Global
    Inherits System.Web.HttpApplication

  Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la aplicación
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la sesión
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al inicio de cada solicitud
    End Sub

    Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al intentar autenticar al usuario
    End Sub

    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa cuando ocurre un error
    End Sub

    Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa cuando finaliza la sesión
    End Sub

    Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa cuando la aplicación se cierra
    End Sub

End Class

Cabe señalar que la clase del controlador deriva de la clase [HttpApplication]. A lo largo del ciclo de vida de una aplicación, existen varios eventos importantes. Estos son gestionados por procedimientos cuyo esqueleto se muestra arriba.

  • [Application_Start]: recordemos que una aplicación web está «encerrada» en una ruta virtual. La aplicación se inicia tan pronto como un cliente solicita una página ubicada en esa ruta virtual. En ese momento se ejecuta el procedimiento [Application_Start]. Esta será la única vez. En este procedimiento se realizarán todas las inicializaciones necesarias para la aplicación, como por ejemplo crear objetos cuya vida útil sea la misma que la de la aplicación.
  • [Application-End]: se ejecuta cuando la aplicación finaliza. A cada aplicación se le asigna un tiempo de inactividad, configurable en [web.config], al cabo del cual se considera que la aplicación ha finalizado. Por lo tanto, es el servidor web el que toma esta decisión en función de la configuración de la aplicación. El tiempo de inactividad de una aplicación se define como el tiempo durante el cual ningún cliente ha realizado una solicitud de un recurso de la aplicación.
  • [Session-Start]/[Session_End]: A cada cliente se le asigna una sesión, a menos que la aplicación esté configurada para no tener sesiones. Un cliente no es un usuario frente a su pantalla. Si este ha abierto dos navegadores para consultar la aplicación, representa a dos clientes. Un cliente se identifica mediante un token de sesión que debe adjuntar a cada una de sus solicitudes. Este token de sesión es una secuencia de caracteres generada aleatoriamente por el servidor web y es única. Dos clientes no pueden tener el mismo token de sesión. Este token seguirá al cliente de la siguiente manera:
    • el cliente que realiza su primera solicitud no envía un token de sesión. El servidor web lo detecta y le asigna uno. Este es el inicio de la sesión y se ejecuta el procedimiento [Session_Start]. Esta será la única vez.
    • El cliente realiza sus solicitudes siguientes enviando el token que lo identifica. Esto le permitirá al servidor web recuperar la información relacionada con ese token. De esta manera, se puede dar seguimiento a las diferentes solicitudes del cliente.
    • La aplicación puede poner a disposición del cliente un formulario para finalizar la sesión. En este caso, es el propio cliente quien solicita el fin de su sesión. Se ejecutará el procedimiento [Session_End]. Esto ocurrirá una sola vez.
    • Es posible que el cliente nunca solicite por sí mismo el cierre de su sesión. En ese caso, tras un cierto período de inactividad de la sesión —que también se puede configurar mediante [web.config]—, el servidor web cerrará la sesión. Entonces se ejecutará el procedimiento [Session_End].
  • [Application_BeginRequest]: este procedimiento se ejecuta tan pronto como llega una nueva solicitud. Por lo tanto, se ejecuta con cada solicitud de cualquier cliente. Es un buen momento para examinar la solicitud antes de transmitirla a la página solicitada. Incluso se puede tomar la decisión de redirigirla a otra página.
  • [Application_Error]: se ejecuta cada vez que ocurre un error que no está manejado explícitamente por el código del controlador [global.asax.vb]. Aquí se puede redirigir la solicitud del cliente a una página que explique la causa del error.

Si no es necesario manejar ninguno de estos eventos, entonces se puede ignorar el archivo [global.asax]. Esto es lo que se hizo con los primeros ejemplos de este capítulo.

4.1.3.2. Ejemplo 1

Desarrollemos una aplicación para comprender mejor los tres momentos que son: el inicio de la aplicación, de la sesión y de una solicitud del cliente. El archivo [global.asax] será el siguiente:

<%@ Application src="Global.asax.vb" Inherits="global" %>

El archivo [global.asax.vb] asociado será el siguiente:


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la aplicación
        ' se registra la hora
        Dim startApplication As String = Date.Now.ToString("T")
        ' Se guarda en el contexto de la aplicación
        Application.Item("startApplication") = startApplication
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la sesión
        ' se registra la hora
        Dim startSession As String = Date.Now.ToString("T")
        ' se incorpora a la sesión
        Session.Item("startSession") = startSession
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' se registra la hora
        Dim startRequest As String = Date.Now.ToString("T")
        ' se agrega a la sesión
        Context.Items("startRequest") = startRequest
    End Sub
End Class

Los puntos importantes del código son los siguientes:

  • el servidor web pone a disposición de la clase [HttpApplication] de [global.asax.vb] una serie de objetos:
    • Aplicación de tipo [HttpApplicationState] —representa la aplicación web— da acceso a un diccionario de objetos [Application.Item] al que pueden acceder todos los clientes de la aplicación; permite compartir información entre diferentes clientes; el acceso simultáneo de varios clientes a un mismo dato en lectura/escritura requiere la sincronización de los clientes.
    • Sesión de tipo [HttpSessionState]: representa a un cliente específico; brinda acceso a un diccionario de objetos [Session.Item], al que pueden acceder todas las solicitudes de ese cliente; permitirá almacenar información sobre un cliente que se podrá recuperar a lo largo de sus solicitudes.
    • Solicitud de tipo [HttpRequest]: representa la solicitud actual HTTP del cliente
    • Respuesta de tipo [HttpResponse]: representa la respuesta HTTP que el servidor está generando para el cliente
    • Servidor de tipo [HttpServerUtility]: ofrece métodos de utilidad, en particular para redirigir la solicitud a una página distinta de la prevista inicialmente.
    • Contexto de tipo [HttpContext]: este objeto se recrea con cada nueva solicitud, pero es compartido por todas las páginas que participan en el procesamiento de la solicitud; permite transmitir información de una página a otra durante el procesamiento de una solicitud gracias a su diccionario Items.
  • El procedimiento [Application_Start] registra el inicio de la aplicación en una variable almacenada en un diccionario accesible a nivel de aplicación
  • El procedimiento [Session_Start] registra el inicio de la sesión en una variable almacenada en un diccionario accesible a nivel de sesión
  • el procedimiento [Application_BeginRequest] registra el inicio de la consulta en una variable almacenada en un diccionario accesible a nivel de consulta (c.a.d, disponible durante todo el tiempo que dura su procesamiento, pero que se pierde al finalizar este)

La página de destino será la siguiente página [main.aspx]:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
    <head>
        <title>global.asax</title>
    </head>
    <body>
        jeton de session  :
        <% =jeton %><br/>
        début Application  :
        <% =startApplication %><br/>
        début Session  :
        <% =startSession %><br/>
        début Requête  :
        <% =startRequest %><br/>        
    </body>
</html>

Esta página de presentación muestra los valores calculados por su controlador [main.aspx.vb]:

Public Class main
    Inherits System.Web.UI.Page

    Protected startApplication As String
    Protected startSession As String
    Protected startRequest As String
    Protected jeton as String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' se recuperan los datos de la aplicación y de la sesión
        jeton=Session.SessionId
        startApplication = Application.Item("startApplication").ToString
        startSession = Session.Item("startSession").ToString
        startRequest = Context.Items("startRequest").ToString
    End Sub

End Class

El controlador simplemente recupera los tres datos almacenados respectivamente en la aplicación, la sesión y el contexto por [global.asax.vb].

Probamos la aplicación de la siguiente manera:

  1. los archivos se agrupan en una misma carpeta <application-path>

Image

  1. se inicia el servidor Cassini con los parámetros (<ruta-de-la-aplicación>,/aspnet/globalasax1)
  2. un primer cliente solicita la URL [http://localhost/aspnet/globalasax1/main.aspx] y obtiene el siguiente resultado:

Image

  1. El mismo cliente realiza una nueva solicitud (opción «Actualizar» del navegador):

Image

Se puede observar que solo ha cambiado la hora de la solicitud. Esto demuestra dos cosas:

  • los procedimientos [Application_Start] y [Session_Start] de [global.asax] no se ejecutaron durante la segunda solicitud.
  • los objetos [Application] y [Session], donde se almacenaban las horas de inicio de la aplicación y de la sesión, siguen estando disponibles para la segunda solicitud.
  1. Abrimos un segundo navegador para crear un segundo cliente y volvemos a solicitar la misma URL:

Image

Esta vez, vemos que la hora de la sesión ha cambiado. El segundo navegador, aunque está en la misma máquina, fue considerado como un segundo cliente y se creó una nueva sesión para él. Se puede observar que los dos clientes no tienen el mismo token de sesión. La hora de inicio de la aplicación no ha cambiado, lo que significa que:

  • el procedimiento [Application_Start] de [global.asax.vb] no se ha ejecutado
  • el objeto [Application], donde se almacenó la hora de inicio de la aplicación, es accesible para el segundo cliente. Por lo tanto, es en este objeto donde se debe almacenar la información que deben compartir los distintos clientes de la aplicación, mientras que el objeto [Session] sirve para almacenar la información que deben compartir las consultas de un mismo cliente.

4.1.3.3. Una visión general

Con lo que hemos aprendido hasta ahora, podemos trazar un primer esquema explicativo del funcionamiento de un servidor web y de las aplicaciones web a las que da servicio:

Image

El diagrama anterior nos muestra un servidor que atiende a dos aplicaciones denominadas A y B, cada una con dos clientes. Un servidor web es capaz de atender varias aplicaciones web simultáneamente. Estas son totalmente independientes entre sí. Nos centraremos en la aplicación A. El procesamiento de una solicitud del cliente-1A a la aplicación A se llevará a cabo de la siguiente manera:

  • el cliente 1A solicita al servidor web un recurso que pertenece al dominio de la aplicación A. Esto significa que solicita un URL con la forma [http://machine:port/VA/ressource], donde VA es la ruta virtual de la aplicación A.
  • Si el servidor web detecta que es la primera solicitud de un recurso de la aplicación A, activa el evento [Application_Start] del archivo [global.asax] de la aplicación A. Se creará un objeto [ApplicationA] de tipo [HttpApplicationState]. Los distintos códigos de la aplicación almacenarán en este objeto datos de los ámbitos [Application] y c.a.d, que contienen información sobre todos los usuarios. El objeto [ApplicationA] permanecerá activo hasta que el servidor web cierre la aplicación A.
  • Si, además, el servidor web detecta que se trata de un nuevo cliente de la aplicación A, activará el evento [Session_Start] del archivo [global.asax] de la aplicación A. Se creará un objeto [Session-1A] de tipo [HttpSessionState]. Este objeto permitirá a la aplicación A almacenar objetos de alcance [Session] y c.a.d que pertenecen a un cliente específico. El objeto [Session-1A] existirá mientras el cliente 1A realice solicitudes. Permitirá el seguimiento de este cliente. El servidor web detecta que se trata de un nuevo cliente en dos casos:
    • el cliente no le ha enviado un token de sesión en los encabezados HTTP de su solicitud
    • el cliente le ha enviado un token de sesión que no existe (mal funcionamiento del cliente o intento de piratería) o que ya no existe. De hecho, un token de sesión caduca tras un cierto período de inactividad del cliente (20 minutos por defecto con IIS). Este período es configurable.
  • En cualquier caso, el servidor web activará el evento [Application_BeginRequest] del archivo [global.asax]. Este evento inicia el procesamiento de una solicitud del cliente. Es común no procesar este evento y pasar el control a la página solicitada por el cliente, la cual se encargará de procesar la solicitud. También se puede utilizar este evento para analizar la solicitud, procesarla y decidir qué página se debe enviar como respuesta. Utilizaremos esta técnica para implementar una aplicación que respete la arquitectura MVC de la que hemos hablado.
  • Una vez que la solicitud del cliente ha pasado por el filtro [global.asax], se transfiere a una página .aspx que se encargará de procesarla. Más adelante veremos que es posible hacer pasar la solicitud a través de un filtro de varias páginas. La última se encargará de enviar la respuesta al cliente. Las páginas pueden agregar a la solicitud inicial del cliente información que ellas mismas hayan calculado. Pueden almacenar esta información en la colección Context.Items. De hecho, todas las páginas involucradas en el procesamiento de la solicitud de un cliente tienen acceso a este repositorio de datos.
  • El código de las distintas páginas tiene acceso a los repositorios de datos que son los objetos [ApplicationA], [Session-1A], ... Hay que recordar que el servidor web atiende simultáneamente a varios clientes para la aplicación A. Todos estos clientes tienen acceso al objeto [Application A]. Si necesitan modificar datos en este objeto, es necesario realizar una sincronización entre los clientes. Además, cada cliente XA tiene acceso al repositorio de datos [Session-XA]. Dado que este le está reservado, no es necesario realizar ninguna sincronización en este caso.
  • El servidor web atiende varias aplicaciones web simultáneamente. No hay ninguna interferencia entre los clientes de estas diferentes aplicaciones.

De estas explicaciones, cabe destacar los siguientes puntos:

  • En un momento dado, un servidor web atiende a múltiples clientes de manera simultánea. Esto significa que no espera a que termine una solicitud para procesar otra. Por lo tanto, en un momento T, hay varias solicitudes en proceso de ser procesadas que pertenecen a diferentes clientes para diferentes aplicaciones. A veces se les llama «hilos de ejecución» a los códigos de procesamiento que se ejecutan al mismo tiempo dentro del servidor web.
  • Los hilos de ejecución de los clientes de diferentes aplicaciones web no interfieren entre sí. Existe aislamiento.
  • Los hilos de ejecución de los clientes de una misma aplicación pueden tener que compartir datos:
    • los hilos de ejecución de las solicitudes de dos clientes diferentes (que no comparten el mismo token de sesión) pueden compartir datos mediante el objeto [Application].
    • Los hilos de ejecución de las solicitudes sucesivas de un mismo cliente pueden compartir datos mediante el objeto [Session].
    • los hilos de ejecución de las páginas sucesivas que procesan una misma solicitud de un cliente determinado pueden compartir datos mediante el objeto [Context].

4.1.3.4. Ejemplo 2

Desarrollemos un nuevo ejemplo que ilustre lo que acabamos de ver. Reunimos en la misma carpeta los siguientes archivos:

[global.asax]

<%@ Application src="Global.asax.vb" Inherits="global" %>

[global.asax.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa cuando se inicia la aplicación
        ' inicializa el contador de clientes
        Application.Item("nbRequêtes") = 0
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la sesión
        ' Inicializa el contador de solicitudes
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

El objetivo de la aplicación será contar el número total de solicitudes realizadas a la aplicación y el número de solicitudes por cliente. Cuando se inicia la aplicación [Application_Start], se pone a 0 el contador de solicitudes realizadas a la aplicación. Este contador se coloca en el ámbito [Application], ya que debe ser incrementado por todos los clientes. Cuando un cliente se conecta por primera vez a [Session_Start], se pone a 0 el contador de solicitudes realizadas por ese cliente. Este contador se coloca en el ámbito [Session], ya que solo se refiere a un cliente específico.

Una vez ejecutado [global.asax], se ejecutará el siguiente archivo [main.aspx]:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
    <head>
        <title>application-session</title>
    </head>
    <body>
        jeton de session :
        <% =jeton %>
        <br />
        requêtes Application :
        <% =nbRequêtesApplication %>
        <br />
        requêtes Client :
        <% =nbRequêtesClient %>
        <br />
    </body>
</html>

Muestra tres datos calculados por su controlador:

  1. la identidad del cliente a través de su token de sesión: [jeton]
  2. el número total de solicitudes realizadas a la aplicación: [nbRequêtesApplication]
  3. el número total de solicitudes realizadas por el cliente identificado en 1: [nbRequêtesClient]

Los tres datos se calculan en [main.aspx.vb]:

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As String
    Protected nbRequêtesClient As String
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' una solicitud más para la aplicación
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
         ' una solicitud más en la sesión
        Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
         ' Inicializa las variables de presentación
        nbRequêtesApplication = Application.Item("nbRequêtes").ToString
        jeton = Session.SessionID
        nbRequêtesClient = Session.Item("nbRequêtes").ToString
    End Sub
End Class

Cuando se ejecuta [main.aspx.vb], estamos procesando una solicitud de un cliente específico. Utilizamos el objeto [Application] para incrementar el número de solicitudes de la aplicación y el objeto [Session] para incrementar el número de solicitudes del cliente cuya solicitud estamos procesando. Recordemos que, si bien todos los clientes de una misma aplicación comparten el mismo objeto [Application], cada uno de ellos tiene un objeto [Session] propio.

Probamos la aplicación colocando los cuatro archivos anteriores en una carpeta que llamamos <application-path> e iniciamos el servidor Cassini con los parámetros (<application-path>,/aspnet/webapplia). Abrimos un primer navegador y solicitamos la URL [http://localhost/aspnet/webapplia/main.aspx]:

Image

Realizamos una segunda solicitud con el botón [Reload]:

Image

Abrimos un segundo navegador para solicitar la misma URL. Para el servidor web, se trata de un nuevo cliente:

Image

Se puede observar que el token de sesión ha cambiado y que, por lo tanto, tenemos un nuevo cliente. Esto se refleja en el número de solicitudes del cliente. Regresemos ahora al primer navegador y volvamos a solicitar la misma URL:

Image

El número de solicitudes realizadas a la aplicación se cuenta correctamente.

4.1.3.5. Sobre la necesidad de sincronizar los clientes de una aplicación

En la aplicación anterior, el contador de solicitudes realizadas a la aplicación se incrementa en el procedimiento [Form_Load] de la página [main.aspx] de la siguiente manera:

         ' una solicitud más para la aplicación
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1

Esta instrucción, aunque sencilla, requiere varias instrucciones del procesador para ejecutarse. Supongamos que se necesitan tres:

  1. lectura del contador
  2. incremento del contador
  3. reescritura del contador

El servidor web se ejecuta en una máquina multitarea, lo que implica que a cada tarea se le asigna el procesador durante unos milisegundos antes de perderlo y recuperarlo una vez que todas las demás tareas también hayan tenido su tiempo de ejecución. Supongamos que dos clientes, A y B, envían una solicitud al mismo tiempo al servidor web. Supongamos que el cliente A es el primero en ser atendido, que llega al procedimiento [Form_Load] desde [main.aspx.vb], lee el contador (=100) y luego es interrumpido porque se le acaba su tiempo de ejecución. Supongamos ahora que le toca el turno al cliente B y que este sufre la misma suerte: logra leer el valor del contador (=100), pero no tiene tiempo de incrementarlo. Tanto el cliente A como el cliente B tienen un contador con valor 100. Supongamos que vuelve a ser el turno del cliente A: incrementa su contador, lo pone en 101 y luego termina. Es el turno del cliente B, quien tiene en su poder el valor anterior del contador y no el nuevo. Por lo tanto, él también pone el valor del contador en 101 y termina. El valor del contador de solicitudes de la aplicación ahora es erróneo.

Para ilustrar este problema, retomamos la aplicación anterior y la modificamos de la siguiente manera:

  • los archivos [global.asax], [global.asax.vb] y [main.aspx] no cambian
  • el archivo [main.aspx.vb] queda así:

Imports System.Threading

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As Integer
    Protected nbRequêtesClient As Integer
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' una solicitud más para la aplicación y la sesión
        ' lectura de contadores
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' espera de 5 s
        Thread.Sleep(5000)
        ' incremento de los contadores
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' registro de los contadores
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' inicialización de variables de presentación
        jeton = Session.SessionID
    End Sub
End Class

El incremento de los contadores se ha dividido en cuatro etapas:

  1. lectura del contador
  2. suspensión del hilo de ejecución
  3. incremento del contador
  4. reescritura del contador

Consideremos nuevamente a nuestros dos clientes, A y B. Entre la fase de lectura y la de incremento de los contadores de solicitudes, obligamos al hilo de ejecución a detenerse durante 5 segundos. Esto tendrá como consecuencia inmediata que pierda el procesador, el cual se asignará entonces a otra tarea. Supongamos que el cliente A pasa primero. Leerá el valor N del contador y será interrumpido durante 5 segundos. Si durante ese tiempo el cliente B tiene acceso al procesador, debería leer el mismo valor N del contador. Al final, ambos clientes deberían mostrar el mismo valor del contador, lo cual sería anormal.

Probamos la aplicación colocando los cuatro archivos anteriores en una carpeta que llamamos <application-path> y ejecutamos el servidor Cassini con los parámetros (<application-path>,/aspnet/webapplib). Preparamos dos navegadores diferentes con la URL [http://localhost/aspnet/webapplib/main.aspx]. Abrimos el primero para que solicite el URL; luego, sin esperar la respuesta que llegará 5 segundos más tarde, abrimos el segundo navegador. Al cabo de poco más de 5 segundos, obtenemos el siguiente resultado:

Image

Se observa:

  • que hay dos clientes diferentes (no tienen el mismo token de sesión)
  • que cada cliente ha realizado una solicitud
  • que el contador de solicitudes realizadas a la aplicación debería estar, por lo tanto, en 2 en uno de los dos navegadores. Pero no es así.

Ahora, hagamos otro experimento. Con el mismo navegador, enviamos cinco solicitudes a la URL [http://localhost/aspnet/webapplib/main.aspx]. Una vez más, las enviamos una tras otra sin esperar los resultados. Cuando se han ejecutado todas las solicitudes, obtenemos el siguiente resultado para la última:

Image

Se puede observar:

  • que las 5 solicitudes se han considerado como provenientes del mismo cliente, ya que el contador de solicitudes del cliente está en 5. Aunque no se muestra arriba, se observa que el token de sesión es efectivamente el mismo para las 5 solicitudes.
  • que el contador de solicitudes realizadas a la aplicación es correcto.

¿Qué podemos concluir? Nada definitivo. ¿Quizás el servidor web no comienza a ejecutar una solicitud de un cliente si este ya tiene una en ejecución? Por lo tanto, nunca habría simultaneidad en la ejecución de las solicitudes de un mismo cliente. Se ejecutarían una tras otra. Este punto debe verificarse. De hecho, puede depender del tipo de cliente utilizado.

4.1.3.6. Sincronización de clientes

El problema que se puso de manifiesto en la aplicación anterior es un problema clásico (pero no fácil de resolver) de acceso exclusivo a un recurso. En nuestro caso particular, hay que asegurarse de que dos clientes, A y B, no puedan estar al mismo tiempo en la secuencia de código:

  1. lectura del contador
  2. incremento del contador
  3. reescritura del contador

A dicha secuencia de código se le denomina secuencia crítica. Requiere la sincronización de los hilos que la ejecutan simultáneamente. La plataforma .NET ofrece diversas herramientas para garantizarla. Aquí utilizaremos la clase [Mutex].

Image

Aquí solo utilizaremos los siguientes constructores y métodos:

public Mutex()
crea un objeto de sincronización M
public bool WaitOne()
El hilo T1 que ejecuta la operación M.WaitOne() solicita la propiedad del objeto de sincronización M. Si ningún hilo tiene el control del mutex M (lo cual ocurre al inicio), se le “asigna” al hilo T1 que lo solicitó. Si un poco más tarde, un hilo T2 realiza la misma operación, quedará bloqueado. De hecho, un mutex solo puede pertenecer a un hilo. Se desbloqueará cuando el hilo T1 libere el mutex M que tiene en su poder. De esta manera, varios hilos pueden quedar bloqueados a la espera del mutex M.
public void ReleaseMutex()
El hilo T1, que realiza la operación M.ReleaseMutex(), deja de tener la propiedad del mutex M. Cuando el hilo T1 pierda el procesador, el sistema podrá asignárselo a uno de los hilos que están en espera del mutex M. Solo uno lo obtendrá a su vez; los demás que esperan el Mutex M permanecerán bloqueados

Un mutex M administra el acceso a un recurso compartido R. Un hilo solicita el recurso R mediante M.WaitOne() y lo devuelve mediante M.ReleaseMutex(). Una sección crítica de código que solo debe ser ejecutada por un único hilo a la vez es un recurso compartido. La sincronización de la ejecución de la sección crítica puede realizarse así:

M.WaitOne()
' solo el hilo puede ingresar aquí
' sección crítica
....
M.ReleaseMutex()

donde M es un objeto Mutex. Por supuesto, nunca hay que olvidarse de liberar un Mutex que ya no se necesite, para que otro hilo pueda ingresar a su vez a la sección crítica; de lo contrario, los hilos que esperan un mutex que nunca se libera nunca tendrán acceso al procesador. Además, hay que evitar la situación de interbloqueo (deadlock) en la que dos hilos se esperan mutuamente. Consideremos las siguientes acciones que se suceden en el tiempo:

  • un hilo T1 obtiene la propiedad de un mutex M1 para tener acceso a un recurso compartido R1
  • un hilo T2 obtiene la propiedad de un mutex M2 para acceder a un recurso compartido R2
  • el hilo T1 solicita el mutex M2. Queda bloqueado.
  • El hilo T2 solicita el mutex M1. Queda bloqueado.

En este caso, los hilos T1 y T2 se esperan mutuamente. Este caso se presenta cuando los hilos necesitan dos recursos compartidos: el recurso R1 controlado por el mutex M1 y el recurso R2 controlado por el mutex M2. Una posible solución es solicitar ambos recursos al mismo tiempo mediante un único mutex M. Sin embargo, esto no siempre es posible, especialmente si implica una ocupación prolongada de un recurso costoso. Otra solución es que un hilo que tenga M1 y no pueda obtener M2 libere entonces M1 para evitar el interbloqueo.

Si ponemos en práctica lo que acabamos de aprender, nuestra aplicación queda así:

  • los archivos [global.asax] y [main.aspx] no cambian
  • el archivo [global.asax.vb] queda así:

Imports System
Imports System.Web
Imports System.Web.SessionState
Imports System.Threading

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la aplicación
        ' inicialización del contador de clientes
        Application.Item("nbRequêtes") = 0
        ' creación de un bloqueo de sincronización
        Application.Item("verrou") = New Mutex
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Se activa al iniciar la sesión
        ' inicialización del contador de solicitudes
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

La única novedad es la creación de un [Mutex] que será utilizado por los clientes para sincronizarse. Dado que debe ser accesible para todos los clientes, se coloca en el objeto [Application].

  • El archivo [main.aspx.vb] queda así:

Imports System.Threading

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As Integer
    Protected nbRequêtesClient As Integer
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' una solicitud más para la aplicación y la sesión
        ' Se ingresa a una sección crítica: se obtiene el bloqueo de sincronización
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' Se solicita entrar en solitario a la siguiente sección crítica
        verrou.WaitOne()
        ' lectura de contadores
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' espera de 5 s
        Thread.Sleep(5000)
        ' incremento de los contadores
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' registro de los contadores
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' se permite el acceso a la sección crítica
        verrou.ReleaseMutex()
        ' inicialización de variables de presentación
        jeton = Session.SessionID
    End Sub
End Class

Se observa que el cliente:

  • solicita ingresar solo a la sección crítica. Para ello, solicita la propiedad exclusiva del mutex [verrou]
  • libera el mutex [verrou] al final de la sección crítica para que otro cliente pueda ingresar a su vez a la sección crítica.

Probamos la aplicación colocando los cuatro archivos anteriores en una carpeta que llamamos <application-path> y ejecutamos el servidor Cassini con los parámetros (<application-path>,/aspnet/webapplic). Preparamos dos navegadores diferentes con la URL [http://localhost/aspnet/webapplic/main.aspx]. Abrimos el primero para que solicite la URL URL y, sin esperar la respuesta que llegará 5 segundos más tarde, abrimos el segundo navegador. Al cabo de poco más de 5 segundos, obtenemos el siguiente resultado:

Image

Esta vez, el contador de solicitudes de la aplicación es correcto.

De esta extensa demostración, cabe destacar la absoluta necesidad de sincronizar los clientes de una misma aplicación web, si estos deben actualizar elementos compartidos por todos los clientes.

4.1.3.7. Manejo del token de sesión

Hemos hablado en numerosas ocasiones del token de sesión que intercambian el cliente y el servidor web. Recordemos su funcionamiento:

  • el cliente realiza una primera solicitud al servidor. No envía un token de sesión.
  • Debido a la ausencia del token de sesión en la solicitud, el servidor reconoce a un nuevo cliente y le asigna un token. A este token también se le asocia un objeto [Session] que se utilizará para almacenar información específica de ese cliente. El token acompañará todas las solicitudes de este cliente. Se incluirá en los encabezados HTTP de la respuesta a la primera solicitud del cliente.
  • El cliente ahora conoce su token de sesión. Lo reenviará en los encabezados HTTP de cada una de las siguientes solicitudes que realice al servidor web. Gracias al token, el servidor podrá recuperar el objeto [Session] vinculado al cliente.

Para ilustrar este mecanismo, retomamos la aplicación anterior modificando únicamente el archivo [main.aspx.vb]:


Imports System.Threading

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As Integer
    Protected nbRequêtesClient As Integer
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' una solicitud más para la aplicación y la sesión
        ' se ingresa a una sección crítica: se obtiene el bloqueo de sincronización
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' se solicita entrar en solitario a la sección siguiente
        verrou.WaitOne()
        ' lectura de contadores
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' espera de 5 s
        Thread.Sleep(5000)
        ' incremento de los contadores
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' registro de los contadores
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' se permite el acceso a la sección crítica
        verrou.ReleaseMutex()
        ' inicialización de variables de presentación
        jeton = Session.SessionID
    End Sub

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        ' se almacena la solicitud del cliente en request.txt de la carpeta de la aplicación
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub
End Class

Cuando ocurre el evento [Page_Init], guardamos la solicitud del cliente en la carpeta de la aplicación. Recordemos algunos puntos:

  • [TemplateSourceDirectory] representa la ruta virtual de la página que se está ejecutando,
  • MapPath (TemplateSourceDirectory) representa la ruta física correspondiente. Esto nos permite construir la ruta física del archivo que se va a generar,
  • [Request] es un objeto que representa la solicitud que se está procesando. Este objeto se construyó a partir de la solicitud sin procesar enviada por el cliente, c.a.d, una secuencia de líneas de texto con el formato:

Image

  • Request.Save([FileName]) guarda la solicitud completa del cliente (las cabeceras HTTP y, si corresponde, el documento que sigue) en un archivo cuya ruta se pasa como parámetro.

De esta manera, podremos saber exactamente cuál fue la solicitud del cliente. Probamos la aplicación colocando los cuatro archivos anteriores en una carpeta que llamamos <application-path> e iniciamos el servidor Cassini con los parámetros (<application-path>,/aspnet/session1). Luego, con un navegador, solicitamos el URL

[http://localhost/aspnet/session1/main.aspx]. Obtenemos el siguiente resultado:

Image

Utilizamos el archivo [request.txt] guardado por [main.aspx.vb] para acceder a la solicitud del navegador:

GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316

Observamos que el navegador realizó la solicitud de URL [/aspnet/session1/main.aspx] y envió otra información de la que ya hablamos en el capítulo anterior. No se observa ningún token de sesión. La página recibida como respuesta, por su parte, muestra que el servidor ha creado un token de sesión. Aún no sabemos si el navegador lo ha recibido. Realicemos ahora una segunda solicitud con el mismo navegador (Actualizar). Obtenemos la siguiente nueva respuesta:

Image

Sí hay un seguimiento de la sesión, ya que el número de solicitudes de la sesión se ha incrementado correctamente. Veamos ahora el contenido del archivo [request.txt]:

GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Cookie: ASP.NET_SessionId=y153tk45sise0lrhdzrf22m3
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316

Se observa que, para esta segunda solicitud, el navegador envió al servidor un nuevo encabezado HTTP [Cookie:] que define una información llamada [ASP.NET_SessionId] y cuyo valor es el token de sesión que apareció en la respuesta a la primera solicitud. Gracias a este token, el servidor web vinculará esta nueva solicitud con el objeto [Session] identificado por el token [y153tk45sise0lrhdzrf22m3] y recuperará el contador de solicitudes asociado.

Aún no sabemos mediante qué mecanismo el servidor envió el token al cliente, ya que no tenemos acceso a la respuesta HTTP del servidor. Recordemos que esta tiene la misma estructura que la solicitud del cliente, es decir, un conjunto de líneas de texto con el siguiente formato:

Image

Tuvimos la oportunidad de utilizar un cliente web que nos daba acceso a la respuesta HTTP del servidor web: el cliente curl. Lo volvemos a utilizar, en una ventana de DOS, para consultar la misma URL que el navegador anterior:

E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:31:42 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                qxnxmqmvhde3al55kzsmx445
                <br>
                requêtes Application :
                3
                <br>
                requêtes Client :
                1
                <br>
        </body>
</HTML>

Ya tenemos la respuesta a nuestra pregunta. El servidor web envía el token de sesión en forma de un encabezado HTTP [Set-Cookie:]:

Set-Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445; path=/

Hagamos la misma solicitud sin enviar el token de sesión. Obtenemos la siguiente respuesta:

E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:36:06 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                cs2p12mehdiz5v55ihev1kaz
                <br>
                requêtes Application :
                4
                <br>
                requêtes Client :
                1
                <br>
        </body>
</HTML>

Como no enviamos el token de sesión, el servidor no pudo identificarnos y nos asignó un nuevo token. Para continuar con una sesión ya iniciada, el cliente debe enviar al servidor el token de sesión que recibió. Lo haremos aquí utilizando la opción [--cookie clé=valeur] de curl, que generará el encabezado HTTP [Cookie: clé=valeur]. Hemos visto que el navegador envió este encabezado HTTP en su segunda solicitud.

E:\curl>curl --include --cookie ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:40:20 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                cs2p12mehdiz5v55ihev1kaz
                <br>
                requêtes Application :
                5
                <br>
                requêtes Client :
                2
                <br>
        </body>
</HTML>

Cabe destacar varias cosas:

  • el contador de solicitudes del cliente se ha incrementado, lo que demuestra que el servidor reconoció correctamente nuestro token.
  • el token de sesión que muestra la página es, efectivamente, el que enviamos
  • el token de sesión ya no aparece en los encabezados HTTP enviados por el servidor web. De hecho, este solo lo envía una vez: al generar el token al inicio de una nueva sesión. Una vez que el cliente ha obtenido su token, le corresponde a él utilizarlo cuando lo desee para que lo reconozcan.

Nada impide que un cliente utilice varios tokens de sesión, como lo muestra el siguiente ejemplo con [curl], donde usamos el token obtenido en nuestra primera solicitud (solicitud n.º 1):

E:\curl>curl --include --cookie ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445 http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:48:47 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                qxnxmqmvhde3al55kzsmx445
                <br>
                requêtes Application :
                6
                <br>
                requêtes Client :
                2
                <br>
        </body>
</HTML>

¿Qué significa este ejemplo? Enviamos un token que habíamos obtenido un poco antes. Cuando el servidor web crea un token, lo conserva mientras el cliente asociado a ese token siga enviándole solicitudes. Después de un cierto tiempo de inactividad (20 minutos por defecto con IIS), el token se elimina. El ejemplo anterior muestra que utilizamos un token que aún estaba activo.

Quizás nos interese saber cuáles fueron las solicitudes HTTP del cliente [curl] durante todas estas operaciones. Sabemos que se registraron en el archivo [request.txt]. Aquí está la última:

GET /aspnet/session1/main.aspx HTTP/1.1
Pragma: no-cache
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4

Aquí se encuentra el encabezado HTTP que envía el token de sesión.

La información transmitida por el servidor a través de los encabezados HTTP y [Set-Cookie:] se conoce como cookies. El servidor puede utilizar este mecanismo para transmitir otra información además del token de sesión. Cuando el servidor S transmite una cookie a un cliente, también indica su tiempo de vida D y el valor U asociado. Esto significa que, para el cliente, cuando solicita al servidor S una URL del tipo /U/ruta, puede reenviar la cookie si no la ha recibido desde hace un tiempo superior a D. Nada impide que un cliente no respete este código de conducta. Los navegadores, por su parte, sí lo respetan. Algunos navegadores permiten acceder al contenido de las cookies que reciben. Este es el caso del navegador Mozilla. A continuación se muestra, por ejemplo, la información relacionada con la cookie enviada por el servidor en un ejemplo anterior:

Image

En ella se encuentra:

  • el nombre de la cookie [ASP.NET_SessionId]
  • su valor: [y153...m3]
  • la máquina a la que está asociada: [localhost]
  • la URL a la que está asociada: [/]
  • su tiempo de vida: [at end of session]

Por lo tanto, el navegador enviará el token de sesión cada vez que solicite una URL con el formato [http://localhost/...], c.a.d. cada vez que solicite una URL al servidor web de la máquina [localhost]. La vida útil de la cookie es la misma que la de la sesión. Para el navegador, esto significa que la cookie nunca caduca. La enviará cada vez que solicite una URL de la máquina [localhost]. Así, si el navegador recibe el token de sesión el día D, se cierra y se vuelve a utilizar al día siguiente, reenviará el token de sesión (que se ha guardado en un archivo). El servidor recibirá este token que ya no tiene, ya que un token de sesión tiene una vida útil limitada en el servidor (20 minutos en IIS). Por lo tanto, iniciará una nueva sesión.

Es posible desactivar el uso de cookies en un navegador. En ese caso, el cliente recibe el token de sesión, pero no lo reenvía, lo que impide el seguimiento de la sesión. Para demostrarlo, desactivamos el uso de cookies en nuestro navegador (Mozilla en este caso):

Image

Además, eliminamos todas las cookies existentes:

Image

Una vez hecho esto, reiniciamos el servidor Cassini para empezar de cero y, con el navegador, volvemos a solicitar la URL [http://localhost/aspnet/session1/main.aspx]:

Image

Veamos si nuestro navegador ha almacenado una cookie:

Image

Observamos que el navegador no ha almacenado la cookie del token de sesión que el servidor le envió. Por lo tanto, es de esperarse que no haya seguimiento de la sesión. Volvemos a solicitar la misma URL (Actualizar):

Image

Obtuvimos lo que esperábamos. El navegador no devolvió el token de sesión, que sí había recibido pero no había almacenado. Por lo tanto, el servidor inició una nueva sesión con un nuevo token. De este ejemplo se desprende que nuestra política de seguimiento de sesiones se ve comprometida si el usuario ha desactivado el uso de cookies en su navegador. Sin embargo, existe otra forma, además de las cookies, de intercambiar el token de sesión entre el servidor y el cliente. De hecho, es posible indicar al servidor web que la aplicación funciona sin cookies. Esto se hace mediante el archivo de configuración [web.config]:


<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <system.web>
        <sessionState cookieless="true" timeout="10" />
    </system.web>
</configuration>

El archivo de configuración anterior indica que la aplicación funcionará sin cookies (cookieless="true") y que el tiempo máximo de inactividad de un token de sesión es de 10 minutos (timeout="10"). Pasado este tiempo, la sesión asociada al token se elimina. El proceso de intercambio del token de sesión entre el servidor y el cliente será el siguiente:

  1. el cliente solicita la URL [http://machine:port/V/chemin], donde V es una carpeta virtual del servidor web
  2. el servidor genera un token J y responde al cliente indicándole que se redirija a la URL [http://machine:port/V/(J)/chemin]. Por lo tanto, ha colocado el token en la URL que se va a consultar, inmediatamente después de la carpeta virtual V
  3. el cliente obedece esta redirección y solicita la nueva URL URL [http://machine:port/V/(J)/chemin].
  4. El servidor responde a esta solicitud y envía una página de respuesta.

Ilustremos estos diferentes puntos. Colocamos toda la aplicación anterior en una nueva carpeta <application-path>. Colocamos en esa misma carpeta el archivo [web.config] anterior. Además, modificamos el código de presentación [main.aspx] para incluir un enlace:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <HEAD>
        <title>application-session</title>
    </HEAD>
    <body>
        jeton de session :
        <% =jeton %>
        <br>
        requêtes Application :
        <% =nbRequêtesApplication %>
        <br>
        requêtes Client :
        <% =nbRequêtesClient %>
        <br>
        <a href="main.aspx">Recharger l'application</a>
    </body>
</HTML>

Este enlace apunta a la página [main.aspx] y, por lo tanto, equivale al botón (Actualizar) del navegador. El servidor Cassini se inicia con los parámetros (<application-path>,/session2). Nos apartamos de nuestra costumbre habitual de anotar la carpeta virtual [/aspnet/XX]. De hecho, debido a la inserción del token de sesión en la URL, la carpeta virtual solo debe contener un elemento: /XX. Primero utilizamos el cliente [curl] para solicitar la URL [http://localhost/session2/main.aspx]:

E:\curl>curl --include http://localhost/session2/main.aspx
HTTP/1.1 302 Found
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 13:52:36 GMT
X-AspNet-Version: 1.1.4322
Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 163
Connection: Close

<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href='/session2/(hinadjag3bt0u155g5hqe245)/main.aspx'>here
</body></html>

Vemos que el servidor responde con el encabezado HTTP [HTTP/1.1 302 Found] en lugar de [HTTP/1.1 200 OK]. Se trata de un encabezado que le pide al cliente que se redirija a la URL indicada por el encabezado HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. Se observa el token de sesión que se ha insertado en la URL de redirección. Un navegador que recibe esta respuesta solicita la nueva URL de manera transparente para el usuario, quien no ve la nueva solicitud. En caso de que el navegador no gestione la redirección por sí solo, se envía un documento HTML junto con el código HTTP mencionado anteriormente. En él se encuentra un enlace a la URL de redirección, enlace en el que el usuario podrá hacer clic.

Ahora, hagamos lo mismo con un navegador en el que se hayan desactivado las cookies. Volvemos a solicitar la URL [http://localhost/session2/main.aspx]. Obtenemos la siguiente respuesta del servidor:

Image

En primer lugar, observemos que la URL que muestra el navegador no es la que solicitamos. Esto indica que se ha producido una redirección. De hecho, el navegador siempre muestra la URL URL del último documento recibido. Por lo tanto, si no muestra la URL [http://localhost/session2/main.aspx], es porque se le ha pedido que se redirija a otra URL. Puede haber varias redirecciones. La URL que muestra el navegador es la de la última redirección. Podemos observar que el token de sesión está presente en la URL que muestra el navegador. Esto se puede ver porque nuestro programa también muestra este token en la página.

Recordemos el código del enlace que se colocó en la página:


        <a href="main.aspx">Recharger l'application</a>

Se trata de un enlace relativo, ya que no comienza con el signo /, lo cual lo convertiría en un enlace absoluto. ¿Relativo a qué? Para entender este punto, debemos volver a la URL del documento que se muestra actualmente: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Los enlaces relativos que se encuentren en este documento serán relativos a la ruta [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Por lo tanto, nuestro enlace anterior es equivalente al siguiente:


        <a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>

Esto es lo que nos muestra el navegador al pasar el cursor sobre el enlace:

Image

Si hacemos clic en el enlace [Recharger l'application], se llamará a la URL

[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx] la que se llama. Por lo tanto, el servidor recibirá el token de sesión y podrá recuperar la información relacionada con él. Esto es lo que nos muestra la respuesta del navegador:

Image

Recordemos que si necesitamos realizar un seguimiento de la sesión en una aplicación web y no estamos seguros de que los navegadores de los usuarios de dicha aplicación vayan a permitir el uso de cookies, entonces

  • debemos configurar la aplicación para que funcione sin cookies
  • las páginas de la aplicación deben incluir enlaces relativos y no absolutos

4.2. Recuperar la información de una solicitud del cliente

4.2.1. El ciclo de solicitud-respuesta del cliente-servidor web

Recordemos aquí el contexto cliente-servidor de una aplicación web:

Image

La solicitud de un cliente para una aplicación web se procesa de la siguiente manera:

  1. el cliente abre una conexión TCP/IP hacia un puerto P del servicio web de la máquina M que aloja la aplicación web
  2. envía por esta conexión una secuencia de líneas de texto según el protocolo HTTP. Este conjunto de líneas forma lo que se conoce como la solicitud del cliente. Tiene la siguiente forma:

Image

Una vez enviada la solicitud, el cliente esperará la respuesta.

  1. La primera línea de los encabezados, HTTP, especifica la acción solicitada al servidor web. Puede tener varias formas:
    1. GET url HTTP/<versión>, donde <versión> es actualmente 1.0 o 1.1. En este caso, la solicitud no incluye la parte [Document]
    2. POST url HTTP/<versión>. En este caso, la solicitud incluye una parte [Document], que suele ser una lista de información destinada a la aplicación web
    3. PUT url HTTP/<versión>. El cliente envía un documento en la parte [Document] y desea almacenarlo en el servidor en la dirección url

Cuando el cliente desea transmitir información a la aplicación web a la que se ha conectado, cuenta principalmente con dos medios:

  • (continuación)
    1. su solicitud es [GET url_enrichie HTTP/<version>], donde url_enrichie tiene el formato [url?param1=val1&param2=val2&...]. El cliente transmite, además de la URL, una serie de datos con el formato [clé=valeur].
    2. Su solicitud es [POST url HTTP/<version>]. En la parte [Document], transmite información con el mismo formato que antes: [param1=val1&param2=val2&...].
  1. En el servidor, toda la cadena de procesamiento de la solicitud del cliente tiene acceso a ella a través de un objeto global llamado Request. El servidor web ha colocado en este objeto la totalidad de la solicitud del cliente en un formato que veremos a continuación. La aplicación solicitada procesará este objeto y generará una respuesta para el cliente. Esta respuesta está disponible en un objeto global llamado Response. La función de la aplicación web es generar un objeto [Response] a partir del objeto [Request] recibido. La cadena de procesamiento también cuenta con los objetos globales [Application] y [Session], de los que ya hemos hablado y que le permitirán compartir datos entre diferentes clientes (Aplicación) o entre solicitudes sucesivas de un mismo cliente (Sesión).
  2. La aplicación enviará su respuesta al servidor mediante el objeto [Response]. Esta, una vez en la red, tendrá la siguiente forma: HTTP:

Image

Una vez enviada esta respuesta, el servidor cerrará la conexión de red de recepción (a menos que el cliente le haya indicado que no lo haga).

  1. El cliente recibirá la respuesta y, a su vez, cerrará la conexión (en transmisión). Lo que se haga con esta respuesta depende del tipo de cliente. Si se trata de un navegador y el documento recibido es un documento HTML, este se mostrará. Si el cliente es un programa, la respuesta se analizará y se procesará.
  2. El hecho de que, tras el ciclo de solicitud-respuesta, se cierre la conexión que unía al cliente con el servidor convierte al protocolo HTTP en un protocolo sin estado. En la siguiente solicitud, el cliente establecerá una nueva conexión de red con el mismo servidor. Dado que ya no se trata de la misma conexión de red, el servidor no tiene forma alguna (a nivel de TCP/IP y HTTP) de vincular esta nueva conexión con una anterior. Es el sistema de token de sesión el que permitirá esta vinculación.

4.2.2. Recuperar la información transmitida por el cliente

Ahora analizaremos algunas propiedades y métodos del objeto [Request] que permiten al código de la aplicación acceder a la solicitud del cliente y, por lo tanto, a la información que este ha transmitido. El objeto [Request] es de tipo [HttpRequest]:

Image

Esta clase cuenta con numerosas propiedades y métodos. Nos interesan las propiedades HttpMethod, QueryString, Form y Params, que nos permitirán acceder a los elementos de la cadena de información [param1=val1&param2=val2&...].

HttpMethod as String
método de consulta del cliente: GET, POST, HEAD, ...
QueryString as NameValueCollection
Recopilación de los elementos de la cadena de consulta param1=val1&param2=val2&... de la primera línea HTTP [méthode]?param1=val1&param2=val2&... donde [méthode] puede ser GET, POST, HEAD.
Form as NameValueCollection
recolección de los elementos de la cadena de consulta param1=val1&param2=val2&... que se encuentran en la parte [Document] de la consulta (método POST).
Params as NameValueCollection
reúne varias colecciones: QueryString, Form, ServerVariables y Cookies en una sola colección.

4.2.3. Ejemplo 1

Apliquemos estos elementos en un primer ejemplo. La aplicación solo tendrá un elemento [main.aspx]. El código de presentación [main.aspx] será el siguiente:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
    <head>
        <title>Requête client</title>
    </head>
    <body>
        Requête :
        <% = méthode %>
        <br />
        nom :
        <% = nom %>
        <br />
        âge :
        <% = age %>
        <br />
    </body>
</html>

La página muestra tres datos [méthode, nom, age] calculados por su controlador [main.aspx.vb]:

Public Class main
    Inherits System.Web.UI.Page

    Protected nom As String = "xx"
    Protected age As String = "yy"
    Protected méthode As String

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
         ' se almacena la solicitud del cliente en request.txt en la carpeta de la aplicación
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' se recuperan los parámetros de la solicitud
        méthode = Request.HttpMethod.ToLower
        If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
        If Not Request.QueryString("age") Is Nothing Then age = Request.QueryString("age").ToString
        If Not Request.Form("nom") Is Nothing Then nom = Request.Form("nom").ToString
        If Not Request.Form("age") Is Nothing Then age = Request.Form("age").ToString
    End Sub

End Class

Cuando se carga la página (Form_Load), se recupera la información de [nom, age] de la solicitud del cliente. Se busca en las dos colecciones [QueryString] y [Form]. . Además, en [Page_Init], almacenamos la solicitud del cliente para poder verificar lo que ha enviado. Colocamos estos dos archivos en una carpeta <application-path> e iniciamos el servidor Cassini con los parámetros (<application-path>,/request1); luego, con un navegador, solicitamos la URL

[http://localhost/request1/main.aspx?nom=tintin&age=27]. Obtenemos la siguiente respuesta:

Image

La información enviada por el cliente se ha recuperado correctamente. La solicitud del navegador almacenada en el archivo [request.txt] es la siguiente:

GET /request1/main.aspx?nom=tintin&age=27 HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

Se observa que el navegador ha realizado una solicitud GET. Para realizar una solicitud POST, utilizaremos el cliente [curl]. En una ventana de DOS, escribimos el siguiente comando:

C:\curl>curl --include --data nom=tintin --data age=27 http://localhost/request1/main.aspx
--include
para mostrar los encabezados HTTP de la respuesta
--data param=valeur
para enviar la información «param=valor» mediante un POST

La respuesta del servidor es la siguiente:

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 09:27:25 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 178
Connection: Close


<html>
        <head>
                <title>Requête client</title>
        </head>
        <body>
                Requête :
                post
                <br />
                nom :
                tintin
                <br />
                âge :
                27
                <br />
        </body>
</html>

El servidor, una vez más, ha recuperado correctamente los parámetros enviados esta vez por un POST. Para asegurarse de este último punto, se puede verificar el contenido del archivo [request.txt]:

POST /request1/main.aspx HTTP/1.1
Pragma: no-cache
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4

nom=tintin&age=27

El cliente [curl] sí realizó un POST. Ahora, combinemos los dos métodos de transmisión de información. Ponemos [age] en la URL solicitada y [nom] en el documento enviado:

E:\curl>curl --include --data nom="tintin" http://localhost/request1/main.aspx?edad=27

La solicitud enviada por [curl] es la siguiente (request.txt):

POST /request1/main.aspx?age=27 HTTP/1.1
Pragma: no-cache
Content-Length: 10
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4

nom=tintin

Se observa que la edad se ha pasado a la URL solicitada. La obtendremos en la colección [QueryString]. El nombre, por su parte, se ha pasado al documento enviado a esa URL. Lo obtendremos en la colección [Form]. La respuesta obtenida por el cliente [curl]:

<html>
        <head>
                <title>Requête client</title>
        </head>
        <body>
                Requête :
                post
                <br />
                nom :
                tintin
                <br />
                âge :
                27
                <br />
        </body>
</html>

Por último, no enviemos ninguna información al servidor:

E:\curl>curl --include http://localhost/request1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 12:43:14 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 173
Connection: Close


<html>
        <head>
                <title>Requête client</title>
        </head>
        <body>
                Requête :
                get
                <br />
                nom :
                xx
                <br />
                âge :
                yy
                <br />
        </body>
</html>

Se invita al lector a volver a leer el código del controlador [main.aspx.vb] para comprender esta respuesta.

4.2.4. Ejemplo 2

El cliente puede enviar varios valores para una misma clave. ¿Qué sucede entonces si, en el ejemplo anterior, solicitamos la URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou], en la que aparece dos veces la clave [nom]? Probemos con un navegador:

Image

Nuestra aplicación recuperó correctamente los dos valores asociados a la clave [nom]. La visualización es un poco engañosa. Se obtuvo mediante la instrucción


        If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString

El método [ToString] generó la cadena [tintin,milou] que se mostró. Esto oculta el hecho de que, en realidad, el objeto [Request.QueryString("nom")] es un array de cadenas de caracteres {"tintin", "milou"}. El siguiente ejemplo ilustra este punto. La página de presentación [main.aspx] será la siguiente:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <HEAD>
        <title>Requête client</title>
    </HEAD>
    <body>
        <P>Informations passées par le client :</P>
        <form runat="server">
            <P>QueryString :</P>
            <P><asp:listbox id="lstQueryString" runat="server" EnableViewState="False" Rows="6"></asp:listbox></P>
            <P>Form :</P>
            <P><asp:listbox id="lstForm" runat="server" EnableViewState="False" Rows="2"></asp:listbox></P>
        </form>
    </body>
</HTML>

Hay novedades en esta página que utiliza lo que se conoce como controles de servidor. Se identifican por el atributo [runat="server"]. Es demasiado pronto para introducir el concepto de control de servidor. Basta con saber que aquí:

  • que la página tiene dos listas (etiquetas <asp:listbox>)
  • que estas listas son objetos (lstQueryString, lstForm) de tipo [ListBox] que serán generados por el controlador de la página
  • que estos objetos solo existen dentro del servidor web. En el momento de la respuesta, se transformarán en etiquetas HTML convencionales que el cliente podrá interpretar. Así, un objeto [listbox] se transformará (también se dice «se renderizará») en las etiquetas HTML <select> y <option>.
  • El objetivo principal de estos objetos es eliminar todo el código VB del código de presentación, ya que este queda confinado al controlador.

El controlador [main.aspx.vb] encargado de construir los dos objetos [lstQueryString] y [lstForm] es el siguiente:


Imports System.Collections
Imports System
Imports System.Collections.Specialized

Public Class main
    Inherits System.Web.UI.Page

    Protected infosQueryString As ArrayList
    Protected WithEvents lstQueryString As System.Web.UI.WebControls.ListBox
    Protected WithEvents lstForm As System.Web.UI.WebControls.ListBox
    Protected infosForm As ArrayList

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        ' se guarda la solicitud del cliente en request.txt dentro de la carpeta de la aplicación
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' se recupera toda la colección de información de QueryString
        infosQueryString = getValeurs(Request.QueryString)
        lstQueryString.DataSource = infosQueryString
        lstQueryString.DataBind()
        infosForm = getValeurs(Request.Form)
        lstForm.DataSource = infosForm
        lstForm.DataBind()
    End Sub

    Private Function getValeurs(ByRef data As NameValueCollection) As ArrayList
        ' al inicio, una lista de información vacía
        Dim infos As New ArrayList
        ' se recuperan las claves de la colección
        Dim clés() As String = data.AllKeys
        ' recorremos la tabla de claves
        Dim valeurs() As String
        For Each clé As String In clés
            ' valores asociados a la clave
            valeurs = data.GetValues(clé)
            ' ¿Un solo valor?
            If valeurs.Length = 1 Then
                infos.Add(clé + "=" + valeurs(0))
            Else
                ' ¿Varios valores?
                For ivalue As Integer = 0 To valeurs.Length - 1
                    infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
                Next
            End If
        Next
        ' se devuelve el resultado
        Return infos
    End Function
End Class

Los puntos importantes de este código son los siguientes:

  • en [Form_Load], la página recupera las dos colecciones [QueryString] y [Form]. Utiliza una función [getValeurs] para colocar el contenido de estas dos colecciones en dos objetos de tipo [ArrayList] que contendrán cadenas de caracteres del tipo [clé=valeur] si la clave de la colección está asociada a un solo valor, o [clé(i)=valeur] si la clave está asociada a varios valores.
  • Cada uno de los objetos [ArrayList] se vincula luego a uno de los objetos [ListBox] de la página de presentación mediante dos instrucciones:
    • [ListBox.DataSource=ArrayList] y [ListBox.DataBind]. Esta última instrucción transfiere los elementos de [DataSource] a la colección [Items] del objeto [ListBox]

Cabe señalar que ninguno de los dos objetos [ListBox] se crea explícitamente mediante una operación [New]. De ello se deduce que, cuando aparece la etiqueta <asp:listbox id="xx">...<asp:listbox/>, el servidor web crea por sí mismo el objeto [ListBox] al que hace referencia el atributo [id] de la etiqueta.

  • La función [getValeurs] utiliza el objeto de tipo [NameValueCollection] que se le pasa como parámetro para generar un resultado de tipo [ArrayList].

Colocamos los dos archivos anteriores en una carpeta <ruta-de-la-aplicación> e iniciamos el servidor Cassini con los parámetros (<ruta-de-la-aplicación>,/request2); luego solicitamos la URL

[http://localhost/request2/main.aspx?nom=tintin&age=27]. Obtenemos la siguiente respuesta:

Image

Ahora solicitamos una URL en la que la clave [nom] aparece dos veces:

Image

Observamos que el objeto [Request.QueryString("nombre")) era efectivamente un array. En este caso, las solicitudes se realizaron mediante un método GET. Utilizamos el cliente [curl] para realizar una consulta POST:

E:\curl>curl --data nom=milou --data nom=tintin --data age=14 --data age=27 http://localhost/request2/main.aspx

<HTML>
        <HEAD>
                <title>Requête client</title>
        </HEAD>
        <body>
                <P>Informations passées par le client :</P>
                <form name="_ctl0" method="post" action="main.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwtMTI3MjA1MzUzMTs7PtCDC7NG4riDYIB4YjyGFpVAAviD" />

                        <P>QueryString :</P>
                        <P><select name="lstQueryString" size="6" id="lstQueryString">

</select></P>
                        <P>Form :</P>
                        <P><select name="lstForm" size="2" id="lstForm">
        <option value="nom(0)=milou">nom(0)=milou</option>
        <option value="nom(1)=tintin">nom(1)=tintin</option>
        <option value="age(0)=14">age(0)=14</option>
        <option value="age(1)=27">age(1)=27</option>

</select></P>
                </form>
        </body>
</HTML>

Se puede observar que el cliente recibe correctamente el código HTML estándar para las dos listas de la página. Aparece información que no hemos ingresado nosotros mismos, como el campo oculto [_VIEWSTATE]. Esta información fue generada por las etiquetas <asp:xx runat="server>. Tendremos que aprender a manejarlas.

4.3. Implementación de una arquitectura MVC

4.3.1. El concepto

Terminemos este extenso capítulo con la implementación de una aplicación construida según el modelo MVC (Model-View-Controller). Una aplicación web diseñada según este modelo se ve así:

Image

  • el cliente envía sus solicitudes a una entidad específica de la aplicación llamada «controlador»
  • El controlador analiza la solicitud del cliente y la ejecuta. Para ello, cuenta con la ayuda de clases que agrupan la lógica de negocio de la aplicación y clases de acceso a datos.
  • Según el resultado de la ejecución de la solicitud, el controlador decide enviar una página determinada como respuesta al cliente

En nuestro modelo, todas las solicitudes pasan por un único controlador, que es el director de orquesta de toda la aplicación web. La ventaja de este modelo es que se puede agrupar en el controlador todo lo que se debe hacer antes de cada solicitud. Supongamos, por ejemplo, que la aplicación requiere una autenticación. Esta se realiza una sola vez. Una vez completada con éxito, la aplicación almacenará en la sesión la información relacionada con el usuario que acaba de autenticarse. Dado que un cliente puede acceder directamente a una página de la aplicación sin autenticarse, cada página deberá verificar en la sesión que la autenticación se haya realizado correctamente. Si todas las solicitudes pasan por un único controlador, es este quien puede encargarse de esa tarea. Las páginas a las que eventualmente se pase la solicitud no tendrán que hacerlo.

4.3.2. Controlar una aplicación MVC sin sesión

Por lo que hemos visto hasta ahora, podríamos pensar que el archivo [global.asax] podría desempeñar el papel de controlador. De hecho, sabemos que todas las solicitudes pasan por él. Por lo tanto, está bien posicionado para controlarlo todo. La siguiente aplicación lo utiliza con este fin. Su ruta virtual será [http://localhost/mvc1/main.aspx]. Para indicar lo que desea, el cliente agregará al final de la URL un parámetro action=valor. Según el valor del parámetro [action], el controlador [global.asax] dirigirá la solicitud a una página específica:

  1. [main.aspx] si el parámetro «action» no está definido o si «action=main»
  2. [action1.aspx] si action=action1
  3. [inconnu.aspx] si «action» no corresponde a los casos 1 ni 2

Las páginas [main.aspx, action1.aspx, inconnu.aspx] simplemente muestran el valor de [action] que provocó su visualización. A continuación enumeramos los ocho archivos de esta aplicación y los comentamos cuando es necesario:

[global.asax]

<%@ Application src="Global.asax.vb" Inherits="Global" %>

[global.asax.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' se recupera la acción a realizar
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' se coloca la acción en el contexto de la solicitud
        Context.Items("action") = action
        ' ejecutamos la acción
        Select Case action
            Case "main"
                Server.Transfer("main.aspx", True)
            Case "action1"
                Server.Transfer("action1.aspx", True)
            Case Else
                Server.Transfer("inconnu.aspx", True)
        End Select
    End Sub
End Class

Puntos a tener en cuenta:

  • interceptamos todas las solicitudes del cliente en el procedimiento [Application_BeginRequest], el cual se ejecuta automáticamente al inicio de cada nueva solicitud realizada a la aplicación.
  • En este procedimiento, tenemos acceso al objeto [Request], que es la representación de la solicitud HTTP del cliente. Como esperamos una URL con el formato [http://localhost/mvc1/main.aspx?action=xx], buscamos una clave [action] en la colección [Request.QueryString]. Si no se encuentra, establecemos por defecto la acción igual a «main».
  • El valor del parámetro [action] se coloca en el objeto [Context]. Al igual que los objetos [Application, Session, Request, Response, Server], este objeto es global y accesible desde cualquier código. Este objeto se pasa de una página a otra si la solicitud es procesada por varias páginas, como será el caso aquí. Se elimina tan pronto como se envía la respuesta al cliente. Por lo tanto, su vida útil es la misma que la del procesamiento de la solicitud.
  • Según el valor del parámetro [action], se pasa la solicitud a la página correspondiente. Para ello se utiliza el objeto global [Server], cuyo método permite transferir la solicitud actual a otra página. Su primer parámetro es el nombre de la página de destino; el segundo, un valor booleano que indica si se deben transferir o no a la página de destino las colecciones [QueryString] y [Form]. En este caso, la respuesta es sí.

Los archivos [main.aspx] y [main.aspx.vb]:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <head>
        <title>main</title></head>
    <body>
        <h3>Page [main]</h3>
        Action : <% =action %>
    </body>
</HTML>

Public Class main
    Inherits System.Web.UI.Page

    Protected action As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' se recupera la acción en curso
        action = Me.Context.Items("action").ToString
    End Sub
End Class

El controlador [main.aspx.vb] simplemente recupera el valor de la clave [action] del contexto; este valor es el que muestra el código de presentación. Aquí se busca mostrar el paso del objeto [Context] entre diferentes páginas que procesan una misma solicitud del cliente. Las páginas [action1.aspx] y [inconnu.aspx] funcionan de manera similar:

[action1.aspx]


<%@ Page src="action1.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="action1" %>
<HTML>
    <head>
        <title>action1</title></head>
    <body>
        <h3>Page [action1]</h3>
        Action : <% =action %>
    </body>
</HTML>

[action1.aspx.vb]

Public Class action1
    Inherits System.Web.UI.Page

    Protected action As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' se recupera la acción en curso
        action = Me.Context.Items("action").ToString
    End Sub
End Class

[inconnu.aspx]


<%@ Page src="inconnu.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="inconnu" %>
<HTML>
    <head>
        <title>inconnu</title></head>
    <body>
        <h3>Page [inconnu]</h3>
        Action : <% =action %>
    </body>
</HTML>

[inconnu.aspx.vb]

Public Class inconnu
    Inherits System.Web.UI.Page

    Protected action As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' se recupera la acción en curso
        action = Me.Context.Items("action").ToString
    End Sub
End Class

Para realizar la prueba, los documentos anteriores se colocan en una carpeta <ruta-de-la-aplicación> y se inicia Cassini con los parámetros (<ruta-de-la-aplicación>,/mvc1). Accedemos a la URL [http://localhost/mvc1/main.aspx]:

Image

La solicitud no envió ningún parámetro [action]. El código del controlador de la aplicación [global.asax.vb] generó la página [main.aspx]. Ahora solicitamos la URL [http://localhost/mvc1/main.aspx?action=action1]:

Image

El código del controlador de la aplicación [global.asax.vb] generó la página [action1.aspx]. Ahora solicitamos la URL [http://localhost/mvc1/main.aspx?action=xx]:

Image

La acción no se reconoció y el controlador [global.asax.vb] generó la página [inconnu.aspx].

4.3.3. Controlar una aplicación MVC con sesión

En la mayoría de los casos, las distintas solicitudes de un cliente para una aplicación deben compartir información. Hemos visto una posible solución a este problema: almacenar la información que se va a compartir en el objeto [Session] de la solicitud. De hecho, este objeto es compartido por todas las solicitudes y puede almacenar información en el formato (clave, valor), donde la clave es de tipo [String] y el valor es de cualquier tipo derivado de [Object].

En el ejemplo anterior, las diferentes páginas asociadas a las distintas acciones se invocaban en el procedimiento [Application_BeginRequest] del archivo [global.asax.vb]:


    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' se recupera la acción a realizar
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' se coloca la acción en el contexto de la solicitud
        Context.Items("action") = action
        ' se ejecuta la acción
        Select Case action
            Case "main"
                Server.Transfer("main.aspx", True)
            Case "action1"
                Server.Transfer("action1.aspx", True)
            Case Else
                Server.Transfer("inconnu.aspx", True)
        End Select
    End Sub

Resulta que en el procedimiento [Application_BeginRequest] no se puede acceder al objeto [Session]. Lo mismo ocurre en la página a la que se transfiere la ejecución. Por lo tanto, este modelo no se puede utilizar para una aplicación con sesión. Podemos asignar la función de controlador a cualquier página, por ejemplo, [default.aspx]. Los archivos [global.asax, global.asax.vb] desaparecen entonces y son reemplazados por los archivos [default.aspx, default.aspx.vb]:

[default.aspx]

<%@ Page codebehind="default.aspx.vb" Inherits="vs.controleur" %>

[default.aspx.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class controleur
    Inherits System.Web.UI.Page

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' se recupera la acción a realizar
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If

        ' se coloca la acción en el contexto de la solicitud
        Context.Items("action") = action
        ' se recupera la acción anterior, si existe
        Context.Items("actionPrec") = Session.Item("actionPrec")
        If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
        ' se guarda la acción actual en la sesión
        Session.Item("actionPrec") = action

        ' se ejecuta la acción
        Select Case action
            Case "main"
                Server.Transfer("main.aspx", True)
            Case "action1"
                Server.Transfer("action1.aspx", True)
            Case Else
                Server.Transfer("inconnu.aspx", True)
        End Select
    End Sub
End Class

Para destacar el mecanismo de sesión, las diferentes páginas mostrarán, además de la acción actual, la acción anterior. Para una secuencia de acciones A1, A2, ..., An, cuando ocurre la acción Ai, el controlador anterior:

  • coloca la acción actual Ai en el contexto
  • busca en la sesión la acción Ai-1 que la precedió. En caso de que no exista (como en la acción A1), la cadena vacía corresponde a la acción anterior.
  • coloca la acción actual Ai en la sesión en sustitución de Ai-1
  • transfiere la ejecución a la página correspondiente

Las tres páginas de la aplicación son las siguientes:

[main.aspx]


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <HEAD>
        <title>main</title>
    </HEAD>
    <body>
        <h3>Page [main]</h3>
        Action courante :
        <% =action %>
        <br>
        Action précédente :
        <% =actionPrec %>
    </body>
</HTML>

[action1.aspx]


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <head>
        <title>action1</title></head>
    <body>
        <h3>Page [action1]</h3>
        Action courante :
        <% =action %>
        <br>
        Action précédente :
        <% =actionPrec %>
    </body>
</HTML>

[inconnu.aspx]


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <head>
        <title>inconnu</title>
    </head>
    <body>
        <h3>Page [inconnu]</h3>
        Action courante :
        <% =action %>
        <br>
        Action précédente :
        <% =actionPrec %>
    </body>
</HTML>

Dado que las tres páginas muestran la misma información [action, actionPrec], las tres pueden tener el mismo controlador de página. Por lo tanto, las hemos derivado todas de la clase [main] del archivo [main.aspx.vb]:


Public Class main
    Inherits System.Web.UI.Page

    Protected action As String
    Protected actionPrec As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' se recupera la acción en curso
        action = Me.Context.Items("action").ToString
        ' y la acción anterior
        actionPrec = Me.Context.Items("actionPrec").ToString
    End Sub
End Class

El código anterior simplemente recupera la información que el controlador de la aplicación [default.aspx.vb] ha colocado en el contexto.

Todos estos archivos se colocan en <ruta-de-la-aplicación> y Cassini se inicia con los parámetros (<ruta-de-la-aplicación>,/mvc2). Primero se solicita la URL [http://localhost/mvc2]:

Image

La URL [http://localhost/mvc2] hace referencia a una carpeta. Sabemos que, en este caso, el servidor devuelve el documento [default.aspx] de esa carpeta, si es que existe. En este caso, no se especificó ninguna acción. Por lo tanto, se ejecutó la acción [main]. Pasemos a la acción [action1]:

Image

La acción actual y la anterior se identificaron correctamente. Pasemos a la acción [xx]:

Image

4.4. Conclusion

Ahora contamos con los elementos básicos a partir de los cuales se construye cualquier aplicación ASP.ET. Sin embargo, aún nos queda por introducir un concepto importante: el de formulario. Ese es el tema del siguiente capítulo.