Skip to content

4. Os fundamentos do desenvolvimento ASP.NET

4.1. O conceito de aplicação web ASP.NET

4.1.1. Introdução

Uma aplicação web é uma aplicação que reúne vários documentos (HTML, código .NET, imagens, sons, etc.). Estes documentos devem estar numa mesma raiz, a que se chama raiz da aplicação web. A esta raiz está associado um caminho virtual do servidor web. Já abordámos o conceito de pasta virtual no servidor web Cassini. Este conceito também existe no servidor web IIS. Uma diferença importante entre os dois servidores é que, num determinado momento, o IIS pode ter um número qualquer de pastas virtuais, enquanto o servidor web Cassini tem apenas uma, aquela que foi especificada no seu arranque. Isto significa que o servidor IIS pode servir várias aplicações web simultaneamente, enquanto o servidor Cassini serve apenas uma de cada vez. Nos exemplos anteriores, o servidor Cassini era sempre iniciado com os parâmetros (<webroot>,/aspnet) que associavam a pasta virtual /aspnet à pasta física <webroot>. O servidor web servia, portanto, sempre a mesma aplicação web. Isso não nos impediu de escrever e testar páginas diferentes e independentes dentro dessa única aplicação web. Cada aplicação web tem os seus próprios recursos, que se encontram na sua raiz física <webroot>:

  • uma pasta [bin], na qual é possível colocar classes pré-compiladas
  • um ficheiro [global.asax] que permite inicializar a aplicação web na sua totalidade, bem como o ambiente de execução de cada um dos seus utilizadores
  • um ficheiro [web.config] que permite configurar o funcionamento da aplicação
  • um ficheiro [default.aspx] que funciona como porta de entrada da aplicação
  • ...

Assim que uma aplicação utiliza um destes três recursos, necessita de um caminho físico e virtual que lhe seja próprio. De facto, não há qualquer razão para que duas aplicações web diferentes sejam configuradas da mesma forma. Todos os nossos exemplos anteriores puderam ser colocados na mesma aplicação (<webroot>,/aspnet) porque não utilizavam nenhum dos recursos acima referidos.

Voltemos à arquitetura MVC recomendada no início deste capítulo para o desenvolvimento de uma aplicação web:

Image

A aplicação web é composta por ficheiros de classe (controlador, classes de negócio, classes de acesso aos dados) e por ficheiros de apresentação (documentos HTML, imagens, sons, folhas de estilo, etc.). O conjunto destes ficheiros será colocado numa mesma raiz a que, por vezes, nos referiremos como <application-path>. Esta raiz será associada a um caminho virtual <application-vpath>. A associação entre este caminho virtual e o caminho físico é feita através da configuração do servidor web. Vimos que, no caso do servidor Cassini, esta associação é feita no momento do arranque do servidor. Por exemplo, numa janela do DOS, o Cassini seria iniciado com o comando:

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

Na pasta <application-path>, encontraremos, consoante as nossas necessidades:

  • a pasta [bin], para colocar classes pré-compiladas (DLL)
  • o ficheiro [global.asax], quando for necessário efetuar inicializações, quer durante a inicialização da aplicação, quer durante a inicialização de uma sessão de utilizador
  • o ficheiro [web.config], quando for necessário configurar a aplicação
  • o ficheiro [default.aspx] quando precisarmos de uma página predefinida na aplicação

Para respeitar este conceito de aplicação web, os exemplos que se seguem serão todos colocados numa pasta <application-path> específica da aplicação, à qual será associada uma pasta virtual <application-vpath>, sendo o servidor Cassini iniciado de forma a ligar estes dois parâmetros.

4.1.2. Configurar uma aplicação web

Se <application-path> for a raiz de uma aplicação ASP.NET, pode utilizar-se o ficheiro <application-path>\web.config para a configurar. Este ficheiro está no formato XML. Eis um exemplo:

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

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

É importante ter em conta que as balizas XML distinguem maiúsculas de minúsculas. Todas as informações de configuração devem estar entre as balizas <configuration> e </configuration>. Existem várias secções de configuração que podem ser utilizadas. Apresentamos aqui apenas uma delas, a secção <appSettings>, que permite inicializar dados com a baliza <add>. A sintaxe desta baliza é a seguinte:

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

Quando o servidor Web inicia uma aplicação, verifica se em <application-path> existe um ficheiro chamado web.config. Se sim, lê-o e armazena as suas informações num objeto do tipo [ConfigurationSettings], que estará disponível em todas as páginas da aplicação enquanto esta estiver ativa. A classe [ConfigurationSettings] possui um método estático [AppSettings]:

Image

Para obter o valor de uma chave C do ficheiro de configuração, escreve-se ConfigurationSettings.AppSettings("C"). Obtém-se uma cadeia de caracteres. Para utilizar o ficheiro de configuração anterior, criemos uma página [default.aspx]. O código VB do ficheiro [default.aspx.vb] será o seguinte:


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
        'recuperam-se as informações de configuração
        nom = ConfigurationSettings.AppSettings("nom")
        age = ConfigurationSettings.AppSettings("age")
    End Sub

End Class

Vê-se que, ao carregar a página, os valores dos parâmetros de configuração [nom] e [age] são recuperados. Estes serão apresentados pelo código de apresentação 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 o teste, colocamos os ficheiros [web.config], [default.aspx] e [default.aspx.vb] na mesma pasta:

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

Seja <application-path> a pasta onde se encontram os três ficheiros da aplicação. O servidor Cassini é iniciado com os parâmetros (<application-path>,/aspnet/config1). Solicitamos os ficheiros URL e [http://localhost/aspnet/config1]. Como [config1] é uma pasta, o servidor web irá procurar um ficheiro [default.aspx] dentro dela e exibi-lo se o encontrar. Neste caso, irá encontrá-lo:

Image

4.1.3. Aplicação, Sessão, Contexto

4.1.3.1. O ficheiro global.asax

O código do ficheiro [global.asax] é sempre executado antes de a página solicitada pela requisição atual ser carregada. Deve estar localizado na raiz <application-path> da aplicação. Se existir, o ficheiro [global.asax] é utilizado em vários momentos pelo servidor web:

  1. quando a aplicação web é iniciada ou encerrada
  2. quando uma sessão de utilizador é iniciada ou encerrada
  3. quando uma solicitação do utilizador é iniciada

Tal como acontece com as páginas .aspx, o ficheiro [global.asax] pode ser escrito de diferentes formas e, em particular, separando o código VB numa classe controladora e no código de apresentação. Esta é a escolha predefinida pela ferramenta Visual Studio e faremos aqui o mesmo. Normalmente, não há qualquer apresentação a fazer, uma vez que essa função cabe às páginas .aspx. O conteúdo do ficheiro [global.asax] fica, assim, reduzido a uma diretiva que faz referência ao ficheiro que contém o código do controlador:


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

Note-se que a diretiva já não é [Page], mas sim [Application]. O código do controlador [global.asax.vb] associado e gerado pela ferramenta Visual Studio é o seguinte:


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)
        ' É acionado quando a aplicação é iniciada
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando a sessão é iniciada
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado no início de cada pedido
    End Sub

    Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado durante uma tentativa de autenticação do utilizador
    End Sub

    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando ocorre um erro
    End Sub

    Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando a sessão termina
    End Sub

    Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando a aplicação é encerrada
    End Sub

End Class

Note-se que a classe do controlador deriva da classe [HttpApplication]. Ao longo do ciclo de vida de uma aplicação, ocorrem vários eventos importantes. Estes são geridos por procedimentos cuja estrutura básica é apresentada acima.

  • [Application_Start]: recorde-se que uma aplicação web está «encerrada» num caminho virtual. A aplicação inicia-se assim que uma página localizada nesse caminho virtual é solicitada por um cliente. O procedimento [Application_Start] é então executado. Esta será a única vez. Neste procedimento, realizar-se-á toda a inicialização necessária para a aplicação, como, por exemplo, a criação de objetos cuja duração corresponde à da aplicação.
  • [Application-End]: é executado quando a aplicação é encerrada. A cada aplicação está associado um período de inatividade, configurável em [web.config], ao fim do qual a aplicação é considerada encerrada. É, portanto, o servidor web que toma esta decisão com base na configuração da aplicação. O período de inatividade de uma aplicação é definido como o tempo durante o qual nenhum cliente efetuou um pedido de um recurso da aplicação.
  • [Session-Start]/[Session_End]: A cada cliente está associada uma sessão, a menos que a aplicação esteja configurada para não utilizar sessões. Um cliente não é um utilizador em frente ao seu ecrã. Se este tiver aberto dois navegadores para aceder à aplicação, representa dois clientes. Um cliente é identificado por um token de sessão que deve anexar a cada uma das suas solicitações. Este token de sessão é uma sequência de caracteres gerada aleatoriamente pelo servidor web e é única. Dois clientes não podem ter o mesmo token de sessão. Este token acompanhará o cliente da seguinte forma:
    • o cliente que efetua a sua primeira solicitação não envia um token de sessão. O servidor web reconhece este facto e atribui-lhe um. Este é o início da sessão e o procedimento [Session_Start] é executado. Esta será a única vez.
    • o cliente efetua as suas solicitações seguintes enviando o token que o identifica. Isto permitirá ao servidor web recuperar informações associadas a esse token. Isto permitirá o acompanhamento entre as diferentes solicitações do cliente.
    • A aplicação pode disponibilizar ao cliente um formulário para encerrar a sessão. Neste caso, é o próprio cliente que solicita o encerramento da sua sessão. O procedimento [Session_End] será executado. Esta será a única vez.
    • O cliente pode nunca solicitar ele próprio o encerramento da sua sessão. Neste caso, após um determinado período de inatividade da sessão, também configurável através do procedimento [web.config], a sessão será encerrada pelo servidor web. Será então executado o procedimento [Session_End].
  • [Application_BeginRequest]: este procedimento é executado assim que chega um novo pedido. É, portanto, executado em cada pedido de qualquer cliente. É um bom momento para analisar o pedido antes de o encaminhar para a página que foi solicitada. É até possível decidir redirecioná-lo para outra página.
  • [Application_Error]: é executado sempre que ocorre um erro não tratado explicitamente pelo código do controlador [global.asax.vb]. Aqui, é possível redirecionar o pedido do cliente para uma página que explique a causa do erro.

Se nenhum destes eventos tiver de ser tratado, então o ficheiro [global.asax] pode ser ignorado. Foi isso que se fez nos primeiros exemplos deste capítulo.

4.1.3.2. Exemplo 1

Vamos desenvolver uma aplicação para compreender melhor os três momentos que são: o arranque da aplicação, da sessão e de um pedido do cliente. O ficheiro [global.asax] será o seguinte:

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

O ficheiro [global.asax.vb] associado será o seguinte:


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)
        ' É acionado quando a aplicação é iniciada
        ' regista-se a hora
        Dim startApplication As String = Date.Now.ToString("T")
        ' É guardada no contexto da aplicação
        Application.Item("startApplication") = startApplication
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando a sessão é iniciada
        ' regista-se a hora
        Dim startSession As String = Date.Now.ToString("T")
        ' coloca-se na sessão
        Session.Item("startSession") = startSession
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' regista-se a hora
        Dim startRequest As String = Date.Now.ToString("T")
        ' Insere-se na sessão
        Context.Items("startRequest") = startRequest
    End Sub
End Class

Os pontos importantes do código são os seguintes:

  • o servidor web disponibiliza à classe [HttpApplication], a partir de [global.asax.vb], um determinado número de objetos:
    • Aplicação do tipo [HttpApplicationState] — representa a aplicação web — dá acesso a um dicionário de objetos [Application.Item] acessível a todos os clientes da aplicação — permite a partilha de informações entre diferentes clientes — o acesso simultâneo de vários clientes a um mesmo dado em modo de leitura/escrita requer a sincronização dos clientes.
    • Sessão do tipo [HttpSessionState] — representa um cliente específico — dá acesso a um dicionário de objetos [Session.Item] acessível a todas as solicitações desse cliente — permitirá memorizar informações sobre um cliente que poderão ser recuperadas ao longo das suas solicitações.
    • Pedido do tipo [HttpRequest] — representa a solicitação HTTP atual do cliente
    • Resposta do tipo [HttpResponse] — representa a resposta HTTP que está a ser construída pelo servidor para o cliente
    • Servidor do tipo [HttpServerUtility] — oferece métodos utilitários, nomeadamente para redirecionar o pedido para uma página diferente daquela inicialmente prevista.
    • Contexto do tipo [HttpContext] — este objeto é recriado a cada nova solicitação, mas é partilhado por todas as páginas que participam no processamento da solicitação — permite transmitir informações de página para página durante o processamento de uma solicitação, graças ao seu dicionário «Items».
  • O procedimento [Application_Start] regista o início da aplicação numa variável armazenada num dicionário acessível ao nível da aplicação
  • o procedimento [Session_Start] regista o início da sessão numa variável armazenada num dicionário acessível ao nível da sessão
  • o procedimento [Application_BeginRequest] regista o início da consulta numa variável armazenada num dicionário acessível ao nível da consulta (c.a.d disponível durante todo o tempo do seu processamento, mas perdida no final do mesmo)

A página de destino será a seguinte 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 apresentação apresenta valores calculados pelo seu 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
         ' recuperam-se as informações da aplicação e da sessão
        jeton=Session.SessionId
        startApplication = Application.Item("startApplication").ToString
        startSession = Session.Item("startSession").ToString
        startRequest = Context.Items("startRequest").ToString
    End Sub

End Class

O controlador limita-se a recuperar as três informações colocadas, respetivamente, na aplicação, na sessão e no contexto por [global.asax.vb].

Testamos a aplicação da seguinte forma:

  1. os ficheiros são reunidos numa mesma pasta <application-path>

Image

  1. o servidor Cassini é iniciado com os parâmetros (<application-path>,/aspnet/globalasax1)
  2. um primeiro cliente solicita o URL [http://localhost/aspnet/globalasax1/main.aspx] e obtém o seguinte resultado:

Image

  1. o mesmo cliente faz uma nova solicitação (opção «Reload» do navegador):

Image

Pode-se verificar que apenas a hora da solicitação mudou. Isto demonstra duas coisas:

  • os procedimentos [Application_Start] e [Session_Start] de [global.asax] não foram executados na segunda solicitação.
  • os objetos [Application] e [Session], onde estavam armazenadas as horas de início da aplicação e da sessão, continuam disponíveis para a segunda solicitação.
  1. abrimos um segundo navegador para criar um segundo cliente e solicitamos novamente a mesma URL:

Image

Desta vez, verificamos que a hora da sessão mudou. O segundo navegador, apesar de estar na mesma máquina, foi considerado um segundo cliente e foi criada uma nova sessão para ele. É possível constatar que os dois clientes não têm o mesmo token de sessão. A hora de início da aplicação não mudou, o que significa que:

  • o procedimento [Application_Start] de [global.asax.vb] não foi executado
  • o objeto [Application], onde foi armazenada a hora de início da aplicação, está acessível ao segundo cliente. É, portanto, neste objeto que devem ser armazenadas as informações que os diferentes clientes da aplicação devem partilhar, servindo o objeto [Session] para armazenar informações que as consultas de um mesmo cliente devem partilhar.

4.1.3.3. Uma visão geral

Com o que aprendemos até agora, estamos em condições de elaborar um primeiro esquema explicativo do funcionamento de um servidor web e das aplicações web que este serve:

Image

O esquema anterior mostra-nos um servidor a servir duas aplicações, designadas por A e B, cada uma com dois clientes. Um servidor web é capaz de servir várias aplicações web em simultâneo. Estas são totalmente independentes umas das outras. Vamos centrar-nos na aplicação A. O processamento de uma solicitação do cliente-1A à aplicação A decorrerá da seguinte forma:

  • o cliente 1A solicita ao servidor web um recurso que pertence ao domínio da aplicação A. Isto significa que solicita um URL com o formato [http://machine:port/VA/ressource], em que VA é o caminho virtual da aplicação A.
  • Se o servidor web detetar que se trata da primeira solicitação de um recurso da aplicação A, aciona o evento [Application_Start] do ficheiro [global.asax] da aplicação A. Será criado um objeto [ApplicationA] do tipo [HttpApplicationState]. Os diferentes códigos da aplicação irão armazenar neste objeto dados com o âmbito [Application], c.a.d, relativos a todos os utilizadores. O objeto [ApplicationA] permanecerá ativo até que o servidor web encerre a aplicação A.
  • Se, além disso, o servidor web detetar que está a lidar com um novo cliente da aplicação A, irá desencadear o evento [Session_Start] do ficheiro [global.asax] da aplicação A. Será criado um objeto [Session-1A] do tipo [HttpSessionState]. Este objeto permitirá que a aplicação A armazene objetos de âmbito [Session] e c.a.d, pertencentes a um cliente específico. O objeto [Session-1A] existirá enquanto o cliente 1A efetuar pedidos. Permitirá o acompanhamento desse cliente. O servidor web deteta que está a lidar com um novo cliente em dois casos:
    • o cliente não lhe enviou um token de sessão nos cabeçalhos HTTP da sua solicitação
    • o cliente enviou-lhe um token de sessão que não existe (mau funcionamento do cliente ou tentativa de pirataria) ou que já não existe. Um token de sessão expira, de facto, após um determinado período de inatividade do cliente (20 minutos por predefinição com IIS). Este período é programável.
  • Em todos os casos, o servidor web irá desencadear o evento [Application_BeginRequest] do ficheiro [global.asax]. Este evento inicia o processamento de uma solicitação do cliente. É comum não processar este evento e passar o controlo para a página solicitada pelo cliente, que, por sua vez, processará a solicitação. Também é possível utilizar este evento para analisar a solicitação, processá-la e decidir qual a página que deve ser enviada como resposta. Utilizaremos esta técnica para implementar uma aplicação que respeite a arquitetura MVC de que falámos.
  • Depois de passar pelo filtro [global.asax], a solicitação do cliente é encaminhada para uma página .aspx que irá processá-la. Veremos mais adiante que é possível fazer a solicitação passar por um filtro composto por várias páginas. A última página ficará encarregada de enviar a resposta ao cliente. As páginas podem adicionar à solicitação inicial do cliente informações que tenham calculado. Podem armazenar essas informações na coleção Context.Items. Com efeito, todas as páginas envolvidas no processamento da solicitação de um cliente têm acesso a este repositório de dados.
  • O código das diferentes páginas tem acesso aos reservatórios de dados que são os objetos [ApplicationA], [Session-1A], ... É importante lembrar que o servidor web processa simultaneamente vários clientes para a aplicação A. Todos estes clientes têm acesso ao objeto [Application A]. Caso precisem de alterar dados neste objeto, é necessário efetuar uma sincronização entre os clientes. Além disso, cada cliente XA tem acesso ao repositório de dados [Session-XA]. Uma vez que este lhe está reservado, não é necessária qualquer sincronização neste caso.
  • O servidor web serve várias aplicações web em simultâneo. Não há qualquer interferência entre os clientes destas diferentes aplicações.

Destas explicações, retemos os seguintes pontos:

  • Num determinado momento, um servidor web atende a vários clientes simultaneamente. Isto significa que não aguarda o fim de um pedido para processar outro. Num instante T, existem, portanto, vários pedidos em curso de processamento, pertencentes a clientes diferentes e para aplicações diferentes. Chama-se por vezes «threads de execução» aos códigos de processamento que decorrem em simultâneo no interior do servidor web.
  • Os threads de execução dos clientes de diferentes aplicações web não interferem entre si. Existe isolamento.
  • Os threads de execução dos clientes de uma mesma aplicação podem ter de partilhar dados:
    • os threads de execução das solicitações de dois clientes diferentes (que não partilham o mesmo token de sessão) podem partilhar dados através do objeto [Application].
    • Os threads de execução de pedidos sucessivos de um mesmo cliente podem partilhar dados através do objeto [Session].
    • os threads de execução das páginas sucessivas que processam uma mesma solicitação de um determinado cliente podem partilhar dados através do objeto [Context].

4.1.3.4. Exemplo 2

Vamos desenvolver um novo exemplo que ilustre o que acabámos de ver. Reunimos na mesma pasta os seguintes ficheiros:

[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)
        ' É acionado quando a aplicação é iniciada
        ' inicializar o contador de clientes
        Application.Item("nbRequêtes") = 0
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando a sessão é iniciada
        ' inicializa o contador de pedidos
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

O princípio da aplicação consiste em contabilizar o número total de pedidos efetuados à aplicação e o número de pedidos por cliente. Quando a aplicação é iniciada ([Application_Start]), o contador de solicitações feitas à aplicação é zerado. Este contador é colocado no âmbito [Application], uma vez que deve ser incrementado por todos os clientes. Quando um cliente acede pela primeira vez a [Session_Start], o contador de solicitações feitas por esse cliente é zerado. Este contador é colocado no âmbito [Session], uma vez que diz respeito apenas a um determinado cliente.

Assim que o [global.asax] for executado, será executado o ficheiro seguinte, [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>

Apresenta três informações calculadas pelo seu controlador:

  1. a identidade do cliente através do seu token de sessão: [jeton]
  2. o número total de pedidos enviados à aplicação: [nbRequêtesApplication]
  3. o número total de pedidos efetuados pelo cliente identificado em 1: [nbRequêtesClient]

As três informações são calculadas em [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
         ' Mais uma solicitação para a aplicação
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
         ' mais uma solicitação na sessão
        Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
         ' inicialização das variáveis de apresentação
        nbRequêtesApplication = Application.Item("nbRequêtes").ToString
        jeton = Session.SessionID
        nbRequêtesClient = Session.Item("nbRequêtes").ToString
    End Sub
End Class

Quando o [main.aspx.vb] é executado, estamos a processar um pedido de um determinado cliente. Utilizamos o objeto [Application] para incrementar o número de pedidos da aplicação e o objeto [Session] para incrementar o número de pedidos do cliente cujo pedido está a ser processado. Recorde-se que, embora todos os clientes de uma mesma aplicação partilhem o mesmo objeto [Application], cada um deles possui um objeto [Session] que lhe é próprio.

Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/webapplia). Abrimos um primeiro navegador e acedemos à URL [http://localhost/aspnet/webapplia/main.aspx]:

Image

Fazemos uma segunda solicitação com o botão [Reload]:

Image

Abrimos um segundo navegador para aceder à mesma URL. Para o servidor web, trata-se de um novo cliente:

Image

É possível verificar que o token de sessão mudou e que, por isso, temos um novo cliente. Isto reflete-se no número de pedidos do cliente. Voltemos agora ao primeiro navegador e solicitemos novamente a mesma URL:

Image

O número de pedidos feitos à aplicação é, de facto, contabilizado na totalidade.

4.1.3.5. Sobre a necessidade de sincronizar os clientes de uma aplicação

Na aplicação anterior, o contador de pedidos feitos à aplicação é incrementado no procedimento [Form_Load] da página [main.aspx] da seguinte forma:

         ' mais uma solicitação para a aplicação
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1

Esta instrução, embora simples, requer várias instruções do processador para ser executada. Suponhamos que sejam necessárias três:

  1. leitura do contador
  2. incremento do contador
  3. regravação do contador

O servidor web é executado numa máquina multitarefa, o que significa que cada tarefa tem acesso ao processador durante alguns milissegundos antes de o perder e de o recuperar depois de todas as outras tarefas terem também tido o seu tempo de execução. Suponhamos que dois clientes, A e B, enviam um pedido ao servidor web ao mesmo tempo. Admitamos que o cliente A seja atendido primeiro, que chegue à rotina [Form_Load] a partir de [main.aspx.vb], leia o contador (=100) e seja interrompido porque o seu tempo de execução se esgotou. Suponhamos agora que seja a vez do cliente B e que este tenha o mesmo destino: consegue ler o valor do contador (=100), mas não tem tempo para o incrementar. Os clientes A e B têm ambos um contador igual a 100. Suponhamos que volta a ser a vez do cliente A: este incrementa o seu contador, passa-o para 101 e, em seguida, termina. É a vez do cliente B, que tem na sua posse o valor antigo do contador e não o novo. Por isso, também ele passa o valor do contador para 101 e termina. O valor do contador de pedidos da aplicação está agora errado.

Para ilustrar este problema, retomamos a aplicação anterior e modificamo-la da seguinte forma:

  • os ficheiros [global.asax], [global.asax.vb] e [main.aspx] não sofrem alterações
  • o ficheiro [main.aspx.vb] passa a ter o seguinte conteúdo:

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
        ' mais uma solicitação para a aplicação e a sessão
        ' leitura 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 dos contadores
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' registo dos contadores
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' inicialização das variáveis de apresentação
        jeton = Session.SessionID
    End Sub
End Class

A atualização dos contadores foi dividida em quatro fases:

  1. leitura do contador
  2. suspensão do thread de execução
  3. incrementação do contador
  4. regravação do contador

Consideremos novamente os nossos dois clientes, A e B. Entre a fase de leitura e a de incremento dos contadores de pedidos, forçamos o thread de execução a parar durante 5 segundos. Isto terá como consequência imediata que ele perca o processador, que será então atribuído a outra tarefa. Suponhamos que o cliente A seja o primeiro a passar. Ele irá ler o valor N do contador e será interrompido durante 5 segundos. Se, durante esse período, o cliente B tiver o processador à sua disposição, deverá ler o mesmo valor N do contador. No final, os dois clientes deveriam apresentar o mesmo valor do contador, o que seria anormal.

Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/webapplib). Preparamos dois navegadores diferentes com a URL [http://localhost/aspnet/webapplib/main.aspx]. Iniciamos o primeiro para que solicite o URL e, sem esperar pela resposta que chegará 5 segundos mais tarde, iniciamos o segundo navegador. Passados pouco mais de 5 segundos, obtemos o seguinte resultado:

Image

Percebe-se:

  • que temos dois clientes diferentes (não têm o mesmo token de sessão)
  • que cada cliente efetuou uma solicitação
  • que o contador de pedidos feitos à aplicação deveria, portanto, estar em 2 num dos dois navegadores. Mas não é esse o caso.

Agora, vamos fazer outra experiência. Com o mesmo navegador, enviamos cinco pedidos para a URL [http://localhost/aspnet/webapplib/main.aspx]. Mais uma vez, enviamos os pedidos um após o outro, sem esperar pelos resultados. Quando todos os pedidos foram executados, obtemos o seguinte resultado para o último:

Image

É possível observar que:

  • que as 5 solicitações foram consideradas como provenientes do mesmo cliente, uma vez que o contador de solicitações do cliente está em 5. Embora não seja mostrado acima, verifica-se que o token de sessão é efetivamente o mesmo para as 5 solicitações.
  • que o contador de pedidos feitos à aplicação está correto.

Que conclusão se pode tirar? Nada definitivo. Talvez o servidor web não comece a executar uma solicitação de um cliente se este já tiver uma em execução? Assim, nunca haveria simultaneidade na execução das solicitações de um mesmo cliente. Seriam executadas uma a seguir à outra. Este ponto deve ser verificado. Pode, de facto, depender do tipo de cliente utilizado.

4.1.3.6. Sincronização dos clientes

O problema evidenciado na aplicação anterior é um problema clássico (mas não simples de resolver) de acesso exclusivo a um recurso. No nosso caso específico, é necessário garantir que dois clientes, A e B, não possam estar simultaneamente na sequência de código:

  1. leitura do contador
  2. incremento do contador
  3. reescrita do contador

A uma sequência de código como esta chama-se sequência crítica. Ela requer a sincronização dos threads que a executam simultaneamente. A plataforma .NET oferece várias ferramentas para garantir essa sincronização. Aqui, vamos utilizar a classe [Mutex].

Image

Aqui, utilizaremos apenas os seguintes construtores e métodos:

public Mutex()
cria um objeto de sincronização M
public bool WaitOne()
O thread T1, que executa a operação M.WaitOne(), solicita a posse do objeto de sincronização M. Se o mutex M não estiver na posse de nenhum thread (o que acontece inicialmente), este é «atribuído» ao thread T1 que o solicitou. Se, pouco tempo depois, um thread T2 realizar a mesma operação, ficará bloqueado. Com efeito, um mutex só pode pertencer a um único thread. Será desbloqueado quando o thread T1 libertar o mutex M que detém. Assim, vários threads podem ficar bloqueados à espera do mutex M.
public void ReleaseMutex()
O thread T1, que executa a operação M.ReleaseMutex(), abdica da posse do mutex M. Quando o thread T1 perder o processador, o sistema poderá atribuí-lo a um dos threads que aguardam o mutex M. Apenas um deles o obterá por sua vez, ficando os outros que aguardam o M bloqueados

Um mutex M gere o acesso a um recurso partilhado R. Uma thread solicita o recurso R através de M.WaitOne() e devolve-o através de M.ReleaseMutex(). Uma secção crítica de código que só deve ser executada por uma única thread de cada vez é um recurso partilhado. A sincronização da execução da secção crítica pode ser feita da seguinte forma:

M.WaitOne()
' apenas este thread entra aqui
' secção crítica
....
M.ReleaseMutex()

onde M é um objeto Mutex. É claro que nunca se deve esquecer de libertar um Mutex que já não seja necessário, para que outro thread possa, por sua vez, entrar na secção crítica; caso contrário, os threads que aguardam um mutex que nunca foi libertado nunca terão acesso ao processador. Além disso, é necessário evitar a situação de interbloqueio (deadlock) em que duas threads esperam uma pela outra. Consideremos as seguintes ações que se sucedem no tempo:

  • um thread T1 obtém a posse de um mutex M1 para aceder a um recurso partilhado R1
  • um thread T2 obtém a posse de um mutex M2 para aceder a um recurso partilhado R2
  • o thread T1 solicita o mutex M2. Fica bloqueado.
  • O thread T2 solicita o mutex M1. Fica bloqueado.

Neste caso, os threads T1 e T2 estão à espera um do outro. Esta situação ocorre quando os threads necessitam de dois recursos partilhados: o recurso R1, controlado pelo mutex M1, e o recurso R2, controlado pelo mutex M2. Uma solução possível consiste em solicitar ambas as recursos simultaneamente através de um único mutex M. No entanto, isso nem sempre é possível, especialmente se implicar uma ocupação prolongada de um recurso dispendioso. Outra solução consiste em que um thread que possua M1 e não consiga obter M2 liberte então M1 para evitar o interbloqueio.

Se pusermos em prática o que acabámos de aprender, a nossa aplicação fica da seguinte forma:

  • os ficheiros [global.asax] e [main.aspx] não se alteram
  • o ficheiro [global.asax.vb] passa a ser o seguinte:

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)
        ' É acionado quando a aplicação é iniciada
        ' inicialização do contador de clientes
        Application.Item("nbRequêtes") = 0
        ' criação de um bloqueio de sincronização
        Application.Item("verrou") = New Mutex
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' É acionado quando a sessão é iniciada
        ' inicialização do contador de pedidos
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

A única novidade é a criação de um [Mutex], que será utilizado pelos clientes para se sincronizarem. Como tem de estar acessível a todos os clientes, é colocado no objeto [Application].

  • O ficheiro [main.aspx.vb] passa a ter o seguinte conteúdo:

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
        ' mais uma solicitação para a aplicação e a sessão
        ' entra-se numa secção crítica — recupera-se o bloqueio de sincronização
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' solicita-se a entrada exclusiva na secção crítica seguinte
        verrou.WaitOne()
        ' leitura dos 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 dos contadores
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' registo dos contadores
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' permite-se o acesso à secção crítica
        verrou.ReleaseMutex()
        ' inicialização das variáveis de apresentação
        jeton = Session.SessionID
    End Sub
End Class

Vê-se que o cliente:

  • solicita entrar sozinho na secção crítica. Para tal, solicita a posse exclusiva do mutex [verrou]
  • libera o mutex [verrou] no final da secção crítica, para que outro cliente possa, por sua vez, entrar na secção crítica.

Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/webapplic). Preparamos dois navegadores diferentes com a URL [http://localhost/aspnet/webapplic/main.aspx]. Iniciamos o primeiro para que solicite o URL e, sem esperar pela resposta que chegará 5 segundos mais tarde, iniciamos o segundo navegador. Passados pouco mais de 5 segundos, obtemos o seguinte resultado:

Image

Desta vez, o contador de pedidos da aplicação está correto.

O que se retira desta longa demonstração é a necessidade absoluta de sincronizar os clientes de uma mesma aplicação web, caso estes tenham de atualizar elementos partilhados por todos os clientes.

4.1.3.7. Gestão do token de sessão

Já falámos várias vezes do token de sessão que o cliente e o servidor web trocam entre si. Recorde-se o seu princípio:

  • o cliente faz uma primeira solicitação ao servidor. Não envia nenhum token de sessão.
  • Devido à ausência do token de sessão na solicitação, o servidor reconhece um novo cliente e atribui-lhe um token. A este token está também associado um objeto [Session], que será utilizado para armazenar informações específicas desse cliente. O token acompanhará todas as solicitações desse cliente. Será incluído nos cabeçalhos HTTP da resposta à primeira solicitação do cliente.
  • O cliente conhece agora o seu token de sessão. Irá reenviá-lo nos cabeçalhos HTTP de cada uma das solicitações seguintes que enviar ao servidor web. Graças ao token, o servidor poderá recuperar o objeto [Session] associado ao cliente.

Para ilustrar este mecanismo, retomamos a aplicação anterior, alterando apenas o ficheiro [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
        ' mais uma solicitação para a aplicação e a sessão
        ' entra-se numa secção crítica — recupera-se o bloqueio de sincronização
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' solicita-se o acesso exclusivo à secção seguinte
        verrou.WaitOne()
        ' leitura 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 dos contadores
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' registo dos contadores
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' permite-se o acesso à secção crítica
        verrou.ReleaseMutex()
        ' inicialização das variáveis de apresentação
        jeton = Session.SessionID
    End Sub

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        ' armazenar o pedido do cliente em request.txt na pasta da aplicação
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub
End Class

Quando ocorre o evento [Page_Init], guardamos o pedido do cliente na pasta da aplicação. Recorde-se alguns pontos:

  • [TemplateSourceDirectory] representa o caminho virtual da página em execução,
  • MapPath (TemplateSourceDirectory) representa o caminho físico correspondente. Isto permite-nos construir o caminho físico do ficheiro a criar,
  • [Request] é um objeto que representa a solicitação atualmente em processamento. Este objeto foi construído a partir da solicitação bruta enviada pelo cliente, c.a.d, uma sequência de linhas de texto com o seguinte formato:

Image

  • Request.Save([FileName]) guarda a totalidade da solicitação do cliente (cabeçalhos HTTP e, eventualmente, o documento que se segue) num ficheiro cujo caminho é passado como parâmetro.

Assim, poderemos saber exatamente qual foi a solicitação do cliente. Testamos a aplicação colocando os quatro ficheiros anteriores numa pasta a que chamamos <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/aspnet/session1). Em seguida, utilizando um navegador, solicitamos o URL

[http://localhost/aspnet/session1/main.aspx]. Obtemos o seguinte resultado:

Image

Utilizamos o ficheiro [request.txt] guardado por [main.aspx.vb] para aceder ao pedido do 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

Constatamos que o navegador efetuou o pedido de URL [/aspnet/session1/main.aspx], tendo enviado outras informações de que já falámos no capítulo anterior. Não se observa aqui nenhum token de sessão. A página recebida em resposta mostra, por sua vez, que o servidor criou um token de sessão. Ainda não sabemos se o navegador o recebeu. Vamos agora efetuar uma segunda solicitação com o mesmo navegador (Atualizar). Obtemos a seguinte nova resposta:

Image

Existe, de facto, um acompanhamento da sessão, uma vez que o número de pedidos da sessão foi corretamente incrementado. Vejamos agora o conteúdo do ficheiro [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

Verifica-se que, para esta segunda solicitação, o navegador enviou ao servidor um novo cabeçalho HTTP [Cookie:] que define uma informação denominada [ASP.NET_SessionId] e cujo valor é o token de sessão que apareceu na resposta à primeira solicitação. Graças a este token, o servidor web irá associar esta nova solicitação ao objeto [Session], identificado pelo token [y153tk45sise0lrhdzrf22m3], e recuperar o contador de solicitações associado.

Ainda não sabemos através de que mecanismo o servidor enviou o token ao cliente, uma vez que não temos acesso à resposta HTTP do servidor. Recorde-se que esta tem a mesma estrutura que o pedido do cliente, ou seja, um conjunto de linhas de texto com o seguinte formato:

Image

Tivemos a oportunidade de utilizar um cliente web que nos dava acesso à resposta HTTP do servidor web, o cliente curl. Utilizamo-lo novamente, numa janela do DOS, para consultar a mesma URL que o 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>

Já temos a resposta à nossa pergunta. O servidor web envia o token de sessão sob a forma de um cabeçalho HTTP [Set-Cookie:]:

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

Vamos fazer a mesma solicitação sem reenviar o token de sessão. Obtemos a seguinte resposta:

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 não reenviámos o token de sessão, o servidor não conseguiu identificar-nos e atribuiu-nos um novo token. Para dar continuidade a uma sessão já iniciada, o cliente deve reenviar ao servidor o token de sessão que recebeu. Vamos fazê-lo aqui utilizando a opção [--cookie clé=valeur] do curl, que irá gerar o cabeçalho HTTP [Cookie: clé=valeur]. Vimos que o navegador enviou este cabeçalho HTTP na sua segunda solicitação.

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>

É possível observar vários aspetos:

  • o contador de pedidos do cliente foi efetivamente incrementado, o que demonstra que o servidor reconheceu o nosso token.
  • o token de sessão apresentado pela página é, de facto, aquele que enviámos
  • o token de sessão já não consta dos cabeçalhos HTTP enviados pelo servidor web. Com efeito, este apenas o envia uma vez: durante a geração do token no início de uma nova sessão. Assim que o cliente obtém o seu token, cabe-lhe a ele utilizá-lo quando quiser para ser reconhecido.

Nada impede um cliente de utilizar vários tokens de sessão, como mostra o exemplo seguinte com [curl], em que utilizamos o token obtido na nossa primeira solicitação (solicitação 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>

O que significa este exemplo? Enviámos um token obtido pouco antes. Quando o servidor web cria um token, este é mantido enquanto o cliente associado a esse token continuar a enviar-lhe pedidos. Após um certo período de inatividade (20 minutos por predefinição com IIS), o token é eliminado. O exemplo anterior mostra que utilizámos um token ainda ativo.

Podemos ficar curiosos em saber quais foram as solicitações HTTP do cliente [curl] durante todas estas operações. Sabemos que foram registadas no ficheiro [request.txt]. Aqui está a ú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

É possível ver aqui o cabeçalho HTTP que envia o token de sessão.

As informações transmitidas pelo servidor através dos cabeçalhos HTTP e [Set-Cookie:] são denominadas cookies. O servidor pode utilizar este mecanismo para transmitir outras informações além do token de sessão. Quando o servidor S transmite um cookie a um cliente, indica também o tempo de vida D do mesmo e o valor associado URL U. Isto significa que, para o cliente, quando solicita ao servidor S um URL do tipo /U/caminho, pode reenviar o cookie se não o tiver recebido há um período superior a D. Nada impede um cliente de não respeitar este código de conduta. Os navegadores, por sua vez, respeitam-no. Alguns navegadores permitem aceder ao conteúdo dos cookies que recebem. É o caso do navegador Mozilla. Aqui estão, por exemplo, as informações relacionadas com o cookie enviado pelo servidor num exemplo anterior:

Image

Nele encontram-se:

  • o nome do cookie [ASP.NET_SessionId]
  • o seu valor: [y153...m3]
  • o dispositivo ao qual está associado: [localhost]
  • o URL ao qual está associado: [/]
  • a sua duração: [at end of session]

O navegador enviará, portanto, o token de sessão sempre que solicitar um URL na forma [http://localhost/...], c.a.d. sempre que solicitar uma URL ao servidor web da máquina [localhost]. A duração do cookie corresponde à da sessão. Para o navegador, isto significa que o cookie nunca expira. O navegador enviá-lo-á sempre que solicitar uma URL da máquina [localhost]. Assim, se o navegador receber o token de sessão no dia D, for fechado e voltar a ser utilizado no dia seguinte, reenviará o token de sessão (que foi guardado num ficheiro). O servidor receberá esse token que já não possui, uma vez que um token de sessão tem uma duração limitada no servidor (20 minutos no IIS). Por isso, iniciará uma nova sessão.

É possível desativar a utilização de cookies num navegador. Nesse caso, o cliente recebe o token de sessão, mas não o reenvia, o que impede o acompanhamento da sessão. Para demonstrar isso, desativamos a utilização de cookies no nosso navegador (Mozilla, neste caso):

Image

Além disso, eliminamos todos os cookies existentes:

Image

Feito isto, reiniciamos o servidor Cassini para recomeçar do zero e, com o navegador, solicitamos novamente a URL [http://localhost/aspnet/session1/main.aspx]:

Image

Vamos verificar se o nosso navegador armazenou um cookie:

Image

Constatamos que o navegador não armazenou o cookie do token de sessão que o servidor lhe enviou. Podemos, portanto, esperar que não haja acompanhamento da sessão. Solicitamos novamente a mesma URL (Atualizar):

Image

O resultado é exatamente o esperado. O navegador não reenviou o token de sessão, que, no entanto, tinha recebido mas não armazenado. O servidor iniciou, portanto, uma nova sessão com um novo token. Retemos deste exemplo que a nossa política de acompanhamento de sessões fica comprometida se o utilizador tiver desativado a utilização de cookies no seu navegador. Existe, no entanto, outra forma, além dos cookies, de trocar o token de sessão entre o servidor e o cliente. É, de facto, possível indicar ao servidor web que a aplicação funciona sem cookies. Isto é feito através do ficheiro de configuração [web.config]:


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

O ficheiro de configuração acima indica que a aplicação irá funcionar sem cookies (cookieless="true") e que o tempo máximo de inatividade de um token de sessão é de 10 minutos (timeout="10"). Após este período, a sessão associada ao token é eliminada. O processo de troca do token de sessão entre o servidor e o cliente será o seguinte:

  1. o cliente solicita o URL [http://machine:port/V/chemin], em que V é uma pasta virtual do servidor web
  2. o servidor gera um token J e responde ao cliente para que este seja redirecionado para a URL [http://machine:port/V/(J)/chemin]. Assim, inseriu o token na URL a ser consultada, imediatamente a seguir à pasta virtual V
  3. o cliente obedece a esta redireção e solicita a nova URL URL [http://machine:port/V/(J)/chemin].
  4. O servidor responde a este pedido e envia uma página de resposta.

Vamos ilustrar estes diferentes pontos. Colocamos toda a aplicação anterior numa nova pasta <application-path>. Colocamos nessa mesma pasta o ficheiro [web.config] anterior. Além disso, alteramos o código de apresentação [main.aspx] para incluir um link:


<%@ 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 link aponta para a página [main.aspx] e é, portanto, equivalente ao botão (Reload) do navegador. O servidor Cassini é iniciado com os parâmetros (<application-path>,/session2). Estamos a desviar-nos da nossa prática habitual, que consistia em registar a pasta virtual [/aspnet/XX]. Com efeito, devido à inserção do token de sessão no URL, a pasta virtual deve conter apenas um elemento: /XX. Começamos por utilizar o cliente [curl] para solicitar o 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>

Verificamos que o servidor responde com o cabeçalho HTTP [HTTP/1.1 302 Found] em vez de [HTTP/1.1 200 OK]. Trata-se de um cabeçalho que solicita ao cliente que se redirecione para o URL indicado pelo cabeçalho HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. É possível ver o token de sessão que foi inserido na URL de redirecionamento. Um navegador que receba esta resposta solicita a nova URL de forma transparente para o utilizador, que não vê a nova solicitação. Caso o navegador não consiga gerir a redireção por si próprio, é enviado um documento HTML a seguir ao código HTTP acima referido. Nele encontra-se um link para a URL de redireção, no qual o utilizador poderá clicar.

Agora, vamos fazer o mesmo com um navegador em que os cookies foram desativados. Solicitamos, mais uma vez, a URL [http://localhost/session2/main.aspx]. Obtemos a seguinte resposta do servidor:

Image

Em primeiro lugar, constatamos que a URL apresentada pelo navegador não é a que solicitámos. Isso indica que ocorreu um redirecionamento. De facto, o navegador exibe sempre a URL URL do último documento recebido. Portanto, se não exibir a URL [http://localhost/session2/main.aspx], é porque lhe foi solicitado que redirecionasse para outra URL. Podem ocorrer vários redirecionamentos. A URL apresentada pelo navegador é a URL do último redirecionamento. Podemos constatar que o token de sessão está presente na URL apresentada pelo navegador. É possível vê-lo porque esse token também é apresentado pelo nosso programa na página.

Recordemos o código do link que foi colocado na página:


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

Trata-se de um link relativo, uma vez que não começa com o sinal /, o que o tornaria um link absoluto. Relativo a quê? Para compreender este ponto, é necessário voltar à URL do documento atualmente exibido: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Os links relativos que forem encontrados neste documento serão relativos ao caminho [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Assim, o nosso link acima é equivalente ao link:


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

É isso que o navegador nos mostra quando passamos o rato sobre o link:

Image

Se clicarmos no link [Recharger l'application], é então a URL

[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx] que é chamada. O servidor irá, assim, receber o token de sessão e poderá recuperar as informações que lhe estão associadas. É isso que a resposta do navegador nos mostra:

Image

Concluímos que, se precisarmos de acompanhar uma sessão numa aplicação web e não tivermos a certeza de que os navegadores dos clientes dessa aplicação irão autorizar a utilização de cookies, então

  • devemos configurar a aplicação para que funcione sem cookies
  • as páginas da aplicação devem incluir links relativos e não absolutos

4.2. Recuperar as informações de um pedido do cliente

4.2.1. O ciclo de pedido-resposta do cliente-servidor web

Recordemos aqui o contexto cliente-servidor de uma aplicação web:

Image

A solicitação de um cliente para uma aplicação web é processada da seguinte forma:

  1. o cliente abre uma ligação TCP/IP para uma porta P do serviço web da máquina M que aloja a aplicação web
  2. envia, através dessa ligação, uma sequência de linhas de texto de acordo com o protocolo HTTP. Este conjunto de linhas constitui o que se denomina «pedido do cliente». Tem o seguinte formato:

Image

Depois de enviada a solicitação, o cliente aguarda a resposta.

  1. A primeira linha dos cabeçalhos HTTP especifica a ação solicitada ao servidor web. Pode assumir várias formas:
    1. GET url HTTP/<versão>, sendo que <versão> é atualmente igual a 1.0 ou 1.1. Neste caso, a solicitação não inclui a parte [Document]
    2. POST url HTTP/<versão>. Neste caso, a solicitação inclui uma parte [Document], na maioria das vezes uma lista de informações destinadas à aplicação web
    3. PUT url HTTP/<versão>. O cliente envia um documento na parte [Document] e pretende armazená-lo no servidor na morada url

Quando o cliente pretende transmitir informações à aplicação web à qual se ligou, dispõe principalmente de dois meios:

  • (continuação)
    1. a sua solicitação é [GET url_enrichie HTTP/<version>], em que url_enrichie tem o formato [url?param1=val1&param2=val2&...]. O cliente transmite, além do URL, uma série de informações no formato [clé=valeur].
    2. A sua solicitação é [POST url HTTP/<version>]. Na parte [Document], transmite informações no mesmo formato que anteriormente: [param1=val1&param2=val2&...].
  1. No servidor, toda a cadeia de processamento da solicitação do cliente tem acesso a esta através de um objeto global denominado Request. O servidor web colocou neste objeto a totalidade da solicitação do cliente num formato que iremos descobrir. A aplicação solicitada irá processar este objeto e construir uma resposta para o cliente. Esta resposta está disponível num objeto global denominado Response. O papel da aplicação web é construir um objeto [Response] a partir do objeto [Request] recebido. A cadeia de processamento dispõe também dos objetos globais [Application] e [Session], dos quais já falámos e que lhe permitirão partilhar dados entre diferentes clientes (Aplicação) ou entre pedidos sucessivos de um mesmo cliente (Sessão).
  2. A aplicação enviará a sua resposta ao servidor através do objeto [Response]. Esta resposta, uma vez na rede, assumirá a seguinte forma: HTTP:

Image

Assim que esta resposta for enviada, o servidor encerrará a ligação de rede de receção (a menos que o cliente lhe tenha indicado para não o fazer).

  1. O cliente irá receber a resposta e, por sua vez, encerrará a ligação (em transmissão). O que será feito com essa resposta depende do tipo de cliente. Se for um navegador e o documento recebido for um documento HTML, este será apresentado. Se o cliente for um programa, a resposta será analisada e processada.
  2. O facto de, após o ciclo de pedido-resposta, a ligação que ligava o cliente ao servidor ser encerrada faz do protocolo HTTP um protocolo sem estado. Na solicitação seguinte, o cliente estabelecerá uma nova ligação de rede ao mesmo servidor. Como já não se trata da mesma ligação de rede, o servidor não tem qualquer possibilidade (ao nível do TCP/IP e do HTTP) de associar esta nova ligação a uma anterior. É o sistema de token de sessão que permitirá essa associação.

4.2.2. Recuperar as informações transmitidas pelo cliente

Analisamos agora algumas propriedades e métodos do objeto [Request] que permitem ao código da aplicação aceder à solicitação do cliente e, consequentemente, às informações que este transmitiu. O objeto [Request] é do tipo [HttpRequest]:

Image

Esta classe possui várias propriedades e métodos. Estamos interessados nas propriedades HttpMethod, QueryString, Form e Params, que nos permitirão aceder aos elementos da cadeia de informações [param1=val1&param2=val2&...].

HttpMethod as String
método de consulta do cliente: GET, POST, HEAD, ...
QueryString as NameValueCollection
recolha dos elementos da cadeia de consulta param1=val1&param2=val2&.. da 1.ª linha HTTP [méthode]?param1=val1&param2=val2&... onde [méthode] pode ser GET, POST, HEAD.
Form as NameValueCollection
recolha dos elementos da cadeia de consulta param1=val1&param2=val2&... que se encontram na parte [Document] da consulta (método POST).
Params as NameValueCollection
reúne várias coleções: QueryString, Form, ServerVariables, Cookies, numa única coleção.

4.2.3. Exemplo 1

Vamos implementar estes elementos num primeiro exemplo. A aplicação terá apenas um elemento [main.aspx]. O código de apresentação [main.aspx] será o seguinte:


<%@ 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>

A página apresenta três informações [méthode, nom, age] calculadas pela sua parte controladora [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
         ' a solicitação do cliente é armazenada em request.txt na pasta da aplicação
        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
         ' recuperam-se os parâmetros da solicitação
        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

Quando a página é carregada (Form_Load), as informações [nom, age] são recuperadas a partir do pedido do cliente. Estas são pesquisadas nas duas coleções [QueryString] e [Form]. . Além disso, em [Page_Init], guardamos a solicitação do cliente para podermos verificar o que este enviou. Colocamos estes dois ficheiros numa pasta <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/request1); em seguida, utilizando um navegador, acedemos à URL

[http://localhost/request1/main.aspx?nom=tintin&age=27]. Obtemos a seguinte resposta:

Image

As informações transmitidas pelo cliente foram recuperadas corretamente. A solicitação do navegador guardada no ficheiro [request.txt] é a seguinte:

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

Vemos que o navegador efetuou uma solicitação GET. Para efetuar uma solicitação POST, vamos utilizar o cliente [curl]. Numa janela do DOS, digitamos o seguinte comando:

C:\curl>curl --include --data nom=tintin --data age=27 http://localhost/request1/main.aspx
--include
para apresentar os cabeçalhos HTTP da resposta
--data param=valeur
para enviar a informação «param=valor» através de um POST

A resposta do servidor é a seguinte:

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>

O servidor recuperou, mais uma vez, os parâmetros enviados, desta vez por um POST. Para confirmar este último ponto, pode-se verificar o conteúdo do ficheiro [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

O cliente [curl] executou corretamente um POST. Agora, vamos combinar os dois métodos de transmissão de informação. Colocamos [age] no URL solicitado e [nom] no documento enviado:

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

A solicitação enviada por [curl] é a seguinte (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

Vê-se que a idade foi incluída no URL solicitado. Obter-se-á essa informação na coleção [QueryString]. O nome, por sua vez, foi incluído no documento enviado para esse URL. Obter-se-á essa informação na coleção [Form]. A resposta obtida pelo cliente [curl]:

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

Por fim, não enviemos nenhuma informação para o 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>

Sugere-se ao leitor que releia o código do controlador [main.aspx.vb] para compreender esta resposta.

4.2.4. Exemplo 2

É possível que o cliente envie vários valores para uma mesma chave. Então, o que acontece se, no exemplo anterior, solicitarmos a URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou], onde a chave [nom] aparece duas vezes? Vamos experimentar com um navegador:

Image

A nossa aplicação recuperou corretamente os dois valores associados à chave [nom]. A apresentação é um pouco enganadora. Foi obtida através da instrução


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

O método [ToString] produziu a cadeia [tintin,milou] que foi apresentada. Esta esconde o facto de que, na realidade, o objeto [Request.QueryString("nom")] é um array de cadeias de caracteres {"tintin","milou"}. O exemplo seguinte ilustra este ponto. A página de apresentação [main.aspx] será a seguinte:


<%@ 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>

Há novidades nesta página que utiliza o que se denomina «controlos de servidor». Estes caracterizam-se pelo atributo [runat="server"]. Ainda é cedo para introduzir o conceito de controlo de servidor. Basta saber que, neste caso:

  • a página tem duas listas (etiquetas <asp:listbox>)
  • que estas listas são objetos (lstQueryString, lstForm) do tipo [ListBox], que serão criados pelo controlador da página
  • que estes objetos só existem no servidor web. No momento da resposta, serão transformados em tags HTML clássicas que o cliente poderá compreender. Um objeto [listbox] será assim transformado (também se diz «renderizado») em tags HTML <select> e <option>.
  • O principal objetivo destes objetos é livrar o código de apresentação de todo o código VB, ficando este confinado ao controlador.

O controlador [main.aspx.vb] responsável pela construção dos dois objetos [lstQueryString] e [lstForm] é o seguinte:


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
        ' a solicitação do cliente é armazenada em request.txt na pasta da aplicação
        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
        ' recupera-se todo o conjunto de informações do 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
        ' inicialmente, uma lista de informações vazia
        Dim infos As New ArrayList
        ' recuperam-se as chaves da coleção
        Dim clés() As String = data.AllKeys
        ' percorre-se a tabela de chaves
        Dim valeurs() As String
        For Each clé As String In clés
            ' valores associados à chave
            valeurs = data.GetValues(clé)
            ' apenas um valor?
            If valeurs.Length = 1 Then
                infos.Add(clé + "=" + valeurs(0))
            Else
                ' vários valores
                For ivalue As Integer = 0 To valeurs.Length - 1
                    infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
                Next
            End If
        Next
        ' retorna o resultado
        Return infos
    End Function
End Class

Os pontos importantes deste código são os seguintes:

  • no [Form_Load], a página recupera as duas coleções [QueryString] e [Form]. Utiliza uma função [getValeurs] para colocar o conteúdo destas duas coleções em dois objetos do tipo [ArrayList], que conterão cadeias de caracteres do tipo [clé=valeur] se a chave da coleção estiver associada a um único valor, ou [clé(i)=valeur], caso a chave esteja associada a vários valores.
  • Cada um dos objetos [ArrayList] é, em seguida, associado a um dos objetos [ListBox] da página de apresentação por meio de duas instruções:
    • [ListBox.DataSource=ArrayList] e [ListBox.DataBind]. Esta última instrução transfere os elementos de [DataSource] para a coleção [Items] do objeto [ListBox]

Note-se que nenhum dos dois objetos [ListBox] é criado explicitamente por uma operação [New]. Deduz-se, portanto, que, na presença da baliza <asp:listbox id="xx">...<asp:listbox/>, o servidor web cria ele próprio o objeto [ListBox] referenciado pelo atributo [id] da baliza.

  • A função [getValeurs] utiliza o objeto do tipo [NameValueCollection] que lhe é passado como parâmetro para produzir um resultado do tipo [ArrayList].

Colocamos os dois ficheiros anteriores numa pasta <application-path> e iniciamos o servidor Cassini com os parâmetros (<application-path>,/request2); em seguida, solicitamos a URL

[http://localhost/request2/main.aspx?nom=tintin&age=27]. Obtemos a seguinte resposta:

Image

Solicitamos agora uma URL em que a chave [nom] aparece duas vezes:

Image

Constatamos que o objeto [Request.QueryString("nome")) era, de facto, um array. Aqui, as solicitações foram feitas através de um método GET. Utilizamos o cliente [curl] para efetuar uma 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>

Pode-se verificar que o cliente recebe, de facto, o código HTML clássico para as duas listas da página. Aparecem informações que não inserimos nós próprios, como o campo oculto [_VIEWSTATE]. Estas informações foram geradas pelas tags <asp:xx runat="server>. Teremos de aprender a dominá-las.

4.3. Implementação de uma arquitetura MVC

4.3.1. O conceito

Vamos concluir este longo capítulo com a implementação de uma aplicação construída de acordo com o modelo MVC (Model-View-Controller). Uma aplicação web arquitetada de acordo com este modelo tem o seguinte aspeto:

Image

  • o cliente envia as suas solicitações a uma entidade específica da aplicação denominada «controlador»
  • o controlador analisa a solicitação do cliente e executa-a. Para tal, conta com a ajuda de classes que agrupam a lógica de negócio da aplicação e de classes de acesso aos dados.
  • Dependendo do resultado da execução da solicitação, o controlador opta por enviar uma determinada página como resposta ao cliente

No nosso modelo, todas as solicitações passam por um único controlador, que é o maestro de toda a aplicação web. A vantagem deste modelo é que é possível agrupar no controlador tudo o que deve ser feito antes de cada solicitação. Suponhamos, por exemplo, que a aplicação exija uma autenticação. Esta é realizada uma única vez. Uma vez bem-sucedida, a aplicação irá armazenar na sessão informações relacionadas com o utilizador que acabou de se autenticar. Como um cliente pode aceder diretamente a uma página da aplicação sem se autenticar, cada página terá, portanto, de verificar na sessão se a autenticação foi efetivamente realizada. Se todas as solicitações passarem por um único controlador, é este que pode realizar essa tarefa. As páginas para as quais a solicitação venha a ser encaminhada não terão de o fazer.

4.3.2. Controlar uma aplicação MVC sem sessão

Pelo que vimos até agora, podemos pensar que o ficheiro [global.asax] poderia desempenhar o papel de controlador. Com efeito, sabemos que todas as solicitações passam por ele. Está, portanto, bem posicionado para controlar tudo. A aplicação que se segue utiliza-o para esse fim. O seu caminho virtual será [http://localhost/mvc1/main.aspx]. Para indicar o que pretende, o cliente irá adicionar à URL um parâmetro action=valor. Dependendo do valor do parâmetro [action], o controlador [global.asax] encaminhará a solicitação para uma página específica:

  1. [main.aspx] se o parâmetro «action» não estiver definido ou se «action=main»
  2. [action1.aspx] se action=action1
  3. [inconnu.aspx] se «action» não se enquadrar nos casos 1 e 2

As páginas [main.aspx, action1.aspx, inconnu.aspx] limitam-se a apresentar o valor de [action] que provocou a sua exibição. Apresentamos abaixo os oito ficheiros desta aplicação e comentamo-los sempre que necessário:

[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)
        ' recuperamos a ação a realizar
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' coloca-se a ação no contexto do pedido
        Context.Items("action") = action
        ' executa-se a ação
        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

Pontos a ter em conta:

  • interceptamos todos os pedidos do cliente no procedimento [Application_BeginRequest], que é executado automaticamente no início de cada novo pedido feito à aplicação.
  • Nesta rotina, temos acesso ao objeto [Request], que é a representação da solicitação HTTP do cliente. Como esperamos uma URL do tipo [http://localhost/mvc1/main.aspx?action=xx], procuramos uma chave [action] na coleção [Request.QueryString]. Se não a encontrarmos, definimos por predefinição a ação igual a «main».
  • O valor do parâmetro [action] é colocado no objeto [Context]. Tal como os objetos [Application, Session, Request, Response, Server], este objeto é global e acessível em qualquer código. Este objeto é passado de página em página se o pedido for processado por várias páginas, como será o caso aqui. É eliminado assim que a resposta for enviada ao cliente. A sua duração corresponde, portanto, à duração do processamento do pedido.
  • De acordo com o valor do parâmetro [action], a solicitação é encaminhada para a página apropriada. Para tal, utiliza-se o objeto global [Server], que, graças ao seu método, permite transferir a solicitação atual para outra página. O seu primeiro parâmetro é o nome da página de destino; o segundo é um valor booleano que indica se se deve ou não transferir para a página de destino as coleções [QueryString] e [Form]. Neste caso, a resposta é sim.

Os ficheiros [main.aspx] e [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
        ' recupera-se a ação em curso
        action = Me.Context.Items("action").ToString
    End Sub
End Class

O controlador [main.aspx.vb] limita-se a recuperar o valor da chave [action] no contexto, sendo esse valor apresentado pelo código de apresentação. O objetivo aqui é mostrar a passagem do objeto [Context] entre diferentes páginas que processam a mesma solicitação do cliente. As páginas [action1.aspx] e [inconnu.aspx] funcionam de forma semelhante:

[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
         ' recupera-se a ação em 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
         ' recupera-se a ação em curso
        action = Me.Context.Items("action").ToString
    End Sub
End Class

Para testar, os documentos anteriores são colocados numa pasta <application-path> e o Cassini é iniciado com os parâmetros (<application-path>,/mvc1). Acedemos à URL [http://localhost/mvc1/main.aspx]:

Image

A solicitação não enviou nenhum parâmetro [action]. O código do controlador da aplicação [global.asax.vb] fez com que fosse apresentada a página [main.aspx]. Agora solicitamos a URL [http://localhost/mvc1/main.aspx?action=action1]:

Image

O código do controlador da aplicação [global.asax.vb] gerou a página [action1.aspx]. Agora solicitamos a URL [http://localhost/mvc1/main.aspx?action=xx]:

Image

A ação não foi reconhecida e o controlador [global.asax.vb] apresentou a página [inconnu.aspx].

4.3.3. Controlar uma aplicação MVC com sessão

Na maioria das vezes, as diferentes solicitações de um cliente para uma aplicação têm de partilhar informações. Vimos uma solução possível para este problema: armazenar as informações a partilhar no objeto [Session] da solicitação. Este objeto é, de facto, partilhado por todas as solicitações e é capaz de armazenar informações na forma (chave, valor), em que a chave é do tipo [String] e o valor é de qualquer tipo derivado de [Object].

No exemplo anterior, as diferentes páginas associadas às diferentes ações eram chamadas no procedimento [Application_BeginRequest] do ficheiro [global.asax.vb]:


    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' recupera-se a ação a realizar
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' coloca-se a ação no contexto do pedido
        Context.Items("action") = action
        ' executa-se a ação
        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

Verifica-se que, no procedimento [Application_BeginRequest], o objeto [Session] não está acessível. O mesmo se verifica na página para a qual a execução é transferida. Por conseguinte, este modelo não pode ser utilizado numa aplicação com sessão. Podemos atribuir a função de controlador a qualquer página, por exemplo, [default.aspx]. Os ficheiros [global.asax, global.asax.vb] desaparecem então para serem substituídos pelos ficheiros [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
        ' recupera-se a ação a realizar
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If

        ' coloca-se a ação no contexto da solicitação
        Context.Items("action") = action
        ' recupera-se a ação anterior, caso exista
        Context.Items("actionPrec") = Session.Item("actionPrec")
        If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
        ' a ação atual é guardada na sessão
        Session.Item("actionPrec") = action

        ' executa-se a ação
        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 o mecanismo de sessão, as diferentes páginas irão apresentar, além da ação atual, a ação que a precedeu. Para uma sequência de ações A1, A2, ..., An, quando a ação Ai ocorre, o controlador acima:

  • coloca a ação atual Ai no contexto
  • recupera na sessão a ação Ai-1 que a precedeu. Caso não exista nenhuma (como no caso da ação A1), a cadeia da ação anterior fica vazia.
  • coloca a ação atual Ai na sessão, substituindo Ai-1
  • transfere a execução para a página adequada

As três páginas da aplicação são as seguintes:

[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>

Como as três páginas apresentam as mesmas informações [action, actionPrec], todas elas podem ter o mesmo controlador de página. Por isso, todas elas foram derivadas da classe [main] do ficheiro [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
        ' recupera-se a ação em curso
        action = Me.Context.Items("action").ToString
        ' e a ação anterior
        actionPrec = Me.Context.Items("actionPrec").ToString
    End Sub
End Class

O código acima limita-se a recuperar as informações inseridas no contexto pelo controlador da aplicação [default.aspx.vb].

Todos estes ficheiros estão localizados em <application-path> e o Cassini é iniciado com os parâmetros (<application-path>,/mvc2). Primeiro, solicita-se a URL [http://localhost/mvc2]:

Image

A URL [http://localhost/mvc2] remete para uma pasta. Sabemos que, neste caso, é o documento [default.aspx] dessa pasta que é devolvido pelo servidor, caso exista. Neste caso, não foi especificada nenhuma ação. Por isso, foi executada a ação [main]. Passemos agora à ação [action1]:

Image

A ação atual e a ação anterior foram identificadas corretamente. Passemos à ação [xx]:

Image

4.4. Conclusion

Temos agora os elementos básicos a partir dos quais qualquer aplicação ASP.ET é construída. Resta-nos, no entanto, introduzir um conceito importante: o de formulário. É esse o tema do capítulo seguinte.