Skip to content

4. Grundlagen der Entwicklung ASP.NET

4.1. Der Begriff der Webanwendung ASP.NET

4.1.1. Einführung

Eine Webanwendung ist eine Anwendung, die verschiedene Dokumente (HTML, Code .NET, Bilder, Sounds usw.) umfasst. Diese Dokumente müssen sich im selben Stammverzeichnis befinden, das als Stammverzeichnis der Webanwendung bezeichnet wird. Diesem Stammverzeichnis ist ein virtueller Pfad des Webservers zugeordnet. Wir haben den Begriff des virtuellen Ordners bereits im Zusammenhang mit dem Webserver Cassini kennengelernt. Dieser Begriff existiert auch für den Webserver IIS. Ein wesentlicher Unterschied zwischen den beiden Servern besteht darin, dass der Webserver IIS zu einem bestimmten Zeitpunkt eine beliebige Anzahl virtueller Verzeichnisse haben kann, während der Cassini-Webserver nur eines hat, nämlich dasjenige, das bei seinem Start festgelegt wurde. Das bedeutet, dass der Server IIS mehrere Webanwendungen gleichzeitig bereitstellen kann, während der Cassini-Server jeweils nur eine bereitstellt. In den vorangegangenen Beispielen wurde der Cassini-Server stets mit den Parametern (<webroot>,/aspnet) gestartet, die den virtuellen Ordner /aspnet dem physischen Ordner <webroot> zuordneten. Der Webserver bediente daher immer dieselbe Webanwendung. Das hat uns jedoch nicht daran gehindert, verschiedene und voneinander unabhängige Seiten innerhalb dieser einzigen Webanwendung zu erstellen und zu testen. Jede Webanwendung verfügt über eigene Ressourcen, die sich unter ihrem physischen Stammverzeichnis <webroot> befinden:

  • ein Ordner „[bin]“, in dem vorkompilierte Klassen abgelegt werden können
  • eine Datei [global.asax], mit der die Webanwendung als Ganzes sowie die Laufzeitumgebung jedes einzelnen Benutzers initialisiert werden können
  • eine Datei [web.config], mit der der Betrieb der Anwendung konfiguriert werden kann
  • eine Datei [default.aspx], die als Einstiegspunkt der Anwendung dient
  • ...

Sobald eine Anwendung eine dieser drei Ressourcen nutzt, benötigt sie einen eigenen physischen und virtuellen Pfad. Es gibt nämlich keinen Grund, warum zwei verschiedene Webanwendungen auf dieselbe Weise konfiguriert werden sollten. Unsere vorherigen Beispiele konnten alle in derselben Anwendung (<webroot>,/aspnet) untergebracht werden, da sie keine der oben genannten Ressourcen nutzten.

Kehren wir zu der zu Beginn dieses Kapitels empfohlenen Architektur MVC für die Entwicklung einer Webanwendung zurück:

Image

Die Webanwendung besteht aus Klassendateien (Controller, Geschäftslogik-Klassen, Datenzugriffsklassen) und Präsentationsdateien (HTML-Dokumente, Bilder, Sounds, Stylesheets usw.). Alle diese Dateien werden in einem gemeinsamen Stammverzeichnis abgelegt, das wir gelegentlich als <application-path> bezeichnen werden. Dieses Stammverzeichnis wird einem virtuellen Pfad <application-vpath> zugeordnet. Die Zuordnung zwischen diesem virtuellen Pfad und dem physischen Pfad erfolgt über die Konfiguration des Webservers. Wir haben gesehen, dass diese Zuordnung beim Cassini-Server beim Start des Servers erfolgt. In einem DOS-Fenster würde man Cassini beispielsweise mit folgendem Befehl starten:

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

Im Ordner <application-path> finden wir je nach Bedarf:

  • den Ordner [bin], um dort vorkompilierte Klassen (DLLs) abzulegen
  • die Datei [global.asax], wenn wir Initialisierungen vornehmen müssen, entweder bei der Initialisierung der Anwendung oder bei der Initialisierung einer Benutzersitzung
  • die Datei [web.config], wenn wir die Anwendung konfigurieren müssen
  • die Datei [default.aspx], wenn wir eine Standardseite in der Anwendung benötigen

Um diesem Webanwendungskonzept gerecht zu werden, werden die folgenden Beispiele alle in einem anwendungsspezifischen Ordner <application-path> abgelegt, dem ein virtueller Ordner <application-vpath> zugeordnet ist, wobei der Cassini-Server so gestartet wird, dass diese beiden Parameter miteinander verknüpft werden.

4.1.2. Eine Webanwendung konfigurieren

Wenn <application-path> das Stammverzeichnis einer Anwendung ASP.NET ist, kann die Datei <application-path>\web.config verwendet werden, um diese zu konfigurieren. Diese Datei hat das Format XML. Hier ein Beispiel:

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

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

Bitte beachten Sie, dass bei den Tags XML die Groß- und Kleinschreibung beachtet werden muss. Alle Konfigurationsangaben müssen zwischen den Tags <configuration> und </configuration> stehen. Es stehen zahlreiche Konfigurationsabschnitte zur Verfügung. Wir stellen hier nur einen davon vor, nämlich den Abschnitt <appSettings>, mit dem Daten mithilfe des Tags <add> initialisiert werden können. Die Syntax dieses Tags lautet wie folgt:

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

Wenn der Webserver eine Anwendung startet, prüft er, ob sich im Verzeichnis <application-path> eine Datei namens web.config befindet. Ist dies der Fall, liest er sie ein und speichert ihre Informationen in einem Objekt vom Typ [ConfigurationSettings], das allen Seiten der Anwendung zur Verfügung steht, solange diese aktiv ist. Die Klasse [ConfigurationSettings] verfügt über eine statische Methode [AppSettings]:

Image

Um den Wert eines Schlüssels C aus der Konfigurationsdatei abzurufen, schreibt man ConfigurationSettings.AppSettings("C"). Man erhält eine Zeichenkette. Um die vorstehende Konfigurationsdatei zu nutzen, erstellen wir eine Seite [default.aspx]. Der Code VB aus der Datei [default.aspx.vb] lautet wie folgt:


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
        'Konfigurationsinformationen werden abgerufen
        nom = ConfigurationSettings.AppSettings("nom")
        age = ConfigurationSettings.AppSettings("age")
    End Sub

End Class

Man sieht, dass beim Laden der Seite die Werte der Konfigurationsparameter [nom] und [age] abgerufen werden. Sie werden durch den Darstellungscode von [default.aspx] angezeigt:


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

Für den Test werden die Dateien [web.config], [default.aspx] und [default.aspx.vb] in denselben Ordner kopiert:

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

Sei <application-path> der Ordner, in dem sich die drei Dateien der Anwendung befinden. Der Cassini-Server wird mit den Parametern (<application-path>,/aspnet/config1) gestartet. Wir fragen nach den Dateien URL und [http://localhost/aspnet/config1]. Da es sich bei [config1] um einen Ordner handelt, sucht der Webserver darin nach einer Datei namens [default.aspx] und zeigt sie an, falls er sie findet. In diesem Fall wird er sie finden:

Image

4.1.3. Anwendung, Sitzung, Kontext

4.1.3.1. Die Datei global.asax

Der Code der Datei [global.asax] wird immer ausgeführt, bevor die durch die aktuelle Anfrage angeforderte Seite geladen wird. Sie muss sich im Stammverzeichnis <application-path> der Anwendung befinden. Falls vorhanden, wird die Datei [global.asax] zu verschiedenen Zeitpunkten vom Webserver verwendet:

  1. beim Start oder Beenden der Webanwendung
  2. wenn eine Benutzersitzung beginnt oder endet
  3. wenn eine Benutzersitzung beginnt

Wie bei den .aspx-Seiten kann die Datei „[global.asax]“ auf verschiedene Arten geschrieben werden, insbesondere durch die Trennung des VB-Codes in eine Controller-Klasse und Präsentationscode. Dies ist die Standardauswahl des Tools Visual Studio, und wir werden hier ebenso vorgehen. Normalerweise ist keine Darstellung erforderlich, da diese Aufgabe den .aspx-Seiten obliegt. Der Inhalt der Datei [global.asax] beschränkt sich daher auf eine Anweisung, die auf die Datei verweist, die den Controller-Code enthält:


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

Es ist zu beachten, dass die Anweisung nicht mehr [Page], sondern [Application] lautet. Der zugehörige Controller-Code [global.asax.vb], der vom Visual Studio-Tool generiert wurde, lautet wie folgt:


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)
        ' Wird ausgelöst, wenn die Anwendung gestartet wird
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn die Sitzung gestartet wird
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird zu Beginn jeder Anfrage ausgelöst
    End Sub

    Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird bei einem Authentifizierungsversuch des Benutzers ausgelöst
    End Sub

    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn ein Fehler auftritt
    End Sub

    Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn die Sitzung beendet wird
    End Sub

    Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn die Anwendung beendet wird
    End Sub

End Class

Es ist zu beachten, dass die Controller-Klasse von der Klasse [HttpApplication] abgeleitet ist. Im Lebenszyklus einer Anwendung gibt es mehrere wichtige Ereignisse. Diese werden durch Prozeduren verwaltet, deren Grundgerüst oben angegeben ist.

  • [Application_Start]: Zur Erinnerung: Eine Webanwendung ist in einem virtuellen Pfad „eingeschlossen“. Die Anwendung startet, sobald eine Seite in diesem virtuellen Pfad von einem Client angefordert wird. Die Prozedur [Application_Start] wird dann ausgeführt. Dies geschieht nur ein einziges Mal. In dieser Prozedur werden alle für die Anwendung notwendigen Initialisierungen vorgenommen, wie zum Beispiel das Anlegen von Objekten, deren Lebensdauer der Lebensdauer der Anwendung entspricht.
  • [Application-End]: wird ausgeführt, wenn die Anwendung beendet ist. Jeder Anwendung ist eine Inaktivitätszeit zugeordnet, die in [web.config] konfiguriert werden kann und nach deren Ablauf die Anwendung als beendet gilt. Diese Entscheidung trifft also der Webserver auf der Grundlage der Anwendungseinstellungen. Die Inaktivitätszeit einer Anwendung ist definiert als die Zeitspanne, in der kein Client eine Anfrage für eine Ressource der Anwendung gestellt hat.
  • [Session-Start]/[Session_End]: Jedem Client ist eine Sitzung zugeordnet, es sei denn, die Anwendung ist so konfiguriert, dass sie keine Sitzungen verwendet. Ein Client ist nicht gleichbedeutend mit einem Benutzer vor seinem Bildschirm. Hat dieser zwei Browser geöffnet, um die Anwendung abzufragen, gilt er als zwei Clients. Ein Client wird durch ein Sitzungstoken gekennzeichnet, das er jeder seiner Anfragen beifügen muss. Dieses Sitzungstoken ist eine vom Webserver zufällig generierte, eindeutige Zeichenfolge. Zwei Clients können nicht dasselbe Sitzungstoken haben. Dieses Token begleitet den Client wie folgt:
    • Der Client, der seine erste Anfrage stellt, sendet kein Sitzungs-Token. Der Webserver erkennt dies und weist ihm eines zu. Dies ist der Beginn der Sitzung, und die Prozedur [Session_Start] wird ausgeführt. Dies geschieht nur einmal.
    • Der Client sendet bei seinen folgenden Anfragen das Token, das ihn identifiziert. Dadurch kann der Webserver Informationen abrufen, die mit diesem Token verknüpft sind. Dies ermöglicht eine Verfolgung der verschiedenen Anfragen des Clients.
    • Die Anwendung kann einem Kunden ein Formular zum Beenden der Sitzung zur Verfügung stellen. In diesem Fall fordert der Kunde selbst das Beenden seiner Sitzung an. Die Prozedur [Session_End] wird ausgeführt. Dies geschieht nur einmal.
    • Es kann vorkommen, dass der Client niemals selbst die Beendigung seiner Sitzung anfordert. In diesem Fall wird die Sitzung nach einer bestimmten Inaktivitätszeit – die ebenfalls über [web.config] konfigurierbar ist – vom Webserver beendet. Dabei wird die Prozedur [Session_End] ausgeführt.
  • [Application_BeginRequest]: Diese Prozedur wird ausgeführt, sobald eine neue Anfrage eingeht. Sie wird also bei jeder Anfrage eines beliebigen Clients ausgeführt. Dies ist ein guter Zeitpunkt, um die Anfrage zu prüfen, bevor sie an die angeforderte Seite weitergeleitet wird. Man kann sogar entscheiden, sie auf eine andere Seite umzuleiten.
  • [Application_Error]: Wird jedes Mal ausgeführt, wenn ein Fehler auftritt, der nicht explizit durch den Code des Controllers [global.asax.vb] behandelt wird. Hier kann die Anfrage des Kunden auf eine Seite umgeleitet werden, auf der die Ursache des Fehlers erläutert wird.

Wenn keines dieser Ereignisse behandelt werden soll, kann die Datei [global.asax] ignoriert werden. Dies wurde bei den ersten Beispielen dieses Kapitels so gehandhabt.

4.1.3.2. Beispiel 1

Entwickeln wir eine Anwendung, um die drei Phasen – den Start der Anwendung, den Start der Sitzung und den Start einer Client-Anfrage – besser zu verstehen. Die Datei [global.asax] sieht wie folgt aus:

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

Die zugehörige Datei [global.asax.vb] sieht wie folgt aus:


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)
        ' Wird ausgelöst, wenn die Anwendung gestartet wird
        ' Die Uhrzeit wird notiert
        Dim startApplication As String = Date.Now.ToString("T")
        ' wird im Anwendungskontext gespeichert
        Application.Item("startApplication") = startApplication
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn die Sitzung gestartet wird
        ' Die Uhrzeit wird notiert
        Dim startSession As String = Date.Now.ToString("T")
        ' wird in die Sitzung übernommen
        Session.Item("startSession") = startSession
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Die Uhrzeit wird notiert
        Dim startRequest As String = Date.Now.ToString("T")
        ' wird in die Sitzung übernommen
        Context.Items("startRequest") = startRequest
    End Sub
End Class

Die wichtigsten Punkte des Codes sind folgende:

  • Der Webserver stellt der Klasse [HttpApplication] von [global.asax.vb] eine bestimmte Anzahl von Objekten zur Verfügung:
    • Anwendung vom Typ [HttpApplicationState] – stellt die Webanwendung dar – gewährt Zugriff auf ein Objektverzeichnis [Application.Item], das für alle Clients der Anwendung zugänglich ist – ermöglicht den Informationsaustausch zwischen verschiedenen Clients – der gleichzeitige Lese-/Schreibzugriff mehrerer Clients auf dieselben Daten erfordert eine Synchronisation der Clients.
    • Sitzung vom Typ [HttpSessionState] – repräsentiert einen bestimmten Client – gewährt Zugriff auf ein Objektverzeichnis [Session.Item], das für alle Anfragen dieses Clients zugänglich ist – ermöglicht das Speichern von Informationen über einen Client, die im Verlauf seiner Anfragen abgerufen werden können.
    • Anfrage vom Typ [HttpRequest] – stellt die aktuelle Anfrage HTTP des Kunden dar
    • Antwort vom Typ [HttpResponse] – stellt die Antwort HTTP dar, die gerade vom Server an den Kunden erstellt wird
    • Server vom Typ [HttpServerUtility] – bietet Hilfsmethoden, insbesondere um die Anfrage an eine andere als die ursprünglich vorgesehene Seite weiterzuleiten.
    • Kontext vom Typ [HttpContext] – dieses Objekt wird bei jeder neuen Anfrage neu erstellt, wird jedoch von allen Seiten gemeinsam genutzt, die an der Bearbeitung der Anfrage beteiligt sind – ermöglicht dank seines Wörterbuchs „Items“ die Übertragung von Informationen von Seite zu Seite während der Bearbeitung einer Anfrage.
  • Die Prozedur [Application_Start] speichert den Start der Anwendung in einer Variablen, die in einem auf Anwendungsebene zugänglichen Dictionary abgelegt ist
  • Die Prozedur [Session_Start] speichert den Beginn der Sitzung in einer Variablen, die in einem auf Sitzungsebene zugänglichen Dictionary abgelegt ist
  • Die Prozedur [Application_BeginRequest] speichert den Beginn der Anfrage in einer Variablen, die in einem auf Anfrageebene zugänglichen Dictionary abgelegt ist (c.a.d ist während der gesamten Bearbeitungszeit verfügbar, geht jedoch am Ende der Bearbeitung verloren)

Die Zielseite ist die folgende Seite [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>

Diese Übersichtsseite zeigt Werte an, die von ihrem Controller [main.aspx.vb] berechnet wurden:

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
         ' Die Informationen zur Anwendung und zur Sitzung werden abgerufen
        jeton=Session.SessionId
        startApplication = Application.Item("startApplication").ToString
        startSession = Session.Item("startSession").ToString
        startRequest = Context.Items("startRequest").ToString
    End Sub

End Class

Der Controller ruft lediglich die drei Informationen ab, die von [global.asax.vb] jeweils in der Anwendung, der Sitzung und dem Kontext abgelegt wurden.

Wir testen die Anwendung wie folgt:

  1. Die Dateien werden in einem einzigen Ordner <application-path> zusammengefasst

Image

  1. Der Cassini-Server wird mit den Parametern (<application-path>,/aspnet/globalasax1) gestartet
  2. Ein erster Client fordert die URL [http://localhost/aspnet/globalasax1/main.aspx] an und erhält das folgende Ergebnis:

Image

  1. Derselbe Client stellt eine neue Anfrage (Option „Neu laden“ des Browsers):

Image

Man sieht, dass sich lediglich der Zeitpunkt der Anfrage geändert hat. Dies zeigt zwei Dinge:

  • Die Prozeduren [Application_Start] und [Session_Start] von [global.asax] wurden bei der zweiten Anfrage nicht ausgeführt.
  • Die Objekte [Application] und [Session], in denen die Startzeiten der Anwendung und der Sitzung gespeichert waren, stehen für die zweite Anfrage weiterhin zur Verfügung.
  1. Wir starten einen zweiten Browser, um einen zweiten Client zu erstellen, und rufen dieselbe URL erneut auf:

Image

Diesmal stellen wir fest, dass sich die Sitzungszeit geändert hat. Der zweite Browser wurde, obwohl er sich auf demselben Rechner befindet, als zweiter Client betrachtet, und für ihn wurde eine neue Sitzung erstellt. Es ist zu erkennen, dass die beiden Clients nicht dasselbe Sitzungstoken haben. Die Startzeit der Anwendung hat sich nicht geändert, was bedeutet, dass:

  • die Prozedur [Application_Start] von [global.asax.vb] nicht ausgeführt wurde
  • das Objekt [Application], in dem der Startzeitpunkt der Anwendung gespeichert wurde, ist für den zweiten Mandanten zugänglich. Daher müssen die Informationen, die von den verschiedenen Clients der Anwendung gemeinsam genutzt werden sollen, in diesem Objekt gespeichert werden, während das Objekt [Session] dazu dient, Informationen zu speichern, die von den Anfragen desselben Clients gemeinsam genutzt werden sollen.

4.1.3.3. Ein Überblick

Mit dem bisher Gelernten sind wir in der Lage, ein erstes Schema zur Funktionsweise eines Webservers und der von ihm bedienten Webanwendungen zu erstellen:

Image

Das vorstehende Schema zeigt einen Server, der zwei Anwendungen mit den Bezeichnungen A und B bedient, die jeweils zwei Clients haben. Ein Webserver ist in der Lage, mehrere Webanwendungen gleichzeitig zu bedienen. Diese sind völlig unabhängig voneinander. Wir betrachten nun die Anwendung A. Die Bearbeitung einer Anfrage von Client-1A an die Anwendung A läuft wie folgt ab:

  • Der Client 1A fordert vom Webserver eine Ressource an, die zum Bereich der Anwendung A gehört. Das bedeutet, dass er eine URL in der Form [http://machine:port/VA/ressource] anfordert, wobei VA der virtuelle Pfad der Anwendung A ist.
  • Wenn der Webserver feststellt, dass es sich um die erste Anfrage nach einer Ressource der Anwendung A handelt, löst er das Ereignis [Application_Start] aus der Datei [global.asax] der Anwendung A aus. Es wird ein Objekt [ApplicationA] vom Typ [HttpApplicationState] erstellt. Die verschiedenen Codes der Anwendung speichern in diesem Objekt Daten mit den Gültigkeitsbereichen [Application] und c.a.d, die alle Benutzer betreffen. Das Objekt [ApplicationA] bleibt bestehen, bis der Webserver die Anwendung A entlastet.
  • Wenn der Webserver zudem feststellt, dass es sich um einen neuen Client der Anwendung A handelt, löst er das Ereignis [Session_Start] aus der Datei [global.asax] der Anwendung A aus. Es wird ein Objekt vom Typ [HttpSessionState] mit der Bezeichnung [Session-1A] erstellt. Dieses Objekt ermöglicht es der Anwendung A, Objekte mit dem Gültigkeitsbereich [Session] und c.a.d zu speichern, die zu einem bestimmten Client gehören. Das Objekt [Session-1A] existiert so lange, wie der Client 1A Anfragen stellt. Es ermöglicht die Nachverfolgung dieses Clients. Der Webserver erkennt in zwei Fällen, dass es sich um einen neuen Client handelt:
    • Der Client hat ihm in den HTTP-Headern seiner Anfrage kein Session-Token gesendet
    • der Client ihm ein Session-Token gesendet hat, das nicht existiert (Fehlfunktion des Clients oder Hacking-Versuch) oder das nicht mehr existiert. Ein Session-Token läuft nämlich nach einer bestimmten Zeit der Inaktivität des Clients ab (standardmäßig 20 Minuten bei IIS). Diese Zeitspanne ist programmierbar.
  • In jedem Fall löst der Webserver das Ereignis [Application_BeginRequest] aus der Datei [global.asax] aus. Dieses Ereignis startet die Verarbeitung einer Client-Anfrage. Häufig wird dieses Ereignis nicht weiterverarbeitet, sondern die Kontrolle an die vom Client angeforderte Seite übergeben, die die Anfrage dann verarbeitet. Man kann dieses Ereignis aber auch nutzen, um die Anfrage zu analysieren, zu verarbeiten und zu entscheiden, welche Seite als Antwort gesendet werden soll. Wir werden diese Technik verwenden, um eine Anwendung zu implementieren, die der zuvor erwähnten Architektur MVC entspricht.
  • Sobald der Filter [global.asax] durchlaufen ist, wird die Anfrage des Kunden an eine .aspx-Seite weitergeleitet, die die Anfrage verarbeitet. Wir werden später sehen, dass es möglich ist, die Anfrage durch einen Filter aus mehreren Seiten zu leiten. Die letzte Seite ist dafür zuständig, die Antwort an den Kunden zu senden. Die Seiten können der ursprünglichen Anfrage des Kunden Informationen hinzufügen, die sie selbst berechnet haben. Sie können diese Informationen in der Sammlung Context.Items speichern. Tatsächlich haben alle Seiten, die an der Bearbeitung der Anfrage eines Kunden beteiligt sind, Zugriff auf diesen Datenspeicher.
  • Der Code der verschiedenen Seiten hat Zugriff auf die Datenpools, d. h. die Objekte [ApplicationA], [Session-1A], … Man muss bedenken, dass der Webserver für die Anwendung A mehrere Clients gleichzeitig bedient. Alle diese Clients haben Zugriff auf das Objekt [Application A]. Müssen sie Daten in diesem Objekt ändern, ist eine Synchronisierung der Clients erforderlich. Jeder Client XA hat darüber hinaus Zugriff auf den Datenspeicher [Session-XA]. Da dieser für ihn reserviert ist, ist hier keine Synchronisierung erforderlich.
  • Der Webserver bedient mehrere Webanwendungen gleichzeitig. Es gibt keinerlei Interferenzen zwischen den Clients dieser verschiedenen Anwendungen.

Aus diesen Erläuterungen lassen sich folgende Punkte ableiten:

  • Zu einem bestimmten Zeitpunkt bedient ein Webserver mehrere Clients gleichzeitig. Das bedeutet, dass er nicht das Ende einer Anfrage abwartet, um eine weitere zu bearbeiten. Zu einem Zeitpunkt T werden also mehrere Anfragen von verschiedenen Clients für verschiedene Anwendungen gleichzeitig bearbeitet. Die Verarbeitungscodes, die gleichzeitig innerhalb des Webservers ablaufen, werden manchmal als Ausführungsthreads bezeichnet.
  • Die Ausführungsthreads der Clients verschiedener Webanwendungen beeinträchtigen sich nicht gegenseitig. Es besteht Isolation.
  • Die Ausführungsthreads von Clients derselben Anwendung müssen unter Umständen Daten gemeinsam nutzen:
    • Die Ausführungsthreads der Anfragen zweier verschiedener Kunden (mit unterschiedlichen Session-Token) können Daten über das Objekt [Application] gemeinsam nutzen.
    • Die Ausführungsthreads aufeinanderfolgender Anfragen desselben Clients können Daten über das Objekt [Session] gemeinsam nutzen.
    • Die Ausführungsthreads aufeinanderfolgender Seiten, die dieselbe Anfrage eines bestimmten Kunden verarbeiten, können Daten über das Objekt [Context] gemeinsam nutzen.

4.1.3.4. Beispiel 2

Entwickeln wir ein neues Beispiel, das das eben Gesagte verdeutlicht. Wir legen die folgenden Dateien im selben Ordner ab:

[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)
        ' Wird ausgelöst, wenn die Anwendung gestartet wird
        ' Kundenzähler initialisieren
        Application.Item("nbRequêtes") = 0
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn die Sitzung gestartet wird
        ' Zähler für Anfragen initialisieren
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

Das Prinzip der Anwendung besteht darin, die Gesamtzahl der an die Anwendung gerichteten Anfragen sowie die Anzahl der Anfragen pro Kunde zu zählen. Beim Start der Anwendung [Application_Start] wird der Zähler für die an die Anwendung gerichteten Anfragen auf 0 gesetzt. Dieser Zähler wird im Gültigkeitsbereich [Application] abgelegt, da er von allen Kunden inkrementiert werden muss. Wenn sich ein Client zum ersten Mal bei [Session_Start] anmeldet, wird der Zähler für die von diesem Client gestellten Anfragen auf 0 gesetzt. Dieser Zähler befindet sich im Gültigkeitsbereich [Session], da er nur einen bestimmten Client betrifft.

Sobald [global.asax] ausgeführt wurde, wird die folgende Datei [main.aspx] ausgeführt:


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

Es zeigt drei vom Controller berechnete Informationen an:

  1. die Identität des Kunden anhand seines Sitzungstokens: [jeton]
  2. die Gesamtzahl der an die Anwendung gesendeten Anfragen: [nbRequêtesApplication]
  3. die Gesamtzahl der vom in 1 identifizierten Kunden gestellten Anfragen: [nbRequêtesClient]

Die drei Informationen werden in [main.aspx.vb] berechnet:

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
         ' Eine weitere Anfrage für die Anwendung
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
         ' Eine weitere Anfrage in der Sitzung
        Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
         ' Darstellungsvariablen initialisieren
        nbRequêtesApplication = Application.Item("nbRequêtes").ToString
        jeton = Session.SessionID
        nbRequêtesClient = Session.Item("nbRequêtes").ToString
    End Sub
End Class

Wenn [main.aspx.vb] ausgeführt wird, wird gerade eine Anfrage eines bestimmten Kunden bearbeitet. Wir verwenden das Objekt [Application], um die Anzahl der Anfragen der Anwendung zu erhöhen, und das Objekt [Session], um die Anzahl der Anfragen des Kunden zu erhöhen, dessen Anfrage gerade bearbeitet wird. Zur Erinnerung: Während alle Kunden derselben Anwendung dasselbe Objekt [Application] gemeinsam nutzen, verfügt jeder von ihnen über ein eigenes Objekt [Session].

Wir testen die Anwendung, indem wir die vier oben genannten Dateien in einen Ordner namens <application-path> ablegen und den Cassini-Server mit den Parametern (<application-path>,/aspnet/webapplia) starten. Wir öffnen einen ersten Browser und rufen die URL [http://localhost/aspnet/webapplia/main.aspx] auf:

Image

Wir führen eine zweite Anfrage über die Schaltfläche [Reload] durch:

Image

Wir starten einen zweiten Browser, um dieselbe URL aufzurufen. Für den Webserver handelt es sich dabei um einen neuen Client:

Image

Man kann feststellen, dass sich das Session-Token geändert hat und es sich somit um einen neuen Client handelt. Dies spiegelt sich in der Anzahl der Client-Anfragen wider. Kehren wir nun zum ersten Browser zurück und rufen wir dieselbe URL erneut auf:

Image

Die Anzahl der an die Anwendung gerichteten Anfragen wird korrekt gezählt.

4.1.3.5. Zur Notwendigkeit, die Clients einer Anwendung zu synchronisieren

In der vorherigen Anwendung wird der Zähler für die an die Anwendung gerichteten Anfragen in der Prozedur [Form_Load] der Seite [main.aspx] wie folgt erhöht:

         ' eine weitere Anfrage für die Anwendung
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1

Diese Anweisung ist zwar einfach, erfordert jedoch mehrere Prozessorbefehle, um ausgeführt zu werden. Nehmen wir an, es sind drei erforderlich:

  1. Auslesen des Zählers
  2. Inkrementieren des Zählers
  3. Zurückschreiben des Zählers

Der Webserver läuft auf einem Multitasking-Rechner, was bedeutet, dass jeder Task für einige Millisekunden die Prozessorzeit erhält, bevor er sie wieder verliert und erst dann wieder erhält, wenn alle anderen Tasks ebenfalls ihr Zeitquantum erhalten haben. Nehmen wir an, dass zwei Clients A und B gleichzeitig eine Anfrage an den Webserver stellen. Nehmen wir an, dass Client A als Erster an der Reihe ist, in die Prozedur [Form_Load] von [main.aspx.vb] gelangt, den Zählerstand (=100) ausliest und dann unterbrochen wird, weil sein Zeitquantum aufgebraucht ist. Nehmen wir nun an, dass Client B an der Reihe ist und ihm dasselbe widerfährt: Er schafft es, den Zählerwert (=100) auszulesen, hat aber keine Zeit mehr, ihn zu erhöhen. Die Clients A und B verfügen beide über einen Zählerwert von 100. Nehmen wir an, dass nun wieder Client A an der Reihe ist: Er erhöht seinen Zähler, setzt ihn auf 101 und beendet dann den Vorgang. Nun ist Client B an der Reihe, der den alten Zählerwert und nicht den neuen besitzt. Er setzt den Zählerwert daher ebenfalls auf 101 und beendet den Vorgang. Der Wert des Anforderungszählers der Anwendung ist nun fehlerhaft.

Um dieses Problem zu veranschaulichen, greifen wir auf die vorherige Anwendung zurück und ändern sie wie folgt:

  • Die Dateien [global.asax], [global.asax.vb] und [main.aspx] bleiben unverändert
  • Die Datei „[main.aspx.vb]“ sieht nun wie folgt aus:

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
        ' eine weitere Anfrage für die Anwendung und die Sitzung
        ' Zähler auslesen
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' 5 Sekunden warten
        Thread.Sleep(5000)
        ' Inkrementierung der Zähler
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' Speichern der Zähler
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' Anzeigevariablen initialisieren
        jeton = Session.SessionID
    End Sub
End Class

Die Zählerstandserfassung wurde in vier Phasen unterteilt:

  1. Zähler auslesen
  2. Setzen des Ausführungs-Threads in den Ruhezustand
  3. Inkrementierung des Zählers
  4. Neuschreiben des Zählers

Betrachten wir noch einmal unsere beiden Clients A und B. Zwischen der Lese- und der Inkrementierungsphase der Anforderungszähler zwingen wir den Ausführungs-Thread, für 5 Sekunden anzuhalten. Dies hat unmittelbar zur Folge, dass er die Prozessorauslastung verliert, die dann einer anderen Aufgabe zugewiesen wird. Nehmen wir an, dass Client A als Erster an der Reihe ist. Er liest den Wert N des Zählers und wird für 5 Sekunden unterbrochen. Wenn während dieser Zeit Client B über den Prozessor verfügt, sollte er denselben Wert N des Zählers lesen. Letztendlich müssten beide Clients denselben Zählerwert anzeigen, was nicht normal wäre.

Wir testen die Anwendung, indem wir die vier oben genannten Dateien in einen Ordner namens <application-path> legen und den Cassini-Server mit den Parametern (<application-path>,/aspnet/webapplib) starten. Wir richten zwei verschiedene Browser mit der URL [http://localhost/aspnet/webapplib/main.aspx] ein. Wir starten den ersten Browser, damit er die Seite URL aufruft, und starten dann, ohne auf die Antwort zu warten, die erst 5 Sekunden später eintreffen wird, den zweiten Browser. Nach etwas mehr als 5 Sekunden erhalten wir das folgende Ergebnis:

Image

Man sieht:

  • dass es sich um zwei verschiedene Clients handelt (nicht dasselbe Session-Token)
  • dass jeder Client eine Anfrage gestellt hat
  • dass der Zähler für die an die Anwendung gesendeten Anfragen in einem der beiden Browser daher bei 2 stehen müsste. Das ist jedoch nicht der Fall.

Machen wir nun ein weiteres Experiment. Mit demselben Browser senden wir fünf Anfragen an die URL [http://localhost/aspnet/webapplib/main.aspx]. Auch hier senden wir sie nacheinander, ohne auf die Ergebnisse zu warten. Wenn alle Anfragen ausgeführt wurden, erhalten wir für die letzte das folgende Ergebnis:

Image

Man kann feststellen:

  • dass die 5 Anfragen als vom selben Client stammend gewertet wurden, da der Zähler für Client-Anfragen bei 5 steht. Obwohl oben nicht dargestellt, lässt sich feststellen, dass das Session-Token für alle 5 Anfragen tatsächlich identisch ist.
  • dass der Zähler für die an die Anwendung gerichteten Anfragen korrekt ist.

Was lässt sich daraus schließen? Nichts Definitives. Möglicherweise beginnt der Webserver nicht mit der Ausführung einer Anfrage eines Kunden, wenn dieser bereits eine Anfrage in Bearbeitung hat? Es käme also niemals zu einer gleichzeitigen Ausführung von Anfragen desselben Kunden. Sie würden nacheinander ausgeführt werden. Dieser Punkt muss überprüft werden. Er kann in der Tat vom verwendeten Kundentyp abhängen.

4.1.3.6. Synchronisation der Clients

Das in der vorherigen Anwendung aufgezeigte Problem ist ein klassisches (aber nicht einfach zu lösendes) Problem des exklusiven Zugriffs auf eine Ressource. In unserem speziellen Fall muss sichergestellt werden, dass sich die beiden Clients A und B nicht gleichzeitig in der folgenden Code-Sequenz befinden können:

  1. Zählerstand ablesen
  2. Inkrementierung des Zählers
  3. Zähler zurücksetzen

Eine solche Code-Sequenz wird als kritische Sequenz bezeichnet. Sie erfordert eine Synchronisation der Threads, die sie gleichzeitig ausführen sollen. Die Plattform .NET bietet verschiedene Werkzeuge, um dies zu gewährleisten. Wir werden hier die Klasse [Mutex] verwenden.

Image

Wir werden hier nur die folgenden Konstruktoren und Methoden verwenden:

public Mutex()
erstellt ein M-Synchronisationsobjekt
public bool WaitOne()
Der Thread T1, der die Operation M.WaitOne() ausführt, fordert die Eigentumsrechte am Synchronisationsobjekt M an. Wenn der Mutex M von keinem Thread gehalten wird (was zu Beginn der Fall ist), wird er dem Thread T1, der ihn angefordert hat, „übergeben“. Wenn wenig später ein Thread T2 denselben Vorgang ausführt, wird er blockiert. Ein Mutex kann nämlich nur einem Thread gehören. Er wird freigegeben, sobald der Thread T1 den von ihm gehaltenen Mutex M freigibt. So können mehrere Threads in Erwartung des Mutex M blockiert sein.
public void ReleaseMutex()
Der Thread T1, der die Operation M.ReleaseMutex() ausführt, gibt den Besitz des Mutex M ab. Wenn der Thread T1 die Prozessorausführung verliert, kann das System diese an einen der Threads vergeben, die auf den Mutex M warten. Nur einer wird ihn an der Reihe erhalten, während die anderen, die auf M warten, weiterhin blockiert bleiben

Ein Mutex M verwaltet den Zugriff auf eine gemeinsam genutzte Ressource R. Ein Thread fordert die Ressource R über M.WaitOne() an und gibt sie über M.ReleaseMutex() zurück. Ein kritischer Codeabschnitt, der jeweils nur von einem einzigen Thread ausgeführt werden darf, ist eine gemeinsam genutzte Ressource. Die Synchronisation der Ausführung des kritischen Abschnitts kann wie folgt erfolgen:

M.WaitOne()
' Nur dieser Thread darf hier eintreten
' kritischer Abschnitt
....
M.ReleaseMutex()

wobei M ein Objekt vom Typ Mutex ist. Man darf natürlich niemals vergessen, ein nicht mehr benötigtes Mutex freizugeben, damit ein anderer Thread seinerseits in den kritischen Abschnitt eintreten kann; andernfalls erhalten die Threads, die auf einen nie freigegebenen Mutex warten, niemals Zugriff auf den Prozessor. Außerdem muss eine Deadlock-Situation (deadlock) vermieden werden, in der zwei Threads gegenseitig aufeinander warten. Betrachten wir die folgenden, zeitlich aufeinanderfolgenden Aktionen:

  • Ein Thread T1 erlangt die Kontrolle über einen Mutex M1, um Zugriff auf eine gemeinsam genutzte Ressource R1 zu erhalten
  • Ein Thread T2 erlangt die Kontrolle über einen Mutex M2, um auf eine gemeinsam genutzte Ressource R2 zugreifen zu können
  • Der Thread T1 fordert den Mutex M2 an. Er wird blockiert.
  • Der Thread T2 fordert den Mutex M1 an. Er wird blockiert.

Hier warten die Threads T1 und T2 gegenseitig aufeinander. Dieser Fall tritt auf, wenn Threads zwei gemeinsam genutzte Ressourcen benötigen: die Ressource R1, die durch den Mutex M1 gesteuert wird, und die Ressource R2, die durch den Mutex M2 gesteuert wird. Eine mögliche Lösung besteht darin, beide Ressourcen gleichzeitig mithilfe eines einzigen Mutex M anzufordern. Dies ist jedoch nicht immer möglich, insbesondere wenn dies zu einer langen Belegung einer ressourcenintensiven Ressource führt. Eine andere Lösung besteht darin, dass ein Thread, der über M1 verfügt und M2 nicht erhalten kann, daraufhin M1 freigibt, um eine gegenseitige Blockade zu vermeiden.

Wenn wir das soeben Gelernte in die Praxis umsetzen, sieht unsere Anwendung wie folgt aus:

  • Die Dateien [global.asax] und [main.aspx] ändern sich nicht
  • Die Datei [global.asax.vb] sieht nun wie folgt aus:

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)
        ' Wird beim Start der Anwendung ausgelöst
        ' Kundenzähler initialisieren
        Application.Item("nbRequêtes") = 0
        ' Erstellung einer Synchronisationssperre
        Application.Item("verrou") = New Mutex
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Wird ausgelöst, wenn die Sitzung gestartet wird
        ' Anfragezähler initialisieren
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

Die einzige Neuerung ist die Erstellung eines [Mutex], das von den Clients zur Synchronisierung verwendet wird. Da es für alle Clients zugänglich sein muss, wird es im Objekt [Application] abgelegt.

  • Die Datei [main.aspx.vb] sieht nun wie folgt aus:

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
        ' Eine weitere Anfrage für die Anwendung und die Sitzung
        ' Eintritt in einen kritischen Abschnitt – Abruf der Synchronisationssperre
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' Es wird beantragt, den folgenden kritischen Abschnitt allein zu betreten
        verrou.WaitOne()
        ' Zähler auslesen
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' 5 Sekunden warten
        Thread.Sleep(5000)
        ' Inkrementierung der Zählerstände
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' Speichern der Zähler
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' Zugriff auf den kritischen Abschnitt wird freigegeben
        verrou.ReleaseMutex()
        ' Anzeigevariablen initialisieren
        jeton = Session.SessionID
    End Sub
End Class

Man sieht, dass der Client:

  • den alleinigen Zugriff auf den kritischen Abschnitt anfordert. Dazu fordert er die exklusive Kontrolle über den Mutex [verrou] an
  • er gibt den Mutex [verrou] am Ende des kritischen Abschnitts frei, damit ein anderer Client seinerseits den kritischen Abschnitt betreten kann.

Wir testen die Anwendung, indem wir die vier oben genannten Dateien in einen Ordner namens <application-path> legen und den Cassini-Server mit den Parametern (<application-path>,/aspnet/webapplic) starten. Wir richten zwei verschiedene Browser mit der URL [http://localhost/aspnet/webapplic/main.aspx] ein. Wir starten den ersten Browser, damit er die Seite URL aufruft, und starten dann, ohne auf die Antwort zu warten, die erst 5 Sekunden später eintrifft, den zweiten Browser. Nach etwas mehr als 5 Sekunden erhalten wir das folgende Ergebnis:

Image

Diesmal ist der Anforderungszähler der Anwendung korrekt.

Aus dieser ausführlichen Demonstration lässt sich ableiten, dass es unbedingt notwendig ist, die Clients einer Webanwendung zu synchronisieren, wenn sie Elemente aktualisieren sollen, die von allen Clients gemeinsam genutzt werden.

4.1.3.7. Verwaltung des Session-Tokens

Wir haben bereits mehrfach über das Session-Token gesprochen, das zwischen dem Client und dem Webserver ausgetauscht wird. Hier noch einmal das Prinzip:

  • Der Client sendet eine erste Anfrage an den Server. Er übermittelt dabei kein Session-Token.
  • Da das Session-Token in der Anfrage fehlt, erkennt der Server einen neuen Client und weist ihm ein Token zu. Mit diesem Token ist außerdem ein Objekt mit der Bezeichnung [Session] verknüpft, das zur Speicherung client-spezifischer Informationen verwendet wird. Das Token begleitet alle Anfragen dieses Kunden. Es wird in die Header HTTP der Antwort auf die erste Anfrage des Kunden aufgenommen.
  • Der Client kennt nun sein Session-Token. Er sendet es in den HTTP-Headern jeder seiner folgenden Anfragen an den Webserver zurück. Mithilfe des Tokens kann der Server das dem Client zugeordnete Objekt [Session] wiederfinden.

Um diesen Mechanismus zu veranschaulichen, greifen wir auf die vorherige Anwendung zurück und ändern dabei lediglich die Datei [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
        ' eine weitere Anfrage für die Anwendung und die Sitzung
        ' Eintritt in einen kritischen Abschnitt – Abruf der Synchronisationssperre
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' Es wird beantragt, den folgenden Abschnitt allein zu betreten
        verrou.WaitOne()
        ' Zähler auslesen
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' 5 Sekunden warten
        Thread.Sleep(5000)
        ' Inkrementierung der Zähler
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' Speichern der Zähler
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' Zugriff auf den kritischen Abschnitt wird gewährt
        verrou.ReleaseMutex()
        ' Anzeigevariablen initialisieren
        jeton = Session.SessionID
    End Sub

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        ' Speichern der Client-Anfrage in request.txt im Anwendungsordner
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub
End Class

Wenn das Ereignis [Page_Init] auftritt, speichern wir die Anfrage des Clients im Anwendungsordner. Hier noch einmal zur Erinnerung:

  • [TemplateSourceDirectory] steht für den virtuellen Pfad der aktuell ausgeführten Seite,
  • MapPath (TemplateSourceDirectory) steht für den entsprechenden physischen Pfad. Damit können wir den physischen Pfad der zu erstellenden Datei zusammenstellen,
  • [Request] ist ein Objekt, das die gerade verarbeitete Anfrage darstellt. Dieses Objekt wurde unter Verwendung der vom Client gesendeten Rohanfrage c.a.d erstellt. Eine Folge von Textzeilen der Form:

Image

  • Request.Save([FileName]) speichert die gesamte Anfrage des Kunden (Header HTTP und gegebenenfalls das nachfolgende Dokument) in einer Datei, deren Pfad als Parameter übergeben wird.

So können wir genau nachvollziehen, wie die Anfrage des Clients lautete. Wir testen die Anwendung, indem wir die vier oben genannten Dateien in einen Ordner namens <application-path> legen und den Cassini-Server mit den Parametern (<application-path>,/aspnet/session1) starten. Anschließend rufen wir mit einem Browser die Datei URL

[http://localhost/aspnet/session1/main.aspx] ab. Wir erhalten das folgende Ergebnis:

Image

Wir verwenden die Datei [request.txt], die von [main.aspx.vb] gespeichert wurde, um Zugriff auf die Browseranfrage zu erhalten:

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

Wir stellen fest, dass der Browser die Anfrage nach URL und [/aspnet/session1/main.aspx] gestellt und weitere Informationen gesendet hat, die wir bereits im vorigen Kapitel besprochen haben. Ein Session-Token ist darin nicht zu erkennen. Die als Antwort empfangene Seite zeigt hingegen, dass der Server ein Session-Token erstellt hat. Es ist noch nicht bekannt, ob der Browser dieses erhalten hat. Führen wir nun eine zweite Anfrage mit demselben Browser durch (Reload). Wir erhalten die folgende neue Antwort:

Image

Es findet tatsächlich eine Sitzungsverfolgung statt, da die Anzahl der Anfragen der Sitzung korrekt erhöht wurde. Sehen wir uns nun den Inhalt der Datei [request.txt] an:

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

Es ist festzustellen, dass der Browser bei dieser zweiten Anfrage einen neuen Header HTTP [Cookie:] an den Server gesendet hat, der eine Information namens [ASP.NET_SessionId] definiert und dessen Wert das Session-Token ist, das in der Antwort auf die erste Anfrage erschienen ist. Mithilfe dieses Tokens ordnet der Webserver diese neue Anfrage dem Objekt [Session] zu, das durch das Token [y153tk45sise0lrhdzrf22m3] identifiziert wird, und ruft den zugehörigen Anfragenzähler ab.

Es ist nach wie vor unklar, über welchen Mechanismus der Server das Token an den Client gesendet hat, da wir keinen Zugriff auf die Antwort HTTP des Servers haben. Zur Erinnerung: Diese hat dieselbe Struktur wie die Anfrage des Clients, nämlich eine Reihe von Textzeilen der Form:

Image

Wir hatten bereits Gelegenheit, einen Web-Client zu verwenden, der uns Zugriff auf die Antwort „HTTP“ des Webservers ermöglichte: den Client „curl“. Wir verwenden ihn erneut in einem DOS-Fenster, um dieselbe URL wie der vorherige Browser abzufragen:

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>

Wir haben die Antwort auf unsere Frage. Der Webserver sendet das Session-Token in Form eines Headers „HTTP [Set-Cookie:]“:

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

Führen wir dieselbe Anfrage durch, ohne das Session-Token zurückzusenden. Wir erhalten folgende Antwort:

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>

Da wir das Sitzungstoken nicht zurückgesendet haben, konnte der Server uns nicht identifizieren und hat uns ein neues Token zugewiesen. Um eine begonnene Sitzung fortzusetzen, muss der Client das empfangene Sitzungstoken an den Server zurücksenden. Wir werden dies hier mithilfe der Option [--cookie clé=valeur] von curl tun, die den Header HTTP [Cookie: clé=valeur] generiert. Wir haben gesehen, dass der Browser diesen Header „HTTP“ bei seiner zweiten Anfrage gesendet hat.

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>

Dabei fallen mehrere Dinge auf:

  • Der Zähler für Client-Anfragen wurde tatsächlich erhöht, was zeigt, dass der Server unser Token erkannt hat.
  • Das von der Seite angezeigte Sitzungstoken ist tatsächlich dasjenige, das wir gesendet haben
  • Das Session-Token ist nicht mehr in den vom Webserver gesendeten Headern „HTTP“ enthalten. Tatsächlich sendet der Webserver dieses Token nur einmal: bei der Generierung des Tokens zu Beginn einer neuen Sitzung. Sobald der Client sein Token erhalten hat, ist es seine Aufgabe, es nach Belieben zu verwenden, um sich identifizieren zu lassen.

Nichts hindert einen Client daran, mit mehreren Sitzungstoken zu arbeiten, wie das folgende Beispiel mit [curl] zeigt, in dem wir das bei unserer ersten Anfrage (Anfrage Nr. 1) erhaltene Token verwenden:

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>

Was bedeutet dieses Beispiel? Wir haben ein zuvor erhaltenes Token gesendet. Wenn der Webserver ein Token erstellt, behält er es so lange bei, wie der diesem Token zugeordnete Client weiterhin Anfragen an ihn sendet. Nach einer bestimmten Zeit der Inaktivität (standardmäßig 20 Minuten bei IIS) wird das Token gelöscht. Das vorangegangene Beispiel zeigt, dass wir ein noch aktives Token verwendet haben.

Vielleicht interessiert es Sie, welche Anfragen der Client [curl] während all dieser Vorgänge gesendet hat. Wir wissen, dass diese in der Datei [request.txt] gespeichert wurden. Hier ist die letzte:

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

Darin findet sich tatsächlich der Header „HTTP“, der das Session-Token übermittelt.

Die vom Server über die Header HTTP und [Set-Cookie:] übermittelten Informationen werden als Cookies bezeichnet. Der Server kann diesen Mechanismus nutzen, um neben dem Session-Token auch andere Informationen zu übermitteln. Wenn der Server S ein Cookie an einen Client übermittelt, gibt er dabei auch dessen Lebensdauer D und den zugehörigen Wert U an. Für den Client bedeutet dies: Wenn er vom Server S eine URL der Form /U/Pfad anfordert, kann er das Cookie zurücksenden, sofern er dieses seit mehr als D nicht mehr erhalten hat. Nichts hindert einen Client daran, diesen Verhaltenskodex nicht einzuhalten. Die Browser hingegen halten sich daran. Einige Browser gewähren Zugriff auf den Inhalt der Cookies, die sie erhalten. Dies ist beispielsweise beim Mozilla-Browser der Fall. Hier sind zum Beispiel die Informationen zu dem Cookie, das der Server in einem vorherigen Beispiel gesendet hat:

Image

Darin finden sich:

  • den Namen des Cookies [ASP.NET_SessionId]
  • seinen Wert: [y153...m3]
  • den Rechner, dem es zugeordnet ist: [localhost]
  • die URL, mit der es verknüpft ist: [/]
  • seine Gültigkeitsdauer: [at end of session]

Der Browser sendet das Sitzungstoken daher jedes Mal, wenn er eine URL im Format URL anfordert, z. B. [http://localhost/...] oder c.a.d. Jedes Mal, wenn er eine URL vom Webserver des Rechners [localhost] anfordert. Die Lebensdauer des Cookies entspricht der Dauer der Sitzung. Für den Browser bedeutet dies, dass das Cookie niemals abläuft. Er sendet es jedes Mal mit, wenn er eine URL vom Rechner [localhost] anfordert. Wenn der Browser also am Tag T das Sitzungstoken erhält, geschlossen und am nächsten Tag wieder verwendet wird, sendet er das Sitzungstoken (das in einer Datei gespeichert wurde) erneut. Der Server empfängt dieses Token, über das er selbst nicht mehr verfügt, da ein Sitzungstoken auf dem Server nur eine begrenzte Lebensdauer hat (20 Minuten auf IIS). Daher startet er eine neue Sitzung.

Es ist möglich, die Verwendung von Cookies in einem Browser zu deaktivieren. In diesem Fall erhält der Client zwar das Session-Token, sendet es jedoch nicht zurück, was die Nachverfolgung der Sitzung verhindert. Um dies zu veranschaulichen, deaktivieren wir die Verwendung von Cookies in unserem Browser (hier Mozilla):

Image

Außerdem löschen wir alle vorhandenen Cookies:

Image

Anschließend starten wir den Cassini-Server neu, um von vorne zu beginnen, und rufen mit dem Browser erneut die URL [http://localhost/aspnet/session1/main.aspx] auf:

Image

Schauen wir mal, ob unser Browser ein Cookie gespeichert hat:

Image

Wir stellen fest, dass der Browser das vom Server gesendete Session-Token-Cookie nicht gespeichert hat. Es ist daher zu erwarten, dass keine Sitzungsverfolgung stattfindet. Wir rufen dieselbe URL erneut auf (Reload):

Image

Das Ergebnis entspricht den Erwartungen. Der Browser hat das Session-Token nicht zurückgesendet, obwohl er es erhalten, aber nicht gespeichert hatte. Der Server hat daher eine neue Sitzung mit einem neuen Token gestartet. Aus diesem Beispiel lässt sich ableiten, dass unsere Richtlinien zur Sitzungsverfolgung untergraben werden, wenn der Nutzer die Verwendung von Cookies in seinem Browser deaktiviert hat. Es gibt jedoch neben Cookies noch eine weitere Möglichkeit, das Sitzungstoken zwischen Server und Client auszutauschen. Es ist nämlich möglich, dem Webserver mitzuteilen, dass die Anwendung ohne Cookies arbeitet. Dies geschieht über die Konfigurationsdatei [web.config]:


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

Die obige Konfigurationsdatei gibt an, dass die Anwendung ohne Cookies arbeitet (cookieless="true") und dass die maximale Inaktivitätsdauer eines Sitzungstokens 10 Minuten beträgt (timeout="10"). Nach Ablauf dieser Zeit wird die mit dem Token verbundene Sitzung beendet. Der Austausch des Sitzungstokens zwischen Server und Client erfolgt wie folgt:

  1. Der Client fordert die URL [http://machine:port/V/chemin] an, wobei V ein virtueller Ordner des Webservers ist
  2. Der Server generiert ein Token J und weist den Client an, sich zur URL [http://machine:port/V/(J)/chemin] umleiten zu lassen. Er hat das Token also in die abzufragende URL eingefügt, unmittelbar hinter dem virtuellen Verzeichnis V
  3. Der Client folgt dieser Weiterleitung und fordert die neue URL URL [http://machine:port/V/(J)/chemin] an.
  4. Der Server beantwortet diese Anfrage und sendet eine Antwortseite.

Veranschaulichen wir diese verschiedenen Punkte. Wir legen die gesamte vorherige Anwendung in einen neuen Ordner <application-path> ab. In denselben Ordner legen wir die vorherige Datei [web.config] ab. Außerdem ändern wir den Präsentationscode [main.aspx], um einen Link einzufügen:


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

Dieser Link verweist auf die Seite [main.aspx] und entspricht somit der Schaltfläche „Neu laden“ des Browsers. Der Cassini-Server wird mit den Parametern (<application-path>,/session2) gestartet. Wir weichen hier von unserer üblichen Vorgehensweise ab, bei der wir den virtuellen Ordner [/aspnet/XX] notiert hätten. Da das Sitzungstoken in die URL eingefügt wird, darf der virtuelle Ordner nämlich nur ein Element /XX enthalten. Zunächst verwenden wir den Client [curl], um die URL [http://localhost/session2/main.aspx] abzufragen:

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>

Wir sehen, dass der Server mit dem Header HTTP [HTTP/1.1 302 Found] anstelle von [HTTP/1.1 200 OK] antwortet. Dieser Header weist den Client an, sich zu der im Header „HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]“ angegebenen URL umzuleiten. Man sieht das Session-Token, das in die Weiterleitungs-URL eingefügt wurde. Ein Browser, der diese Antwort erhält, fordert die neue URL an, ohne dass der Benutzer die neue Anfrage bemerkt. Für den Fall, dass der Browser die Weiterleitung nicht selbstständig verarbeitet, wird hinter dem oben genannten Code HTTP ein Dokument mit dem Namen HTML gesendet. Darin befindet sich ein Link zur Weiterleitungs-URL, auf den der Benutzer klicken kann.

Machen wir nun dasselbe mit einem Browser, bei dem Cookies deaktiviert wurden. Wir fordern erneut die URL [http://localhost/session2/main.aspx] an. Wir erhalten folgende Antwort vom Server:

Image

Zunächst ist festzustellen, dass die vom Browser angezeigte URL nicht diejenige ist, die wir angefordert haben. Dies ist ein Zeichen dafür, dass eine Weiterleitung stattgefunden hat. Tatsächlich zeigt der Browser immer die URL „URL“ des zuletzt empfangenen Dokuments an. Wenn er also nicht die URL „[http://localhost/session2/main.aspx]“ anzeigt, bedeutet dies, dass er angewiesen wurde, zu einer anderen URL umzuleiten. Es können mehrere Weiterleitungen stattgefunden haben. Die vom Browser angezeigte URL ist die URL der letzten Weiterleitung. Wir können feststellen, dass das Session-Token in der vom Browser angezeigten URL enthalten ist. Das lässt sich erkennen, da dieses Token auch von unserem Programm auf der Seite angezeigt wird.

Erinnern wir uns an den Code des Links, der auf der Seite platziert wurde:


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

Es handelt sich um einen relativen Link, da er nicht mit dem Zeichen / beginnt, das ihn zu einem absoluten Link machen würde. Relativ zu was? Um diesen Punkt zu verstehen, müssen wir zur URL des aktuell angezeigten Dokuments zurückkehren: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Relative Links, die in diesem Dokument vorkommen, beziehen sich auf den Pfad [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Somit entspricht unser obiger Link dem folgenden Link:


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

Das zeigt uns der Browser an, wenn wir mit der Maus über den Link fahren:

Image

Wenn wir auf den Link [Recharger l'application] klicken, wird also die URL

[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx] aufgerufen. Der Server erhält somit das Session-Token und kann die damit verbundenen Informationen abrufen. Das zeigt uns die Antwort des Browsers:

Image

Wir sollten uns merken: Wenn wir in einer Webanwendung eine Sitzungsverfolgung durchführen müssen und nicht sicher sind, ob die Browser der Nutzer dieser Anwendung die Verwendung von Cookies zulassen, dann

  • müssen wir die Anwendung so konfigurieren, dass sie ohne Cookies funktioniert
  • müssen die Seiten der Anwendung relative statt absolute Links enthalten

4.2. Informationen aus einer Client-Anfrage abrufen

4.2.1. Der Anfrage-Antwort-Zyklus im Web-Client-Server-Modell

Erinnern wir uns hier an den Client-Server-Kontext einer Webanwendung:

Image

Die Anfrage eines Clients an eine Webanwendung wird wie folgt verarbeitet:

  1. Der Client öffnet eine TCP/IP-Verbindung zu einem Port P des Webdienstes auf dem Rechner M, auf dem die Webanwendung gehostet wird
  2. er sendet über diese Verbindung eine Folge von Textzeilen gemäß dem Protokoll HTTP. Diese Reihe von Zeilen bildet die sogenannte Anfrage des Clients. Sie hat folgende Form:

Image

Sobald die Anfrage gesendet wurde, wartet der Client auf die Antwort.

  1. Die erste Zeile der Header „HTTP“ gibt die vom Webserver angeforderte Aktion an. Sie kann verschiedene Formen annehmen:
    1. GET url HTTP/<version>, wobei <version> derzeit 1.0 oder 1.1 ist. In diesem Fall enthält die Anfrage den Teil [Document] nicht
    2. POST url HTTP/<version>. In diesem Fall enthält die Anfrage einen Teil [Document], meist eine Liste von Informationen, die für die Webanwendung bestimmt sind
    3. PUT url HTTP/<version>. Der Client sendet ein Dokument im Teil [Document] und möchte es auf dem Server unter der URL speichern

Wenn der Client Informationen an die Webanwendung übermitteln möchte, mit der er verbunden ist, stehen ihm im Wesentlichen zwei Möglichkeiten zur Verfügung:

  • (Fortsetzung)
    1. Seine Anfrage lautet [GET url_enrichie HTTP/<version>], wobei url_enrichie die Form [url?param1=val1&param2=val2&...] hat. Der Client übermittelt neben der URL eine Reihe von Informationen in der Form [clé=valeur].
    2. Seine Anfrage lautet [POST url HTTP/<version>]. Im Teil [Document] übermittelt er Informationen in derselben Form wie zuvor: [param1=val1&param2=val2&...].
  1. Auf dem Server hat die gesamte Verarbeitungskette der Client-Anfrage über ein globales Objekt namens „Request“ Zugriff auf diese. Der Webserver hat in diesem Objekt die gesamte Client-Anfrage in einer Form abgelegt, die wir im Folgenden näher betrachten werden. Die aufgerufene Anwendung verarbeitet dieses Objekt und erstellt eine Antwort an den Client. Diese steht in einem globalen Objekt namens „Response“ zur Verfügung. Die Aufgabe der Webanwendung besteht darin, aus dem empfangenen Objekt „[Request]“ ein Objekt „[Response]“ zu erstellen. Die Verarbeitungskette verfügt außerdem über die globalen Objekte „[Application]“ und „[Session]“, die wir bereits erwähnt haben und die es ihr ermöglichen, Daten zwischen verschiedenen Kunden (Anwendung) oder zwischen aufeinanderfolgenden Anfragen desselben Kunden (Sitzung) auszutauschen.
  2. Die Anwendung sendet ihre Antwort über das Objekt [Response] an den Server. Sobald diese im Netzwerk angekommen ist, hat sie die folgende Form: HTTP:

Image

Sobald diese Antwort gesendet wurde, schließt der Server die empfangende Netzwerkverbindung (es sei denn, der Client hat ihm mitgeteilt, dies nicht zu tun).

  1. Der Client erhält die Antwort und schließt seinerseits die Verbindung (beim Senden). Was mit dieser Antwort geschieht, hängt vom Typ des Clients ab. Handelt es sich um einen Browser und ist das empfangene Dokument ein HTML-Dokument, wird dieses angezeigt. Handelt es sich um ein Programm, wird die Antwort analysiert und verarbeitet.
  2. Die Tatsache, dass nach dem Anfrage-Antwort-Zyklus die Verbindung zwischen dem Client und dem Server geschlossen wird, macht das Protokoll HTTP zu einem zustandslosen Protokoll. Bei der nächsten Anfrage baut der Client eine neue Netzwerkverbindung zum selben Server auf. Da es sich nicht mehr um dieselbe Netzwerkverbindung handelt, hat der Server (auf TCP/IP- und HTTP-Ebene) keine Möglichkeit, diese neue Verbindung mit einer früheren zu verknüpfen. Das System der Sitzungs-Token ermöglicht diese Verknüpfung.

4.2.2. Abrufen der vom Client übermittelten Informationen

Wir betrachten nun einige Eigenschaften und Methoden des Objekts [Request], die es dem Anwendungscode ermöglichen, auf die Anfrage des Clients und damit auf die von ihm übermittelten Informationen zuzugreifen. Das Objekt [Request] ist vom Typ [HttpRequest]:

Image

Diese Klasse verfügt über zahlreiche Eigenschaften und Methoden. Uns interessieren die Eigenschaften HttpMethod, QueryString, Form und Params, die uns den Zugriff auf die Elemente der Informationskette [param1=val1&param2=val2&...] ermöglichen.

HttpMethod as String
Client-Abfragemethoden: GET, POST, HEAD, ...
QueryString as NameValueCollection
Zusammenstellung der Elemente der Abfragekette „param1=val1&param2=val2&..“ aus der ersten Zeile HTTP [méthode]?param1=val1&param2=val2&... wobei [méthode] auch GET, POST oder HEAD sein kann.
Form as NameValueCollection
Abfrage der Elemente der Abfragezeichenfolge „param1=val1&param2=val2&...“, die sich im Teil „[Document]“ der Abfrage befinden (Methode POST).
Params as NameValueCollection
Fasst mehrere Sammlungen zusammen: QueryString, Form, ServerVariables, Cookies in einer einzigen Sammlung.

4.2.3. Beispiel 1

Lassen Sie uns diese Elemente anhand eines ersten Beispiels umsetzen. Die Anwendung wird nur ein Element [main.aspx] enthalten. Der Darstellungscode [main.aspx] lautet wie folgt:


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

Die Seite zeigt drei Informationen an: [méthode, nom, age], die von ihrem Controller-Teil [main.aspx.vb] berechnet wurden:

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
         ' Die Anfrage des Clients wird in der Datei „request.txt“ im Anwendungsordner gespeichert
        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
         ' Die Parameter der Anfrage werden abgerufen
        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

Wenn die Seite geladen wird (Form_Load), werden die Informationen aus [nom, age] aus der Client-Anfrage abgerufen. Diese werden in den beiden Sammlungen [QueryString] und [Form] gesucht. . Außerdem speichern wir in [Page_Init] die Client-Anfrage, um überprüfen zu können, was der Client gesendet hat. Wir legen diese beiden Dateien in einem Ordner <application-path> ab und starten den Cassini-Server mit den Parametern (<application-path>,/request1). Anschließend rufen wir mit einem Browser die URL

[http://localhost/request1/main.aspx?nom=tintin&age=27] auf. Wir erhalten folgende Antwort:

Image

Die vom Client übermittelten Informationen wurden korrekt abgerufen. Die in der Datei [request.txt] gespeicherte Browseranfrage lautet wie folgt:

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

Man sieht, dass der Browser eine Anfrage mit der Nummer GET gestellt hat. Um eine Anfrage mit der Nummer POST zu stellen, verwenden wir den Client [curl]. In einem DOS-Fenster geben wir den folgenden Befehl ein:

C:\curl>curl --include --data nom=tintin --data age=27 http://localhost/request1/main.aspx
--include
um die HTTP-Header der Antwort anzuzeigen
--data param=valeur
um die Information „param=Wert“ mittels eines POST zu senden

Die Antwort des Servers lautet wie folgt:

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>

Der Server hat auch diesmal die Parameter erfolgreich abgerufen, die diesmal von einem POST gesendet wurden. Um dies zu überprüfen, kann man den Inhalt der Datei [request.txt] überprüfen:

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

Der Client [curl] hat tatsächlich einen POST erstellt. Nun wollen wir die beiden Methoden der Informationsübermittlung miteinander kombinieren. Wir setzen [age] in die angeforderte URL und [nom] in das gesendete Dokument ein:

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

Die von [curl] gesendete Anfrage lautet wie folgt (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

Man sieht, dass das Alter in die angeforderte URL übernommen wurde. Es ist in der Sammlung [QueryString] zu finden. Der Name wurde hingegen in das an diese URL gesendete Dokument übernommen. Er ist in der Sammlung [Form] zu finden. Die vom Client erhaltene Antwort [curl]:

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

Schließlich senden wir keine Informationen an den Server:

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>

Der Leser wird gebeten, den Code des Controllers [main.aspx.vb] noch einmal durchzulesen, um diese Antwort zu verstehen.

4.2.4. Beispiel 2

Der Client kann mehrere Werte für denselben Schlüssel senden. Was passiert also, wenn im vorherigen Beispiel die URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou] aufgerufen wird, in der der Schlüssel [nom] zweimal vorkommt? Probieren wir es mit einem Browser aus:

Image

Unsere Anwendung hat die beiden Werte, die dem Schlüssel [nom] zugeordnet sind, korrekt abgerufen. Die Anzeige ist etwas irreführend. Sie wurde durch die Anweisung


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

Die Methode [ToString] hat die Zeichenkette [tintin,milou] erzeugt, die angezeigt wurde. Sie verschleiert die Tatsache, dass das Objekt [Request.QueryString("nom")] in Wirklichkeit ein Array von Zeichenketten ist: {"tintin", "milou"}. Das folgende Beispiel verdeutlicht diesen Punkt. Die Darstellungsseite [main.aspx] sieht wie folgt aus:


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

Auf dieser Seite gibt es einige Neuerungen, die sogenannte Server-Steuerelemente verwenden. Diese sind durch das Attribut [runat="server"] gekennzeichnet. Es ist noch zu früh, um den Begriff „Server-Steuerelement“ einzuführen. Es reicht zu wissen, dass hier:

  • dass die Seite zwei Listen enthält (<asp:listbox>-Tags)
  • dass diese Listen Objekte (lstQueryString, lstForm) vom Typ [ListBox] sind, die vom Controller der Seite erstellt werden
  • dass diese Objekte nur auf dem Webserver existieren. Bei der Antwort werden sie in klassische HTML-Tags umgewandelt, die der Client verstehen kann. Ein Objekt vom Typ [listbox] wird somit in die Tags HTML <select> und <option> umgewandelt (man spricht auch von „Rendering“).
  • Der Hauptzweck dieser Objekte besteht darin, den Präsentationscode von jeglichem VB-Code zu befreien, der auf den Controller beschränkt bleibt.

Der Controller [main.aspx.vb], der für die Erstellung der beiden Objekte [lstQueryString] und [lstForm] zuständig ist, sieht wie folgt aus:


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
        ' Die Anfrage des Clients wird in der Datei „request.txt“ im Anwendungsordner gespeichert
        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
        ' Die gesamte Sammlung der Informationen aus QueryString wird abgerufen
        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
        ' Zunächst eine leere Liste mit Informationen
        Dim infos As New ArrayList
        ' Die Schlüssel der Sammlung werden abgerufen
        Dim clés() As String = data.AllKeys
        ' Das Schlüsselarray wird durchlaufen
        Dim valeurs() As String
        For Each clé As String In clés
            ' dem Schlüssel zugeordnete Werte
            valeurs = data.GetValues(clé)
            ' Nur ein Wert?
            If valeurs.Length = 1 Then
                infos.Add(clé + "=" + valeurs(0))
            Else
                ' mehrere Werte
                For ivalue As Integer = 0 To valeurs.Length - 1
                    infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
                Next
            End If
        Next
        ' das Ergebnis wird zurückgegeben
        Return infos
    End Function
End Class

Die wichtigsten Punkte dieses Codes sind folgende:

  • In [Form_Load] ruft die Seite die beiden Sammlungen [QueryString] und [Form] ab. Sie verwendet eine Funktion [getValeurs], um den Inhalt dieser beiden Sammlungen in zwei Objekte vom Typ [ArrayList] zu übertragen, die Zeichenfolgen vom Typ [clé=valeur] enthalten, wenn der Schlüssel der Sammlung einem einzigen Wert zugeordnet ist, oder vom Typ [clé(i)=valeur], wenn der Schlüssel mehreren Werten zugeordnet ist.
  • Jedes der Objekte vom Typ [ArrayList] wird anschließend mithilfe von zwei Anweisungen an eines der Objekte vom Typ [ListBox] der Übersichtsseite angehängt:
    • [ListBox.DataSource=ArrayList] und [ListBox.DataBind]. Die letztgenannte Anweisung überträgt die Elemente von [DataSource] in die Sammlung [Items] des Objekts [ListBox]

Es ist zu beachten, dass keines der beiden Objekte [ListBox] explizit durch eine Operation [New] erstellt wird. Daraus lässt sich ableiten, dass bei Vorhandensein des Tags <asp:listbox id="xx">...<asp:listbox/> der Webserver das Objekt [ListBox], auf das das Attribut [id] des Tags verweist, selbst erstellt.

  • Die Funktion [getValeurs] verarbeitet das Objekt vom Typ [NameValueCollection], das ihr als Parameter übergeben wird, um ein Ergebnis vom Typ [ArrayList] zu erzeugen.

Wir legen die beiden oben genannten Dateien in einem Ordner <application-path> ab und starten den Cassini-Server mit den Parametern (<application-path>,/request2). Anschließend rufen wir die URL

[http://localhost/request2/main.aspx?nom=tintin&age=27] ab. Wir erhalten folgende Antwort:

Image

Nun rufen wir eine URL auf, in der der Schlüssel [nom] zweimal vorkommt:

Image

Wir stellen fest, dass das Objekt [Request.QueryString("name")) tatsächlich ein Array war. Hier wurden die Anfragen über die Methode GET gestellt. Wir verwenden den Client [curl], um eine Anfrage POST zu stellen:

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>

Man sieht, dass der Client für beide Listen auf der Seite den klassischen Code HTML erhält. Es erscheinen Informationen, die wir nicht selbst eingefügt haben, wie beispielsweise das versteckte Feld [_VIEWSTATE]. Diese Informationen wurden durch die Tags <asp:xx runat="server"> generiert. Wir müssen lernen, diese zu beherrschen.

4.3. Implementierung einer MVC-Architektur

4.3.1. Das Konzept

Beenden wir dieses lange Kapitel mit der Umsetzung einer Anwendung, die nach dem MVC-Modell (Model-View-Controller) aufgebaut ist. Eine solche nach diesem Modell strukturierte Webanwendung sieht wie folgt aus:

Image

  • Der Client richtet seine Anfragen an eine bestimmte Komponente der Anwendung, den sogenannten Controller
  • Der Controller analysiert die Anfrage des Clients und lässt sie ausführen. Dabei wird er von Klassen unterstützt, die die Geschäftslogik der Anwendung bündeln, sowie von Klassen für den Datenzugriff.
  • Je nach Ergebnis der Anfrageausführung entscheidet der Controller, eine bestimmte Seite als Antwort an den Client zu senden

In unserem Modell laufen alle Anfragen über einen einzigen Controller, der als Koordinator der gesamten Webanwendung fungiert. Der Vorteil dieses Modells besteht darin, dass im Controller alles zusammengefasst werden kann, was vor jeder Anfrage erledigt werden muss. Nehmen wir zum Beispiel an, die Anwendung erfordert eine Authentifizierung. Diese wird nur einmal durchgeführt. Nach erfolgreicher Authentifizierung speichert die Anwendung Informationen über den gerade authentifizierten Benutzer in der Sitzung. Da ein Client eine Seite der Anwendung direkt aufrufen kann, ohne sich zu authentifizieren, muss jede Seite daher in der Sitzung überprüfen, ob die Authentifizierung tatsächlich erfolgt ist. Wenn alle Anfragen über einen einzigen Controller laufen, kann dieser diese Aufgabe übernehmen. Die Seiten, an die die Anfrage möglicherweise weitergeleitet wird, müssen dies nicht tun.

4.3.2. Steuerung einer Anwendung „MVC“ ohne Sitzung

Nach dem, was wir bisher gesehen haben, könnte man annehmen, dass die Datei [global.asax] die Rolle des Controllers übernehmen könnte. Tatsächlich wissen wir, dass alle Anfragen über sie laufen. Sie ist daher gut geeignet, alles zu kontrollieren. Die folgende Anwendung nutzt sie zu diesem Zweck. Ihr virtueller Pfad lautet [http://localhost/mvc1/main.aspx]. Um anzugeben, was er möchte, fügt der Client hinter der URL einen Parameter „action=Wert“ hinzu. Je nach Wert des Parameters „[action]“ leitet der Controller „[global.asax]“ die Anfrage an eine bestimmte Seite weiter:

  1. [main.aspx], wenn der Parameter „action“ nicht definiert ist oder wenn „action=main“ gilt
  2. [action1.aspx], wenn „action“ = „action1“
  3. [inconnu.aspx], wenn „action“ weder unter Fall 1 noch unter Fall 2 fällt

Die Seiten [main.aspx, action1.aspx, inconnu.aspx] zeigen lediglich den Wert von [action] an, der ihre Anzeige ausgelöst hat. Im Folgenden listen wir die acht Dateien dieser Anwendung auf und kommentieren sie, wo nötig:

[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)
        ' die auszuführende Aktion wird abgerufen
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' Die Aktion wird in den Kontext der Anfrage eingefügt
        Context.Items("action") = action
        ' Die Aktion wird ausgeführt
        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

Zu beachten:

  • Wir fangen alle Client-Anfragen in der Prozedur [Application_BeginRequest] ab, die automatisch beim Start jeder neuen Anfrage an die Anwendung ausgeführt wird.
  • In dieser Prozedur haben wir Zugriff auf das Objekt [Request], das die Abbildung der Client-Anfrage HTTP darstellt. Da wir eine URL der Form [http://localhost/mvc1/main.aspx?action=xx] erwarten, suchen wir in der Sammlung [Request.QueryString] nach einem Schlüssel [action]. Ist dieser nicht vorhanden, setzen wir die Aktion standardmäßig auf „main“.
  • Der Wert des Parameters [action] wird in das Objekt [Context] geschrieben. Wie die Objekte [Application, Session, Request, Response, Server] ist auch dieses Objekt global und in jedem Code zugänglich. Dieses Objekt wird von Seite zu Seite weitergegeben, wenn die Anfrage von mehreren Seiten bearbeitet wird, wie es hier der Fall sein wird. Es wird gelöscht, sobald die Antwort an den Client gesendet wurde. Seine Lebensdauer entspricht somit der Dauer der Bearbeitung der Anfrage.
  • Je nach Wert des Parameters [action] wird die Anfrage an die entsprechende Seite weitergeleitet. Dazu wird das globale Objekt [Server] verwendet, das es dank seiner Methode ermöglicht, die aktuelle Anfrage an eine andere Seite zu übertragen. Sein erster Parameter ist der Name der Zielseite, der zweite ein boolescher Wert, der angibt, ob die Sammlungen [QueryString] und [Form] an die Zielseite übergeben werden sollen oder nicht. In diesem Fall lautet die Antwort „Ja“.

Die Dateien [main.aspx] und [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
        ' Die aktuelle Aktion wird abgerufen
        action = Me.Context.Items("action").ToString
    End Sub
End Class

Der Controller [main.aspx.vb] ruft lediglich den Wert des Schlüssels [action] aus dem Kontext ab, wobei dieser Wert vom Präsentationscode angezeigt wird. Hier soll der Übergang des Objekts [Context] zwischen verschiedenen Seiten veranschaulicht werden, die dieselbe Kundenanfrage bearbeiten. Die Seiten [action1.aspx] und [inconnu.aspx] funktionieren analog:

[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
         ' Die laufende Aktion wird abgerufen
        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
         ' Die laufende Aktion wird abgerufen
        action = Me.Context.Items("action").ToString
    End Sub
End Class

Zum Testen werden die oben genannten Dokumente in einem Ordner unter <application-path> abgelegt und Cassini mit den Parametern (<application-path>,/mvc1) gestartet. Wir rufen die URL [http://localhost/mvc1/main.aspx] auf:

Image

Die Anfrage hat keine Parameter gesendet: [action]. Der Controller-Code der Anwendung [global.asax.vb] hat die Seite [main.aspx] ausgeliefert. Nun fordern wir die URL [http://localhost/mvc1/main.aspx?action=action1] an:

Image

Der Code des Anwendungscontrollers [global.asax.vb] hat die Seite [action1.aspx] ausgeliefert. Nun fordern wir die URL [http://localhost/mvc1/main.aspx?action=xx] an:

Image

Die Aktion wurde nicht erkannt, und der Controller [global.asax.vb] hat die Seite [inconnu.aspx] ausgegeben.

4.3.3. Eine Anwendung MVC mit Sitzung steuern

In den meisten Fällen müssen sich die verschiedenen Anfragen eines Clients an eine Anwendung Informationen teilen. Wir haben eine mögliche Lösung für dieses Problem gesehen: Die zu teilenden Informationen werden im Objekt [Session] der Anfrage gespeichert. Dieses Objekt wird tatsächlich von allen Anfragen gemeinsam genutzt und kann Informationen in der Form (Schlüssel, Wert) speichern, wobei der Schlüssel vom Typ [String] ist und der Wert ein beliebiger von [Object] abgeleiteter Typ sein kann.

Im vorangegangenen Beispiel wurden die verschiedenen Seiten, die den verschiedenen Aktionen zugeordnet waren, in der Prozedur [Application_BeginRequest] der Datei [global.asax.vb] aufgerufen:


    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Die auszuführende Aktion wird abgerufen
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' Die Aktion wird in den Kontext der Anfrage eingefügt
        Context.Items("action") = action
        ' Die Aktion wird ausgeführt
        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

Es stellt sich heraus, dass im Prozedur [Application_BeginRequest] das Objekt [Session] nicht zugänglich ist. Das Gleiche gilt für die Seite, an die die Ausführung weitergeleitet wird. Daher ist dieses Modell für eine Anwendung mit Sitzung nicht verwendbar. Wir können jede beliebige Seite, zum Beispiel [default.aspx], als Controller einsetzen. Die Dateien [global.asax, global.asax.vb] werden dann entfernt und durch die Dateien [default.aspx, default.aspx.vb] ersetzt:

[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
        ' Die auszuführende Aktion wird abgerufen
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If

        ' Die Aktion wird in den Kontext der Anfrage eingefügt
        Context.Items("action") = action
        ' Die vorherige Aktion wird abgerufen, sofern vorhanden
        Context.Items("actionPrec") = Session.Item("actionPrec")
        If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
        ' Die aktuelle Aktion wird in der Sitzung gespeichert
        Session.Item("actionPrec") = action

        ' Die Aktion wird ausgeführt
        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

Um den Sitzungsmechanismus zu veranschaulichen, zeigen die verschiedenen Seiten neben der aktuellen Aktion auch die vorhergehende Aktion an. Bei einer Abfolge von Aktionen A1, A2, ..., An wird, wenn die Aktion Ai auftritt, der oben genannte Controller:

  • die aktuelle Aktion Ai in den Kontext einfügt
  • sucht in der Sitzung die vorhergehende Aktion Ai-1. Falls keine vorhanden ist (wie bei der Aktion A1), wird die leere Zeichenkette als vorhergehende Aktion gesetzt.
  • fügt die aktuelle Aktion Ai als Ersatz für Ai-1 in die Sitzung ein
  • leitet die Ausführung an die entsprechende Seite weiter

Die drei Seiten der Anwendung lauten wie folgt:

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

Da alle drei Seiten dieselben Informationen anzeigen ([action, actionPrec]), können sie alle denselben Seiten-Controller haben. Daher wurden sie alle von der Klasse [main] aus der Datei [main.aspx.vb] abgeleitet:


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
        ' Die aktuelle Aktion wird abgerufen
        action = Me.Context.Items("action").ToString
        ' sowie die vorherige Aktion
        actionPrec = Me.Context.Items("actionPrec").ToString
    End Sub
End Class

Der obige Code ruft lediglich die Informationen ab, die vom Controller der Anwendung [default.aspx.vb] in den Kontext gestellt wurden.

Alle diese Dateien befinden sich im Verzeichnis <application-path>, und Cassini wird mit den Parametern (<application-path>,/mvc2) gestartet. Zunächst wird die URL [http://localhost/mvc2] aufgerufen:

Image

Die URL [http://localhost/mvc2] verweist auf einen Ordner. Wir wissen, dass in diesem Fall das Dokument [default.aspx] aus diesem Ordner vom Server zurückgegeben wird, sofern es existiert. Hier wurde keine Aktion angegeben. Daher wurde die Aktion [main] ausgeführt. Kommen wir nun zur Aktion [action1]:

Image

Die aktuelle und die vorherige Aktion wurden korrekt identifiziert. Kommen wir nun zur Aktion [xx]:

Image

4.4. Conclusion

Wir verfügen nun über die Grundelemente, auf denen jede Anwendung ASP.ET aufbaut. Es bleibt jedoch noch ein wichtiger Begriff einzuführen: der des Formulars. Dies ist Gegenstand des folgenden Kapitels.