4. Podstawy tworzenia aplikacji ASP.NET
4.1. Pojęcie aplikacji internetowej ASP.NET
4.1.1. Wprowadzenie
Aplikacja internetowa to aplikacja skupiająca różne dokumenty (HTML, kod .NET, obrazy, dźwięki itp.). Dokumenty te muszą znajdować się w tym samym katalogu głównym, zwanym katalogiem głównym aplikacji internetowej. Z tym katalogiem głównym powiązana jest wirtualna ścieżka serwera internetowego. Znajomy jest nam już pojęcie folderu wirtualnego w przypadku serwera internetowego Cassini. Pojęcie to istnieje również w przypadku serwera internetowego IIS. Istotną różnicą między tymi dwoma serwerami jest to, że w danym momencie serwer IIS może posiadać dowolną liczbę folderów wirtualnych, podczas gdy serwer internetowy Cassini ma tylko jeden – ten, który został określony podczas jego uruchomienia. Oznacza to, że serwer IIS może obsługiwać jednocześnie wiele aplikacji internetowych, podczas gdy serwer Cassini obsługuje tylko jedną naraz. W poprzednich przykładach serwer Cassini był zawsze uruchamiany z parametrami (<webroot>,/aspnet), które kojarzyły folder wirtualny /aspnet z folderem fizycznym <webroot>. Serwer internetowy zawsze więc obsługiwał tę samą aplikację internetową. Nie przeszkodziło nam to jednak w tworzeniu i testowaniu różnych, niezależnych stron w ramach tej jednej aplikacji internetowej. Każda aplikacja internetowa posiada własne zasoby, które znajdują się w jej fizycznym katalogu głównym <webroot>:
- folder [bin], w którym można umieścić wstępnie skompilowane klasy
- plik [global.asax], który pozwala zainicjować całą aplikację internetową, a także środowisko uruchomieniowe każdego z jej użytkowników
- plik [web.config], który pozwala skonfigurować działanie aplikacji
- plik [default.aspx], który pełni rolę punktu wejścia do aplikacji
- ...
Gdy tylko aplikacja korzysta z jednego z tych trzech zasobów, potrzebuje własnej ścieżki fizycznej i wirtualnej. Nie ma bowiem żadnego powodu, aby dwie różne aplikacje internetowe były skonfigurowane w ten sam sposób. Wszystkie nasze poprzednie przykłady mogły zostać umieszczone w tej samej aplikacji (<webroot>,/aspnet), ponieważ nie korzystały z żadnego z wyżej wymienionych zasobów.
Wróćmy do architektury MVC zalecanej na początku tego rozdziału do tworzenia aplikacji internetowej:

Aplikacja internetowa składa się z plików klas (kontroler, klasy biznesowe, klasy dostępu do danych) oraz plików prezentacyjnych (dokumenty HTML, obrazy, dźwięki, arkusze stylów itp.). Wszystkie te pliki zostaną umieszczone w tym samym katalogu głównym, który czasami będziemy nazywać <application-path>. Katalog ten zostanie powiązany z wirtualną ścieżką <application-vpath>. Powiązanie tej wirtualnej ścieżki ze ścieżką fizyczną odbywa się poprzez konfigurację serwera internetowego. Widzieliśmy już, że w przypadku serwera Cassini powiązanie to następuje podczas uruchamiania serwera. Na przykład w oknie wiersza poleceń uruchamiamy Cassini za pomocą polecenia:
W folderze <application-path> znajdziemy, w zależności od potrzeb:
- folder [bin], w którym umieszczamy wstępnie skompilowane klasy (dll)
- plik [global.asax], gdy zajdzie potrzeba przeprowadzenia inicjalizacji – albo podczas uruchamiania aplikacji, albo podczas inicjalizacji sesji użytkownika
- plik [web.config], gdy zajdzie potrzeba skonfigurowania aplikacji
- plik [default.aspx], gdy będziemy potrzebować strony domyślnej w aplikacji
Aby zachować tę koncepcję aplikacji internetowej, wszystkie poniższe przykłady zostaną umieszczone w folderze <application-path> właściwym dla danej aplikacji, z którym powiązany będzie folder wirtualny <application-vpath>, przy czym serwer Cassini zostanie uruchomiony w taki sposób, aby połączyć te dwa parametry.
4.1.2. Konfiguracja aplikacji internetowej
Jeśli <application-path> jest katalogiem głównym aplikacji ASP.NET, do jej konfiguracji można użyć pliku <application-path>\web.config. Plik ten ma format XML. Oto przykład:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Należy zwrócić uwagę, że znaczniki XML rozróżniają wielkość liter. Wszystkie informacje konfiguracyjne muszą znajdować się pomiędzy znacznikami <configuration> i </configuration>. Dostępnych jest wiele sekcji konfiguracyjnych. W niniejszym dokumencie przedstawiamy tylko jedną z nich – sekcję <appSettings>, która umożliwia inicjalizację danych za pomocą znacznika <add>. Składnia tego znacznika jest następująca:
Gdy serwer WWW uruchamia aplikację, sprawdza, czy w <application-path> znajduje się plik o nazwie web.config. Jeśli tak, odczytuje go i zapisuje zawarte w nim informacje w obiekcie typu [ConfigurationSettings], który będzie dostępny na wszystkich stronach aplikacji tak długo, jak długo aplikacja będzie aktywna. Klasa [ConfigurationSettings] posiada metodę statyczną [AppSettings]:

Aby uzyskać wartość klucza C z pliku konfiguracyjnego, należy wpisać ConfigurationSettings.AppSettings("C"). Otrzymujemy ciąg znaków. Aby wykorzystać powyższy plik konfiguracyjny, utwórzmy stronę [default.aspx]. Kod VB z pliku [default.aspx.vb] będzie wyglądał następująco:
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
'pobieranie informacji konfiguracyjnych
nom = ConfigurationSettings.AppSettings("nom")
age = ConfigurationSettings.AppSettings("age")
End Sub
End Class
Widać, że podczas ładowania strony pobierane są wartości parametrów konfiguracyjnych [nom] i [age]. Zostaną one wyświetlone przez kod prezentacyjny [default.aspx]:
<%@ Page src="default.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="_default" %>
<html>
<head>
<title>Configuration</title>
</head>
<body>
Nom :
<% =nom %><br/>
Age :
<% =age %><br/>
</body>
</html>
W celu przeprowadzenia testu umieszczamy pliki [web.config], [default.aspx] i [default.aspx.vb] w tym samym folderze:
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
Niech <application-path> będzie folderem, w którym znajdują się trzy pliki aplikacji. Serwer Cassini uruchamia się z parametrami (<application-path>,/aspnet/config1). Żądamy plików URL i [http://localhost/aspnet/config1]. Ponieważ [config1] jest katalogiem, serwer WWW wyszuka w nim plik [default.aspx] i wyświetli go, jeśli go znajdzie. W tym przypadku plik zostanie znaleziony:

4.1.3. Aplikacja, sesja, kontekst
4.1.3.1. Plik global.asax
Kod zawarty w pliku [global.asax] jest zawsze wykonywany przed załadowaniem strony żądanej w bieżącym żądaniu. Musi on znajdować się w katalogu głównym <application-path> aplikacji. Jeśli plik [global.asax] istnieje, serwer WWW wykorzystuje go w różnych momentach:
- podczas uruchamiania lub zamykania aplikacji internetowej
- gdy rozpoczyna się lub kończy sesja użytkownika
- gdy rozpoczyna się żądanie użytkownika
Podobnie jak w przypadku stron .aspx, plik [global.asax] można napisać na różne sposoby, a w szczególności poprzez oddzielenie kodu VB w klasie kontrolera od kodu prezentacji. Jest to domyślny wybór dokonywany przez narzędzie Visual Studio i my również tak postąpimy. Zazwyczaj nie ma potrzeby zajmowania się prezentacją, ponieważ rolę tę pełnią strony .aspx. Zawartość pliku [global.asax] sprowadza się zatem do dyrektywy odwołującej się do pliku zawierającego kod kontrolera:
<%@ Application src="Global.asax.vb" Inherits="Global" %>
Warto zauważyć, że dyrektywa nie brzmi już [Page], lecz [Application]. Kod kontrolera [global.asax.vb], powiązany z tym plikiem i wygenerowany przez narzędzie Visual Studio, wygląda następująco:
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)
' Uruchamia się po uruchomieniu aplikacji
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Uruchamia się po rozpoczęciu sesji
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' Uruchamia się na początku każdego żądania
End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
' Wyzwalane podczas próby uwierzytelnienia użytkownika
End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Wyzwalane jest w momencie wystąpienia błędu
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
' Wyzwalane po zakończeniu sesji
End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
' Występuje po zamknięciu aplikacji
End Sub
End Class
Należy zauważyć, że klasa kontrolera wywodzi się z klasy [HttpApplication]. W cyklu życia aplikacji występuje kilka ważnych zdarzeń. Są one obsługiwane przez procedury, których szkielet przedstawiono powyżej.
- [Application_Start]: przypomnijmy, że aplikacja internetowa jest „zamknięta” w ścieżce wirtualnej. Aplikacja uruchamia się, gdy tylko klient zażąda strony znajdującej się w tej ścieżce wirtualnej. Wówczas wykonywana jest procedura [Application_Start]. Będzie to jedyny raz. W tej procedurze przeprowadzimy wszelkie inicjalizacje niezbędne dla aplikacji, takie jak na przykład tworzenie obiektów, których czas życia pokrywa się z czasem działania aplikacji.
- [Application-End]: jest wykonywana po zakończeniu działania aplikacji. Każdej aplikacji przypisany jest czas bezczynności, konfigurowalny w procedurze [web.config], po upływie którego aplikacja jest uznawana za zakończoną. Decyzję tę podejmuje zatem serwer internetowy na podstawie ustawień aplikacji. Czas bezczynności aplikacji definiuje się jako okres, w którym żaden klient nie wysłał żądania dotyczącego zasobu aplikacji.
- [Session-Start]/[Session_End]: Do każdego klienta przypisana jest sesja, chyba że aplikacja jest skonfigurowana tak, aby nie korzystać z sesji. Klient to nie użytkownik siedzący przed ekranem. Jeśli użytkownik otworzył dwie przeglądarki w celu wysłania zapytania do aplikacji, stanowi on dwóch klientów. Klient jest identyfikowany za pomocą tokenu sesji, który musi dołączać do każdego swojego żądania. Ten token sesji jest unikalnym ciągiem znaków generowanym losowo przez serwer WWW. Dwóch klientów nie może mieć tego samego tokenu sesji. Token ten będzie towarzyszył klientowi w następujący sposób:
- klient wysyłający pierwsze żądanie nie przesyła tokenu sesji. Serwer WWW rozpoznaje ten fakt i przypisuje mu token. Jest to początek sesji i uruchamiana jest procedura [Session_Start]. Dzieje się to tylko raz.
- Klient wysyła kolejne żądania, przekazując token, który go identyfikuje. Dzięki temu serwer WWW może odzyskać informacje powiązane z tym tokenem. Umożliwia to śledzenie kolejnych żądań klienta.
- Aplikacja może udostępnić klientowi formularz zakończenia sesji. W takim przypadku to sam klient żąda zakończenia swojej sesji. Zostanie uruchomiona procedura [Session_End]. Odbędzie się to tylko raz.
- Klient może nigdy sam nie zażądać zakończenia swojej sesji. W takim przypadku, po upływie określonego czasu bezczynności sesji, który również można skonfigurować za pomocą procedury [web.config], sesja zostanie zakończona przez serwer WWW. Zostanie wówczas uruchomiona procedura [Session_End].
- [Application_BeginRequest]: procedura ta jest uruchamiana natychmiast po nadejściu nowego żądania. Jest więc uruchamiana przy każdym żądaniu dowolnego klienta. Jest to dobre miejsce do sprawdzenia żądania przed przekazaniem go do żądanej strony. Można nawet podjąć decyzję o przekierowaniu go na inną stronę.
- [Application_Error]: jest uruchamiana za każdym razem, gdy wystąpi błąd, który nie został wyraźnie obsłużony przez kod kontrolera [global.asax.vb]. W tym miejscu można przekierować żądanie klienta na stronę wyjaśniającą przyczynę błędu.
Jeśli żadne z tych zdarzeń nie wymaga obsługi, plik [global.asax] można pominąć. Tak właśnie postąpiono w pierwszych przykładach tego rozdziału.
4.1.3.2. Przykład 1
Stwórzmy aplikację, aby lepiej zrozumieć trzy etapy: uruchomienie aplikacji, sesji oraz żądania klienta. Plik [global.asax] będzie wyglądał następująco:
Powiązany plik [global.asax.vb] będzie wyglądał następująco:
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)
' Występuje przy uruchomieniu aplikacji
' rejestruje się godzinę
Dim startApplication As String = Date.Now.ToString("T")
' zapisuje się ją w kontekście aplikacji
Application.Item("startApplication") = startApplication
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Uruchamia się po rozpoczęciu sesji
' rejestruje się godzinę
Dim startSession As String = Date.Now.ToString("T")
' umieszcza się ją w sesji
Session.Item("startSession") = startSession
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' rejestruje się godzinę
Dim startRequest As String = Date.Now.ToString("T")
' zapisuje się ją w sesji
Context.Items("startRequest") = startRequest
End Sub
End Class
Najważniejsze elementy kodu to:
- serwer WWW udostępnia klasie [HttpApplication] z klasy [global.asax.vb] pewną liczbę obiektów:
- Aplikacja typu [HttpApplicationState] – reprezentująca aplikację internetową – zapewnia dostęp do słownika obiektów [Application.Item], dostępnego dla wszystkich klientów aplikacji – umożliwia wymianę informacji między różnymi klientami – jednoczesny dostęp wielu klientów do tych samych danych w trybie odczytu/zapisu wymaga synchronizacji klientów.
- Sesja typu [HttpSessionState] – reprezentuje konkretnego klienta – zapewnia dostęp do słownika obiektów [Session.Item], dostępnego dla wszystkich żądań tego klienta – umożliwi zapamiętanie informacji o kliencie, które będzie można odzyskać w trakcie kolejnych żądań tego klienta.
- Żądanie typu [HttpRequest] – reprezentuje bieżące żądanie klienta o numerze HTTP
- Odpowiedź typu [HttpResponse] – odpowiada odpowiedzi HTTP, która jest właśnie generowana przez serwer dla klienta
- Serwer typu [HttpServerUtility] – udostępnia metody pomocnicze, w szczególności służące do przekierowania żądania na stronę inną niż pierwotnie przewidziana.
- Kontekst typu [HttpContext] – obiekt ten jest odtwarzany przy każdym nowym żądaniu, ale jest współdzielony przez wszystkie strony uczestniczące w przetwarzaniu żądania – umożliwia przekazywanie informacji między stronami podczas przetwarzania żądania dzięki słownikowi Items.
- Procedura [Application_Start] rejestruje początek aplikacji w zmiennej przechowywanej w słowniku dostępnym na poziomie aplikacji
- procedura [Session_Start] zapisuje początek sesji w zmiennej przechowywanej w słowniku dostępnym na poziomie sesji
- procedura [Application_BeginRequest] zapisuje początek zapytania w zmiennej przechowywanej w słowniku dostępnym na poziomie zapytania (c.a.d dostępna przez cały czas jego przetwarzania, ale tracona po jego zakończeniu)
Stroną docelową będzie następująca strona [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>
Ta strona prezentacyjna wyświetla wartości obliczone przez jej kontroler [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected startApplication As String
Protected startSession As String
Protected startRequest As String
Protected jeton as String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' pobieramy informacje o aplikacji i sesji
jeton=Session.SessionId
startApplication = Application.Item("startApplication").ToString
startSession = Session.Item("startSession").ToString
startRequest = Context.Items("startRequest").ToString
End Sub
End Class
Kontroler ogranicza się do pobrania trzech informacji umieszczonych odpowiednio w aplikacji, sesji i kontekście przez [global.asax.vb].
Testujemy aplikację w następujący sposób:
- pliki są zgromadzone w jednym folderze <application-path>

- serwer Cassini jest uruchamiany z parametrami (<application-path>,/aspnet/globalasax1)
- pierwszy klient wysyła żądanie o adres URL [http://localhost/aspnet/globalasax1/main.aspx] i otrzymuje następujący wynik:

- ten sam klient wysyła nowe żądanie (opcja „Odśwież” w przeglądarce):

Można zauważyć, że zmieniła się jedynie godzina wysłania żądania. Świadczy to o dwóch rzeczach:
- procedury [Application_Start] i [Session_Start] z [global.asax] nie zostały wykonane podczas drugiego żądania.
- obiekty [Application] i [Session], w których przechowywane były godziny rozpoczęcia działania aplikacji i sesji, są nadal dostępne dla drugiego żądania.
- uruchamiamy drugą przeglądarkę, aby utworzyć drugiego klienta, i ponownie wysyłamy żądanie do tego samego adresu URL:

Tym razem widzimy, że czas sesji uległ zmianie. Druga przeglądarka, mimo że działa na tym samym komputerze, została uznana za drugiego klienta i utworzono dla niej nową sesję. Można zauważyć, że obaj klienci nie mają tego samego tokenu sesji. Czas rozpoczęcia działania aplikacji nie uległ zmianie, co oznacza, że:
- procedura [Application_Start] wynikająca z [global.asax.vb] nie została wykonana
- obiekt [Application], w którym zapisano czas rozpoczęcia działania aplikacji, jest dostępny dla drugiego klienta. W związku z tym to właśnie w tym obiekcie należy przechowywać informacje, które mają być współdzielone przez różnych klientów aplikacji, natomiast obiekt [Session] służy do przechowywania informacji, które mają być współdzielone przez zapytania tego samego klienta.
4.1.3.3. Przegląd ogólny
Korzystając z dotychczas zdobytej wiedzy, jesteśmy w stanie stworzyć wstępny schemat wyjaśniający działanie serwera WWW i obsługiwanych przez niego aplikacji internetowych:

Powyższy schemat przedstawia serwer obsługujący dwie aplikacje oznaczone jako A i B, z których każda ma dwóch klientów. Serwer WWW może obsługiwać jednocześnie wiele aplikacji internetowych. Aplikacje te są od siebie całkowicie niezależne. Skupimy się na aplikacji A. Przetwarzanie żądania od klienta-1A skierowanego do aplikacji A przebiega w następujący sposób:
- klient 1A zwraca się do serwera WWW z prośbą o zasób należący do domeny aplikacji A. Oznacza to, że żąda on zasobu o nazwie URL w postaci [http://machine:port/VA/ressource], gdzie VA jest ścieżką wirtualną aplikacji A.
- Jeśli serwer WWW wykryje, że jest to pierwsze żądanie zasobu aplikacji A, wyzwala zdarzenie [Application_Start] z pliku [global.asax] aplikacji A. Zostanie utworzony obiekt [ApplicationA] typu [HttpApplicationState]. Różne moduły aplikacji będą przechowywać w tym obiekcie dane o zakresie [Application], c.a.d – dane dotyczące wszystkich użytkowników. Obiekt [ApplicationA] będzie istnieć do momentu, aż serwer WWW zamknie aplikację A.
- jeśli serwer WWW wykryje ponadto, że ma do czynienia z nowym klientem aplikacji A, wywoła zdarzenie [Session_Start] z pliku [global.asax] aplikacji A. Zostanie utworzony obiekt [Session-1A] typu [HttpSessionState]. Obiekt ten umożliwi aplikacji A przechowywanie obiektów o zakresie [Session] i c.a.d, należących do konkretnego klienta. Obiekt [Session-1A] będzie istnieć tak długo, jak długo klient 1A będzie wysyłał żądania. Umożliwi on śledzenie tego klienta. Serwer WWW wykrywa, że ma do czynienia z nowym klientem w dwóch przypadkach:
- klient nie przesłał mu tokenu sesji w nagłówkach HTTP swojego żądania
- klient przesłał mu token sesji, który nie istnieje (nieprawidłowe działanie klienta lub próba włamania) lub który już nie istnieje. Token sesji wygasa bowiem po upływie określonego czasu bezczynności klienta (domyślnie 20 minut w przypadku IIS). Czas ten można ustawić.
- W każdym przypadku serwer WWW wywoła zdarzenie [Application_BeginRequest] z pliku [global.asax]. Zdarzenie to rozpoczyna przetwarzanie żądania klienta. Często zdarza się, że zdarzenie to nie jest przetwarzane, a kontrola przekazywana jest do strony żądanej przez klienta, która sama zajmie się przetworzeniem żądania. Można również wykorzystać to zdarzenie do analizy żądania, jego przetworzenia oraz podjęcia decyzji, która strona powinna zostać wysłana w odpowiedzi. Wykorzystamy tę technikę do wdrożenia aplikacji zgodnej z architekturą MVC, o której wspomnieliśmy.
- Po przejściu przez filtr [global.asax] żądanie klienta jest przekazywane do strony .aspx, która je przetworzy. Później zobaczymy, że możliwe jest przepuszczenie żądania przez filtr składający się z kilku stron. Ostatnia z nich będzie odpowiedzialna za wysłanie odpowiedzi do klienta. Strony mogą uzupełniać pierwotne żądanie klienta o informacje, które same obliczyły. Mogą one przechowywać te informacje w kolekcji Context.Items. Wszystkie strony zaangażowane w przetwarzanie żądania klienta mają bowiem dostęp do tego zbioru danych.
- Kod poszczególnych stron ma dostęp do zbiorów danych, którymi są obiekty [ApplicationA], [Session-1A], ... Należy pamiętać, że serwer WWW obsługuje jednocześnie wielu klientów korzystających z aplikacji A. Wszyscy ci klienci mają dostęp do obiektu [Application A]. Jeśli muszą oni modyfikować dane w tym obiekcie, konieczne jest przeprowadzenie synchronizacji między klientami. Każdy klient XA ma ponadto dostęp do zbioru danych [Session-XA]. Ponieważ jest on dla niego zarezerwowany, nie ma tu potrzeby przeprowadzania synchronizacji.
- Serwer WWW obsługuje jednocześnie kilka aplikacji internetowych. Nie występują żadne zakłócenia między klientami tych różnych aplikacji.
Z powyższych wyjaśnień należy zapamiętać następujące kwestie:
- W danym momencie serwer WWW obsługuje wielu klientów jednocześnie. Oznacza to, że nie czeka on na zakończenie jednego żądania, aby przetworzyć kolejne. W momencie T przetwarzanych jest zatem kilka żądań należących do różnych klientów i dotyczących różnych aplikacji. Kody przetwarzania, które przebiegają jednocześnie na serwerze WWW, nazywane są czasem wątkami wykonawczymi.
- Wątki wykonawcze klientów różnych aplikacji internetowych nie zakłócają się nawzajem. Istnieje izolacja.
- Wątki wykonawcze klientów tej samej aplikacji mogą być zmuszone do współdzielenia danych:
- wątki wykonawcze żądań dwóch różnych klientów (nieposiadających tego samego tokenu sesji) mogą współdzielić dane za pomocą obiektu [Application].
- Wątki wykonawcze kolejnych żądań tego samego klienta mogą współdzielić dane za pomocą obiektu [Session].
- wątki wykonawcze kolejnych stron przetwarzających to samo żądanie od danego klienta mogą współdzielić dane za pomocą obiektu [Context].
4.1.3.4. Przykład 2
Opracujmy nowy przykład ilustrujący to, co właśnie omówiliśmy. W tym samym folderze umieszczamy następujące pliki:
[global.asax]
[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)
' Uruchamia się po uruchomieniu aplikacji
' inicjuje licznik klientów
Application.Item("nbRequêtes") = 0
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' uruchamia się po rozpoczęciu sesji
' inicjuje licznik żądań
Session.Item("nbRequêtes") = 0
End Sub
End Class
Zasadą działania aplikacji będzie zliczanie całkowitej liczby żądań skierowanych do aplikacji oraz liczby żądań na klienta. Po uruchomieniu aplikacji [Application_Start] licznik żądań skierowanych do aplikacji jest zerowany. Licznik ten umieszczony jest w zakresie [Application], ponieważ musi być zwiększany przez wszystkich klientów. Gdy klient po raz pierwszy łączy się z aplikacją [Session_Start], licznik żądań wysłanych przez tego klienta jest zerowany. Licznik ten znajduje się w zakresie [Session], ponieważ dotyczy wyłącznie danego klienta.
Po wykonaniu [global.asax] zostanie uruchomiony następny plik [main.aspx]:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>application-session</title>
</head>
<body>
jeton de session :
<% =jeton %>
<br />
requêtes Application :
<% =nbRequêtesApplication %>
<br />
requêtes Client :
<% =nbRequêtesClient %>
<br />
</body>
</html>
Wyświetla trzy informacje obliczone przez kontroler:
- tożsamość klienta na podstawie jego tokenu sesji: [jeton]
- całkowita liczba żądań wysłanych do aplikacji: [nbRequêtesApplication]
- łączna liczba żądań wysłanych przez klienta zidentyfikowanego w punkcie 1: [nbRequêtesClient]
Te trzy informacje są obliczane w [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As String
Protected nbRequêtesClient As String
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' kolejne żądanie dla aplikacji
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
' kolejne żądanie w sesji
Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
' inicjowanie zmiennych prezentacji
nbRequêtesApplication = Application.Item("nbRequêtes").ToString
jeton = Session.SessionID
nbRequêtesClient = Session.Item("nbRequêtes").ToString
End Sub
End Class
Gdy uruchamiany jest obiekt [main.aspx.vb], przetwarzamy zapytanie od danego klienta. Używamy obiektu [Application] do zwiększania liczby zapytań w aplikacji, a obiektu [Session] do zwiększania liczby zapytań klienta, którego zapytanie jest właśnie przetwarzane. Należy pamiętać, że chociaż wszyscy klienci tej samej aplikacji współużytkują ten sam obiekt [Application], to każdy z nich posiada swój własny obiekt [Session].
Testujemy aplikację, umieszczając cztery powyższe pliki w folderze o nazwie <application-path> i uruchamiając serwer Cassini z parametrami (<application-path>,/aspnet/webapplia). Otwieramy pierwszą przeglądarkę i wpisujemy adres URL [http://localhost/aspnet/webapplia/main.aspx]:

Wysyłamy drugie żądanie, klikając przycisk [Reload]:

Uruchamiamy drugą przeglądarkę, aby wywołać ten sam adres URL. Dla serwera WWW jest to nowy klient:

Można zauważyć, że token sesji uległ zmianie, co oznacza, że mamy do czynienia z nowym klientem. Znajduje to odzwierciedlenie w liczbie żądań wysyłanych przez klienta. Wróćmy teraz do pierwszej przeglądarki i ponownie wywołajmy ten sam adres URL:

Liczba żądań wysłanych do aplikacji jest prawidłowo zliczana.
4.1.3.5. O konieczności synchronizacji klientów aplikacji
W poprzedniej aplikacji licznik żądań wysyłanych do aplikacji jest zwiększany w procedurze [Form_Load] na stronie [main.aspx] w następujący sposób:
' kolejne żądanie dla aplikacji
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
Ta instrukcja, choć prosta, wymaga do wykonania kilku instrukcji procesora. Załóżmy, że potrzeba trzech:
- odczyt licznika
- inkrementacja licznika
- zapis licznika
Serwer WWW działa na komputerze wielozadaniowym, co oznacza, że każde zadanie otrzymuje dostęp do procesora na kilka milisekund, po czym traci go, by odzyskać go ponownie, gdy wszystkie pozostałe zadania również otrzymają swój czas procesora. Załóżmy, że dwóch klientów, A i B, wysyła jednocześnie zapytania do serwera WWW. Załóżmy, że klient A jest pierwszy, trafia do procedury [Form_Load] z [main.aspx.vb], odczytuje licznik (=100), a następnie zostaje przerwany, ponieważ wyczerpał się jego czas procesowy. Załóżmy teraz, że przyszła kolej na klienta B i spotkał go ten sam los: zdążył odczytać wartość licznika (=100), ale nie miał czasu, by ją zwiększyć. Klienci A i B posiadają liczniki o wartości równej 100. Załóżmy, że znów nadchodzi kolej klienta A: zwiększa on wartość swojego licznika do 101, a następnie kończy działanie. Teraz kolej na klienta B, który posiada starą wartość licznika, a nie nową. On również zmienia wartość licznika na 101 i kończy działanie. Wartość licznika żądań aplikacji jest teraz błędna.
Aby zilustrować ten problem, powracamy do poprzedniej aplikacji, którą modyfikujemy w następujący sposób:
- pliki [global.asax], [global.asax.vb] i [main.aspx] pozostają bez zmian
- plik [main.aspx.vb] przyjmuje następującą postać:
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
' kolejne żądanie dla aplikacji i sesji
' odczyt liczników
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' oczekiwanie 5 s
Thread.Sleep(5000)
' zwiększenie wartości liczników
nbRequêtesApplication += 1
nbRequêtesClient += 1
' zapis liczników
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' inicjowanie zmiennych prezentacji
jeton = Session.SessionID
End Sub
End Class
Odczyt liczników został podzielony na cztery etapy:
- odczyt licznika
- przełączenie wątku wykonawczego w stan uśpienia
- inkrementacja licznika
- zapisanie stanu licznika
Rozważmy ponownie naszych dwóch klientów, A i B. Pomiędzy fazą odczytu a fazą zwiększania liczników żądań wymuszamy zatrzymanie wątku wykonawczego na 5 sekund. Bezpośrednim skutkiem tego będzie utrata przez niego dostępu do procesora, który zostanie wówczas przydzielony innemu zadaniu. Załóżmy, że klient A jest pierwszy. Odczyta on wartość N licznika i zostanie przerwany na 5 sekund. Jeśli w tym czasie klient B będzie dysponował procesorem, powinien odczytać tę samą wartość N licznika. Ostatecznie obaj klienci powinni wyświetlić tę samą wartość licznika, co byłoby nieprawidłowe.
Testujemy aplikację, umieszczając cztery powyższe pliki w folderze o nazwie <application-path> i uruchamiając serwer Cassini z parametrami (<application-path>,/aspnet/webapplib). Przygotowujemy dwie różne przeglądarki z adresem URL [http://localhost/aspnet/webapplib/main.aspx]. Uruchamiamy pierwszą przeglądarkę, aby zażądała strony URL, a następnie, nie czekając na odpowiedź, która nadejdzie 5 sekund później, uruchamiamy drugą przeglądarkę. Po nieco ponad 5 sekundach otrzymujemy następujący wynik:

Widać, że:
- że mamy dwóch różnych klientów (nie ten sam token sesji)
- że każdy klient wysłał żądanie
- że licznik żądań wysłanych do aplikacji powinien zatem wynosić 2 w jednej z tych przeglądarek. Tak jednak nie jest.
Teraz przeprowadźmy kolejny eksperyment. W tej samej przeglądarce wysyłamy pięć żądań pod adresem URL [http://localhost/aspnet/webapplib/main.aspx]. Również w tym przypadku wysyłamy je jedno po drugim, nie czekając na wyniki. Po wykonaniu wszystkich żądań otrzymujemy następujący wynik dla ostatniego z nich:

Można zauważyć, że:
- że wszystkie 5 żądań zostało uznanych za pochodzące od tego samego klienta, ponieważ licznik żądań klienta wskazuje wartość 5. Chociaż nie zostało to pokazane powyżej, można zauważyć, że token sesji jest rzeczywiście ten sam dla wszystkich 5 żądań.
- że licznik żądań kierowanych do aplikacji działa poprawnie.
Jakie wnioski można z tego wyciągnąć? Nic ostatecznego. Być może serwer WWW nie rozpoczyna realizacji żądania od klienta, jeśli ten ma już jedno żądanie w trakcie realizacji? W związku z tym nigdy nie dochodziłoby do jednoczesnej realizacji żądań od tego samego klienta. Byłyby one realizowane jedno po drugim. Kwestię tę należy zweryfikować. Może to bowiem zależeć od typu używanego klienta.
4.1.3.6. Synchronizacja klientów
Problem ujawniony w poprzedniej aplikacji jest klasycznym (ale niełatwym do rozwiązania) problemem wyłącznego dostępu do zasobu. W naszym konkretnym przypadku należy zadbać o to, aby dwaj klienci A i B nie mogli jednocześnie znajdować się w sekwencji kodu:
- odczyt licznika
- inkrementacja licznika
- zapis licznika
Taką sekwencję kodu nazywamy sekwencją krytyczną. Wymaga ona synchronizacji wątków, które mają ją wykonywać jednocześnie. Platforma .NET oferuje różne narzędzia zapewniające tę synchronizację. W tym przypadku wykorzystamy klasę [Mutex].

W tym przypadku wykorzystamy wyłącznie następujące konstruktory i metody:
tworzy obiekt synchronizacji M | |
Wątek T1, który wykonuje operację M.WaitOne(), żąda własności obiektu synchronizacji M. Jeśli muteks M nie jest posiadany przez żaden wątek (co ma miejsce na początku), zostaje on „przyznany” wątku T1, który o to poprosił. Jeśli nieco później wątek T2 wykona tę samą operację, zostanie zablokowany. W rzeczywistości muteks może należeć tylko do jednego wątku. Zostanie odblokowany, gdy wątek T1 zwolni muteks M, który posiada. W ten sposób wiele wątków może zostać zablokowanych w oczekiwaniu na muteks M. | |
Wątek T1, który wykonuje operację M.ReleaseMutex(), rezygnuje z posiadania muteksu M. Gdy wątek T1 straci dostęp do procesora, system będzie mógł przydzielić go jednemu z wątków oczekujących na muteks M. Tylko jeden z nich otrzyma go w kolejności, pozostałe wątki oczekujące na muteks M pozostaną zablokowane |
Mutex M zarządza dostępem do zasobu współdzielonego R. Wątek żąda zasobu R za pomocą funkcji M.WaitOne() i zwraca go za pomocą funkcji M.ReleaseMutex(). Sekcja krytyczna kodu, która może być wykonywana tylko przez jeden wątek naraz, stanowi zasób współdzielony. Synchronizacja wykonywania sekcji krytycznej może przebiegać w następujący sposób:
gdzie M jest obiektem typu Mutex. Oczywiście nie wolno zapominać o zwolnieniu obiektu Mutex, który stał się zbędny, aby inny wątek mógł z kolei wejść do sekcji krytycznej; w przeciwnym razie wątki oczekujące na muteks, który nigdy nie został zwolniony, nigdy nie uzyskają dostępu do procesora. Ponadto należy unikać sytuacji wzajemnego blokowania (deadlock), w której dwa wątki czekają na siebie nawzajem. Rozważmy następujące działania, które następują po sobie w czasie:
- wątek T1 uzyskuje własność muteksu M1, aby uzyskać dostęp do zasobu współdzielonego R1
- wątek T2 uzyskuje własność muteksu M2, aby uzyskać dostęp do zasobu współdzielonego R2
- wątek T1 żąda muteksu M2. Zostaje zablokowany.
- Wątek T2 próbuje uzyskać muteks M1. Zostaje zablokowany.
W tym przypadku wątki T1 i T2 oczekują na siebie nawzajem. Sytuacja ta występuje, gdy wątki potrzebują dwóch współdzielonych zasobów: zasobu R1 kontrolowanego przez muteks M1 oraz zasobu R2 kontrolowanego przez muteks M2. Jednym z możliwych rozwiązań jest jednoczesne żądanie obu zasobów za pomocą jednego muteksu M. Nie zawsze jest to jednak możliwe, zwłaszcza jeśli wiąże się to z długotrwałym zajęciem kosztownego zasobu. Innym rozwiązaniem jest sytuacja, w której wątek posiadający zasób M1, który nie może uzyskać zasobu M2, zwalnia wówczas zasób M1, aby uniknąć wzajemnego blokowania.
Jeśli zastosujemy w praktyce to, czego się właśnie nauczyliśmy, nasza aplikacja będzie wyglądać następująco:
- pliki [global.asax] i [main.aspx] pozostają bez zmian
- plik [global.asax.vb] przyjmuje następującą postać:
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)
' Uruchamia się po uruchomieniu aplikacji
' inicjowanie licznika klientów
Application.Item("nbRequêtes") = 0
' utworzenie blokady synchronizacji
Application.Item("verrou") = New Mutex
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Uruchamia się po rozpoczęciu sesji
' inicjowanie licznika żądań
Session.Item("nbRequêtes") = 0
End Sub
End Class
Jedyną nowością jest utworzenie pliku [Mutex], który będzie wykorzystywany przez klientów do synchronizacji. Ponieważ musi on być dostępny dla wszystkich klientów, umieszczono go w obiekcie [Application].
- Plik [main.aspx.vb] przyjmuje następującą postać:
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
' kolejne żądanie dla aplikacji i sesji
' wchodzimy do sekcji krytycznej – pobieramy blokadę synchronizacji
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' wnosimy prośbę o samodzielne wejście do następującej sekcji krytycznej
verrou.WaitOne()
' odczyt liczników
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' oczekiwanie przez 5 s
Thread.Sleep(5000)
' inkrementacja liczników
nbRequêtesApplication += 1
nbRequêtesClient += 1
' zapis stanów liczników
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' zezwalamy na dostęp do sekcji krytycznej
verrou.ReleaseMutex()
' inicjowanie zmiennych prezentacji
jeton = Session.SessionID
End Sub
End Class
Widać, że klient:
- prosi o samodzielne wejście do sekcji krytycznej. W tym celu żąda wyłącznej własności muteksu [verrou]
- zwalnia muteks [verrou] na końcu sekcji krytycznej, aby inny klient mógł z kolei wejść do sekcji krytycznej.
Testujemy aplikację, umieszczając cztery powyższe pliki w folderze o nazwie <application-path> i uruchamiając serwer Cassini z parametrami (<application-path>,/aspnet/webapplic). Przygotowujemy dwie różne przeglądarki z adresem URL [http://localhost/aspnet/webapplic/main.aspx]. Uruchamiamy pierwszą przeglądarkę, aby wysłała żądanie o adres URL, a następnie, nie czekając na odpowiedź, która nadejdzie 5 sekund później, uruchamiamy drugą przeglądarkę. Po nieco ponad 5 sekundach otrzymujemy następujący wynik:

Tym razem licznik żądań aplikacji jest prawidłowy.
Z tej długiej demonstracji wynika, że absolutnie konieczne jest zsynchronizowanie klientów tej samej aplikacji internetowej, jeśli mają oni aktualizować elementy współdzielone przez wszystkich klientów.
4.1.3.7. Zarządzanie tokenem sesji
Wielokrotnie wspominaliśmy o tokenie sesji, który jest wymieniany między klientem a serwerem internetowym. Przypomnijmy jego zasadę działania:
- klient wysyła pierwsze żądanie do serwera. Nie wysyła przy tym tokenu sesji.
- Ze względu na brak tokenu sesji w żądaniu serwer rozpoznaje nowego klienta i przypisuje mu token. Z tym tokenem powiązany jest również obiekt [Session], który będzie służył do przechowywania informacji dotyczących tego konkretnego klienta. Token będzie towarzyszył wszystkim żądaniom tego klienta. Zostanie on dołączony do nagłówków HTTP w odpowiedzi na pierwsze żądanie klienta.
- Klient zna teraz swój token sesji. Będzie go odsyłał w nagłówkach HTTP każdego kolejnego żądania, które skieruje do serwera WWW. Dzięki tokenowi serwer będzie mógł odnaleźć obiekt [Session] powiązany z tym klientem.
Aby zilustrować ten mechanizm, powracamy do poprzedniej aplikacji, zmieniając jedynie plik [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
' kolejne żądanie dla aplikacji i sesji
' wchodzimy do sekcji krytycznej – pobieramy blokadę synchronizacyjną
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' wnosimy prośbę o samodzielne wejście do następnej sekcji
verrou.WaitOne()
' odczyt liczników
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' oczekiwanie przez 5 s
Thread.Sleep(5000)
' zwiększenie wartości liczników
nbRequêtesApplication += 1
nbRequêtesClient += 1
' zapis stanów liczników
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' zezwalamy na dostęp do sekcji krytycznej
verrou.ReleaseMutex()
' inicjowanie zmiennych prezentacji
jeton = Session.SessionID
End Sub
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' zapisywanie żądania klienta w pliku request.txt w katalogu aplikacji
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
End Class
Gdy wystąpi zdarzenie [Page_Init], zapisujemy żądanie klienta w folderze aplikacji. Przypomnijmy kilka kwestii:
- [TemplateSourceDirectory] oznacza wirtualną ścieżkę do aktualnie wykonywanej strony,
- MapPath (TemplateSourceDirectory) oznacza odpowiadającą mu ścieżkę fizyczną. Pozwala nam to skonstruować fizyczną ścieżkę do pliku, który ma zostać utworzony,
- [Request] to obiekt reprezentujący aktualnie przetwarzane żądanie. Obiekt ten został utworzony na podstawie surowego żądania wysłanego przez klienta, c.a.d, czyli ciągu wierszy tekstu o postaci:

- Request.Save([FileName]) zapisuje całe żądanie klienta (nagłówki HTTP i ewentualnie następujący po nich dokument) w pliku, którego ścieżka jest przekazywana jako parametr.
Będziemy więc mogli dokładnie ustalić, jakie było żądanie klienta. Testujemy aplikację, umieszczając cztery powyższe pliki w folderze o nazwie <application-path> i uruchamiamy serwer Cassini z parametrami (<application-path>,/aspnet/session1). Następnie za pomocą przeglądarki wysyłamy żądanie do pliku URL
[http://localhost/aspnet/session1/main.aspx]. Otrzymujemy następujący wynik:

Wykorzystujemy plik [request.txt] zapisany przez [main.aspx.vb], aby uzyskać dostęp do żądania przeglądarki:
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
Zauważamy, że przeglądarka wysłała żądanie dotyczące URL [/aspnet/session1/main.aspx] oraz przesłała inne informacje, o których wspominaliśmy już w poprzednim rozdziale. Nie widać tam tokenu sesji. Strona otrzymana w odpowiedzi pokazuje natomiast, że serwer utworzył token sesji. Nie wiadomo jeszcze, czy przeglądarka go otrzymała. Wyślijmy teraz drugie żądanie za pomocą tej samej przeglądarki (Reload). Otrzymujemy następującą nową odpowiedź:

Sesja jest rzeczywiście śledzona, ponieważ liczba żądań w sesji została poprawnie zwiększona. Przyjrzyjmy się teraz zawartości pliku [request.txt]:
GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0,5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Cookie: ASP.NET_SessionId=y153tk45sise0lrhdzrf22m3
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316
Można zauważyć, że w przypadku tego drugiego żądania przeglądarka wysłała do serwera nowy nagłówek HTTP [Cookie:], definiujący informację o nazwie [ASP.NET_SessionId], której wartością jest token sesji, który pojawił się w odpowiedzi na pierwsze żądanie. Dzięki temu tokenowi serwer WWW powiąże to nowe żądanie z obiektem [Session], zidentyfikowanym przez token [y153tk45sise0lrhdzrf22m3], i odnajdzie powiązany licznik żądań.
Nadal nie wiadomo, za pomocą jakiego mechanizmu serwer wysłał token do klienta, ponieważ nie mamy dostępu do odpowiedzi serwera o numerze HTTP. Przypomnijmy, że ma ona taką samą strukturę jak żądanie klienta, a mianowicie zbiór wierszy tekstu w postaci:

Mieliśmy okazję skorzystać z klienta internetowego, który zapewnił nam dostęp do odpowiedzi serwera HTTP – był to klient curl. Używamy go ponownie, w oknie wiersza poleceń, aby wysłać zapytanie do tego samego adresu URL, co poprzednia przeglądarka:
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>
Mamy odpowiedź na nasze pytanie. Serwer WWW wysyła token sesji w postaci nagłówka HTTP [Set-Cookie:]:
Wykonajmy to samo żądanie bez wysyłania tokenu sesji. Otrzymujemy następującą odpowiedź:
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>
Ponieważ nie odesłaliśmy tokenu sesji, serwer nie mógł nas zidentyfikować i przydzielił nam nowy token. Aby kontynuować rozpoczętą sesję, klient musi odesłać do serwera otrzymany token sesji. Zrobimy to tutaj, korzystając z opcji [--cookie clé=valeur] programu curl, która wygeneruje nagłówek HTTP [Cookie: clé=valeur]. Zauważyliśmy, że przeglądarka wysłała ten nagłówek HTTP podczas swojego drugiego żądania.
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>
Warto zwrócić uwagę na kilka kwestii:
- licznik żądań klienta został prawidłowo zwiększony, co wskazuje, że serwer rozpoznał nasz token.
- token sesji wyświetlany na stronie jest dokładnie tym, który wysłaliśmy
- token sesji nie znajduje się już w nagłówkach HTTP wysyłanych przez serwer WWW. Serwer wysyła go bowiem tylko raz: podczas generowania tokenu przy rozpoczęciu nowej sesji. Gdy klient otrzyma już swój token, to on sam decyduje, kiedy go wykorzystać, aby zostać rozpoznanym.
Nic nie stoi na przeszkodzie, aby klient korzystał z kilku tokenów sesji, co pokazuje poniższy przykład z tokenem [curl], w którym wykorzystujemy token uzyskany podczas naszego pierwszego żądania (żądanie nr 1):
E:\curl>curl --include --cookie ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445 http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:48:47 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
qxnxmqmvhde3al55kzsmx445
<br>
requêtes Application :
6
<br>
requêtes Client :
2
<br>
</body>
</HTML>
Co oznacza ten przykład? Wysłaliśmy token uzyskany nieco wcześniej. Gdy serwer WWW tworzy token, zachowuje go tak długo, jak długo klient powiązany z tym tokenem wysyła do niego żądania. Po upływie określonego czasu bezczynności (domyślnie 20 minut w przypadku IIS) token zostaje usunięty. Poprzedni przykład pokazuje, że użyliśmy tokenu, który był nadal aktywny.
Można być ciekawym, jakie żądania o numerze HTTP wysłał klient o numerze [curl] podczas wszystkich tych operacji. Wiemy, że zostały one zapisane w pliku o nazwie [request.txt]. Oto ostatnie z nich:
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
Widać tu nagłówek HTTP, który wysyła token sesji.
Informacje przesyłane przez serwer za pośrednictwem nagłówków HTTP i [Set-Cookie:] nazywane są plikami cookie. Serwer może wykorzystać ten mechanizm do przesyłania informacji innych niż token sesji. Gdy serwer S przesyła plik cookie do klienta, podaje również jego czas życia D oraz powiązaną wartość U. Oznacza to dla klienta, że gdy wysyła on do serwera S żądanie adresu URL w postaci /U/ścieżka, może odesłać plik cookie, jeśli nie otrzymał go od czasu dłuższego niż D. Nic nie stoi na przeszkodzie, aby klient nie przestrzegał tego kodeksu postępowania. Przeglądarki natomiast przestrzegają tych zasad. Niektóre przeglądarki umożliwiają wgląd w zawartość otrzymywanych plików cookie. Tak jest w przypadku przeglądarki Mozilla. Oto na przykład informacje dotyczące pliku cookie wysłanego przez serwer w poprzednim przykładzie:

Znajdują się w nim:
- nazwę pliku cookie [ASP.NET_SessionId]
- jego wartość: [y153...m3]
- urządzenie, z którym jest powiązany: [localhost]
- adres URL, z którym jest powiązany: [/]
- jego okres ważności: [at end of session]
Przeglądarka będzie więc wysyłać token sesji za każdym razem, gdy zażąda adresu URL w postaci URL, [http://localhost/...], c.a.d. za każdym razem, gdy będzie wysyłał żądanie adresu URL do serwera WWW na komputerze [localhost]. Okres ważności pliku cookie pokrywa się z czasem trwania sesji. Dla przeglądarki oznacza to, że plik cookie nigdy nie wygasa. Będzie go wysyłać za każdym razem, gdy będzie wysyłał żądanie adresu URL do serwera WWW na komputerze [localhost]. Jeśli więc przeglądarka otrzyma token sesji w dniu D, zostanie zamknięta, a następnego dnia ponownie uruchomiona, wówczas prześle token sesji (który został zapisany w pliku). Serwer otrzyma ten token, którego już nie posiada, ponieważ token sesji ma ograniczony czas życia na serwerze (20 minut na serwerze IIS). W związku z tym rozpocznie nową sesję.
Możliwe jest wyłączenie obsługi plików cookie w przeglądarce. W takim przypadku klient otrzymuje token sesji, ale go nie odsyła, co uniemożliwia śledzenie sesji. Aby to zademonstrować, wyłączamy obsługę plików cookie w naszej przeglądarce (w tym przypadku Mozilla):

Ponadto usuwamy wszystkie istniejące pliki cookie:

Po wykonaniu tych czynności ponownie uruchamiamy serwer Cassini, aby zacząć od zera, a w przeglądarce ponownie wywołujemy adres URL [http://localhost/aspnet/session1/main.aspx]:

Sprawdźmy, czy nasza przeglądarka zapisała plik cookie:

Stwierdzamy, że przeglądarka nie zapisała pliku cookie z tokenem sesji, który serwer jej przesłał. Można zatem oczekiwać, że sesja nie będzie śledzona. Ponownie wysyłamy żądanie tego samego adresu URL (odświeżamy stronę):

Wynik jest zgodny z oczekiwaniami. Przeglądarka nie odesłała tokenu sesji, który wprawdzie otrzymała, ale nie zapisała. Serwer rozpoczął zatem nową sesję z nowym tokenem. Z tego przykładu wynika, że nasza polityka śledzenia sesji jest zagrożona, jeśli użytkownik wyłączył obsługę plików cookie w swojej przeglądarce. Istnieje jednak inny sposób, poza plikami cookie, na wymianę tokenu sesji między serwerem a klientem. Możliwe jest bowiem poinformowanie serwera WWW, że aplikacja działa bez plików cookie. Odbywa się to za pomocą pliku konfiguracyjnego [web.config]:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<system.web>
<sessionState cookieless="true" timeout="10" />
</system.web>
</configuration>
Powyższy plik konfiguracyjny wskazuje, że aplikacja będzie działać bez plików cookie (cookieless="true") oraz że maksymalny czas bezczynności tokenu sesji wynosi 10 minut (timeout="10"). Po upływie tego czasu sesja powiązana z tokenem zostaje zakończona. Proces wymiany tokenu sesji między serwerem a klientem będzie przebiegał następująco:
- klient wysyła żądanie do adresu URL [http://machine:port/V/chemin], gdzie V jest wirtualnym katalogiem serwera WWW
- serwer generuje token J i odpowiada klientowi, aby przekierował się na adres URL [http://machine:port/V/(J)/chemin]. Umieścił więc token w adresie URL, który ma zostać wywołany, bezpośrednio za wirtualnym katalogiem V
- klient stosuje się do tego przekierowania i wysyła żądanie do nowego adresu URL [http://machine:port/V/(J)/chemin].
- Serwer odpowiada na to żądanie i wysyła stronę odpowiedzi.
Zilustrujmy te różne punkty. Umieszczamy całą poprzednią aplikację w nowym katalogu <application-path>. W tym samym katalogu umieszczamy poprzedni plik [web.config]. Ponadto modyfikujemy kod prezentacji [main.aspx], aby zawrzeć w nim link:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
<% =jeton %>
<br>
requêtes Application :
<% =nbRequêtesApplication %>
<br>
requêtes Client :
<% =nbRequêtesClient %>
<br>
<a href="main.aspx">Recharger l'application</a>
</body>
</HTML>
Ten link prowadzi do strony [main.aspx] i jest zatem odpowiednikiem przycisku (Reload) w przeglądarce. Serwer Cassini jest uruchamiany z parametrami (<application-path>,/session2). Odstępujemy od naszej dotychczasowej praktyki, polegającej na odnotowywaniu katalogu wirtualnego [/aspnet/XX]. W rzeczywistości, ze względu na wstawienie tokenu sesji do adresu URL, folder wirtualny musi zawierać tylko jeden element /XX. Najpierw używamy klienta [curl], aby zażądać adresu URL [http://localhost/session2/main.aspx]:
E:\curl>curl --include http://localhost/session2/main.aspx
HTTP/1.1 302 Found
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 13:52:36 GMT
X-AspNet-Version: 1.1.4322
Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 163
Connection: Close
<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href='/session2/(hinadjag3bt0u155g5hqe245)/main.aspx'>here
</body></html>
Widzimy, że serwer odpowiada nagłówkiem HTTP [HTTP/1.1 302 Found] zamiast [HTTP/1.1 200 OK]. Jest to nagłówek, który nakazuje klientowi przekierowanie się na adres URL wskazany w nagłówku HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. Widać token sesji, który został wstawiony do adresu URL przekierowania. Przeglądarka otrzymująca tę odpowiedź wysyła żądanie do nowego adresu URL w sposób przezroczysty dla użytkownika, który nie widzi nowego żądania. W przypadku, gdy przeglądarka nie obsługuje przekierowania samodzielnie, za powyższym kodem HTTP wysyłany jest dokument HTML. Znajduje się w nim link do adresu URL przekierowania, w który użytkownik może kliknąć.
Teraz zróbmy to samo w przeglądarce, w której wyłączono pliki cookie. Ponownie wysyłamy żądanie o adres URL [http://localhost/session2/main.aspx]. Otrzymujemy następującą odpowiedź z serwera:

Po pierwsze, zauważmy, że adres URL wyświetlany przez przeglądarkę nie jest tym, o który prosiliśmy. To znak, że nastąpiło przekierowanie. W rzeczywistości przeglądarka zawsze wyświetla adres URL ostatniego otrzymanego dokumentu. Jeśli więc nie wyświetla adresu [http://localhost/session2/main.aspx], oznacza to, że otrzymała polecenie przekierowania na inny adres. Może wystąpić kilka przekierowań. Adres URL wyświetlany przez przeglądarkę to adres ostatniego przekierowania. Możemy zauważyć, że token sesji znajduje się w adresie URL wyświetlanym przez przeglądarkę. Widać to, ponieważ token ten jest również wyświetlany przez nasz program na stronie.
Przypomnijmy kod linku, który został umieszczony na stronie:
<a href="main.aspx">Recharger l'application</a>
Jest to link względny, ponieważ nie zaczyna się od znaku /, który uczyniłby go linkiem bezwzględnym. Względny względem czego? Aby to zrozumieć, należy powrócić do adresu URL aktualnie wyświetlanego dokumentu: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Linki względne, które zostaną znalezione w tym dokumencie, będą odnosiły się do ścieżki [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Zatem nasz powyższy link jest równoważny linkowi:
<a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>
Właśnie to pokazuje przeglądarka po najechaniu kursorem myszy na link:

Jeśli klikniemy na link [Recharger l'application], wywołany zostanie adres URL
[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Serwer otrzyma zatem token sesji i będzie mógł odzyskać powiązane z nim informacje. Tak właśnie wygląda odpowiedź przeglądarki:

Warto zapamiętać, że jeśli musimy śledzić sesję w aplikacji internetowej i nie mamy pewności, czy przeglądarki użytkowników tej aplikacji zezwolą na użycie plików cookie, to
- należy skonfigurować aplikację tak, aby działała bez plików cookie
- strony aplikacji muszą zawierać linki względne, a nie bezwzględne
4.2. Pobieranie informacji z żądania klienta
4.2.1. Cykl żądanie-odpowiedź w relacji klient-serwer internetowy
Przypomnijmy tutaj kontekst klient-serwer aplikacji internetowej:

Żądanie klienta skierowane do aplikacji internetowej jest przetwarzane w następujący sposób:
- klient nawiązuje połączenie TCP/IP z portem P serwisu internetowego na komputerze M, na którym znajduje się aplikacja internetowa
- przez to połączenie wysyła sekwencję wierszy tekstu zgodnie z protokołem HTTP. Ten zbiór wierszy stanowi tzw. żądanie klienta. Ma ono następującą postać:

Po wysłaniu żądania klient oczekuje na odpowiedź.
- Pierwszy wiersz nagłówków HTTP określa akcję, o którą prosi serwer internetowy. Może ona przybierać różne formy:
- GET url HTTP/<wersja>, gdzie <wersja> wynosi obecnie 1.0 lub 1.1. W tym przypadku żądanie nie zawiera części [Document]
- POST url HTTP/<wersja>. W tym przypadku żądanie zawiera część [Document], najczęściej listę informacji przeznaczonych dla aplikacji internetowej
- PUT adres URL HTTP/<wersja>. Klient wysyła dokument w części [Document] i chce go zapisać na serwerze pod adresem URL
Gdy klient chce przekazać informacje do aplikacji internetowej, z którą się połączył, ma do dyspozycji głównie dwa sposoby:
- (ciąg dalszy)
- jego żądanie ma postać [GET url_enrichie HTTP/<version>], gdzie url_enrichie ma postać [url?param1=val1¶m2=val2&...]. Klient przesyła oprócz adresu URL szereg informacji w postaci [clé=valeur].
- Jego żądanie ma postać [POST url HTTP/<version>]. W części [Document] przesyła informacje w tej samej postaci co poprzednio: [param1=val1¶m2=val2&...].
- Na serwerze cały łańcuch przetwarzania żądania klienta ma do niego dostęp za pośrednictwem obiektu globalnego o nazwie Request. Serwer WWW umieścił w tym obiekcie całe żądanie klienta w formie, którą wkrótce poznamy. Wzywana aplikacja przetworzy ten obiekt i utworzy odpowiedź dla klienta. Odpowiedź ta jest dostępna w obiekcie globalnym o nazwie Response. Zadaniem aplikacji internetowej jest utworzenie obiektu [Response] na podstawie otrzymanego obiektu [Request]. Łańcuch przetwarzania dysponuje również obiektami globalnymi [Application] i [Session], o których już wspominaliśmy, a które umożliwią jej wymianę danych między różnymi klientami (aplikacja) lub między kolejnymi żądaniami tego samego klienta (sesja).
- Aplikacja wyśle swoją odpowiedź do serwera za pomocą obiektu [Response]. Odpowiedź ta, po przesłaniu do sieci, będzie miała następującą postać: HTTP:

Po wysłaniu tej odpowiedzi serwer zamknie połączenie sieciowe po stronie odbiorczej (chyba że klient polecił mu, aby tego nie robił).
- Klient otrzyma odpowiedź i z kolei zamknie połączenie (podczas wysyłania). To, co zostanie zrobione z tą odpowiedzią, zależy od typu klienta. Jeśli jest to przeglądarka, a otrzymany dokument to dokument HTML, zostanie on wyświetlony. Jeśli klientem jest program, odpowiedź zostanie przeanalizowana i wykorzystana.
- Fakt, że po zakończeniu cyklu żądanie-odpowiedź połączenie łączące klienta z serwerem zostaje zamknięte, sprawia, że protokół HTTP jest protokołem bezstanowym. Podczas kolejnego żądania klient nawiąże nowe połączenie sieciowe z tym samym serwerem. Ponieważ nie jest to już to samo połączenie sieciowe, serwer nie ma żadnej możliwości (na poziomie TCP/IP i HTTP) powiązania tego nowego połączenia z poprzednim. To system tokenów sesji umożliwi takie powiązanie.
4.2.2. Pobieranie informacji przesłanych przez klienta
Przyjrzyjmy się teraz niektórym właściwościom i metodom obiektu [Request], które umożliwiają kodowi aplikacji dostęp do żądania klienta, a tym samym do informacji przez niego przesłanych. Obiekt [Request] jest typu [HttpRequest]:

Klasa ta posiada wiele właściwości i metod. Interesują nas właściwości HttpMethod, QueryString, Form oraz Params, które umożliwią nam dostęp do elementów łańcucha informacji [param1=val1¶m2=val2&...].
metody żądania klienta: GET, POST, HEAD, ... | |
zbiór elementów ciągu zapytania param1=val1¶m2=val2&.. z pierwszego wiersza HTTP [méthode]?param1=val1¶m2=val2&... gdzie [méthode] może być GET, POST, HEAD. | |
zbiór elementów ciągu zapytania param1=val1¶m2=val2&... znajdujących się w części [Document] zapytania (metoda POST). | |
łączy kilka zbiorów: QueryString, Form, ServerVariables, Cookies w ramach jednego zbioru. |
4.2.3. Przykład 1
Zastosujmy te elementy w pierwszym przykładzie. Aplikacja będzie zawierała tylko jeden element [main.aspx]. Kod prezentacji [main.aspx] będzie wyglądał następująco:
<%@ 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>
Strona wyświetla trzy informacje [méthode, nom, age] obliczone przez jej moduł sterujący [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected nom As String = "xx"
Protected age As String = "yy"
Protected méthode As String
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' zapisujemy żądanie klienta w pliku request.txt w katalogu aplikacji
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
' pobieramy parametry żądania
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
Po załadowaniu strony (Form_Load) informacje [nom, age] są pobierane z żądania klienta. Szukamy ich w obu kolekcjach: [QueryString] i [Form]. . Ponadto w pliku [Page_Init] zapisujemy żądanie klienta, aby móc sprawdzić, co zostało wysłane. Umieszczamy te dwa pliki w folderze <application-path> i uruchamiamy serwer Cassini z parametrami (<application-path>,/request1), a następnie za pomocą przeglądarki wysyłamy żądanie pod adresem
[http://localhost/request1/main.aspx?nom=tintin&age=27]. Otrzymujemy następującą odpowiedź:

Informacje przesłane przez klienta zostały poprawnie odzyskane. Żądanie przeglądarki zapisane w pliku [request.txt] ma następującą postać:
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
Widać, że przeglądarka wysłała żądanie o numerze GET. Aby wysłać żądanie o numerze POST, użyjemy klienta o numerze [curl]. W oknie wiersza poleceń wpisujemy następujące polecenie:
aby wyświetlić nagłówki odpowiedzi HTTP | |
aby wysłać informację param=wartość za pomocą POST |
Odpowiedź serwera jest następująca:
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>
Serwer ponownie pomyślnie pobrał parametry wysłane tym razem przez plik POST. Aby upewnić się co do tego ostatniego punktu, można sprawdzić zawartość pliku [request.txt]:
POST /request1/main.aspx HTTP/1.1
Pragma: no-cache
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
nom=tintin&age=27
Klient [curl] poprawnie wykonał POST. Teraz połączmy te dwie metody przekazywania informacji. W żądanym adresie URL umieszczamy [age], a w wysyłanym dokumencie – [nom]:
Żądanie wysłane przez [curl] wygląda następująco (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
Widać, że wiek został przekazany w żądanym adresie URL. Znajdziemy go w kolekcji [QueryString]. Imię zostało natomiast przekazane w dokumencie wysłanym na ten adres URL. Znajdziemy je w kolekcji [Form]. Odpowiedź otrzymana przez klienta [curl]:
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
W końcu nie wysyłajmy żadnych informacji na serwer:
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>
Aby zrozumieć tę odpowiedź, prosimy o ponowne zapoznanie się z kodem kontrolera [main.aspx.vb].
4.2.4. Przykład 2
Klient może wysłać kilka wartości dla tego samego klucza. Co się więc stanie, jeśli w poprzednim przykładzie wywołamy adres URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou], w którym dwukrotnie występuje klucz [nom]? Sprawdźmy to w przeglądarce:

Nasza aplikacja poprawnie pobrała obie wartości powiązane z kluczem [nom]. Wyświetlany wynik jest nieco mylący. Został on uzyskany za pomocą instrukcji
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
Metoda [ToString] wygenerowała ciąg znaków [tintin,milou], który został wyświetlony. Ukrywa to fakt, że w rzeczywistości obiekt [Request.QueryString("nom")] jest tablicą ciągów znaków {"tintin","milou"}. Poniższy przykład ilustruje tę kwestię. Strona prezentacyjna [main.aspx] będzie wyglądać następująco:
<%@ 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>
Na tej stronie pojawiły się nowości wykorzystujące tzw. kontrolki serwerowe. Charakteryzują się one atrybutem [runat="server"]. Jest jeszcze za wcześnie, aby wprowadzać pojęcie kontrolki serwerowej. Wystarczy wiedzieć, że w tym przypadku:
- strona zawiera dwie listy (tagi <asp:listbox>)
- że listy te są obiektami (lstQueryString, lstForm) typu [ListBox], które zostaną utworzone przez kontroler strony
- że obiekty te istnieją wyłącznie na serwerze WWW. W momencie wysłania odpowiedzi zostaną one przekształcone w standardowe tagi HTML, które klient będzie w stanie odczytać. Obiekt [listbox] zostanie zatem przekształcony (mówi się również o renderowaniu) w tagi HTML <select> oraz <option>.
- Głównym celem tych obiektów jest wyeliminowanie kodu VB z kodu prezentacyjnego, który pozostaje ograniczony do kontrolera.
Kontroler [main.aspx.vb] odpowiedzialny za tworzenie dwóch obiektów [lstQueryString] i [lstForm] wygląda następująco:
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
' zapisujemy żądanie klienta w pliku request.txt w katalogu aplikacji
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
' pobieramy cały zbiór informacji z pliku QueryString
infosQueryString = getValeurs(Request.QueryString)
lstQueryString.DataSource = infosQueryString
lstQueryString.DataBind()
infosForm = getValeurs(Request.Form)
lstForm.DataSource = infosForm
lstForm.DataBind()
End Sub
Private Function getValeurs(ByRef data As NameValueCollection) As ArrayList
' początkowo lista informacji jest pusta
Dim infos As New ArrayList
' pobieramy klucze z kolekcji
Dim clés() As String = data.AllKeys
' przeglądamy tablicę kluczy
Dim valeurs() As String
For Each clé As String In clés
' wartości powiązane z kluczem
valeurs = data.GetValues(clé)
' tylko jedna wartość?
If valeurs.Length = 1 Then
infos.Add(clé + "=" + valeurs(0))
Else
' wiele wartości
For ivalue As Integer = 0 To valeurs.Length - 1
infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
Next
End If
Next
' zwracamy wynik
Return infos
End Function
End Class
Najważniejsze elementy tego kodu to:
- w [Form_Load] strona pobiera dwie kolekcje: [QueryString] i [Form]. Wykorzystuje ona funkcję [getValeurs], aby umieścić zawartość tych dwóch kolekcji w dwóch obiektach typu [ArrayList], które będą zawierały ciągi znaków typu [clé=valeur], jeśli klucz kolekcji jest powiązany z jedną wartością, lub typu [clé(i)=valeur], jeśli klucz jest powiązany z wieloma wartościami.
- Każdy z obiektów typu [ArrayList] jest następnie przypisywany do jednego z obiektów typu [ListBox] na stronie prezentacyjnej za pomocą dwóch instrukcji:
- [ListBox.DataSource=ArrayList] oraz [ListBox.DataBind]. Ta ostatnia instrukcja przenosi elementy z obiektu [DataSource] do kolekcji [Items] obiektu [ListBox]
należy zauważyć, że żaden z dwóch obiektów [ListBox] nie został utworzony w sposób jawny przez operację [New]. Można z tego wywnioskować, że w przypadku obecności tagu <asp:listbox id="xx">...<asp:listbox/> serwer WWW samodzielnie tworzy obiekt [ListBox], do którego odwołuje się atrybut [id] tego tagu.
- Funkcja [getValeurs] wykorzystuje obiekt typu [NameValueCollection], który jest jej przekazywany jako parametr, w celu wygenerowania wyniku typu [ArrayList].
Umieszczamy oba powyższe pliki w folderze <application-path> i uruchamiamy serwer Cassini z parametrami (<application-path>,/request2), a następnie wysyłamy żądanie o adres URL
[http://localhost/request2/main.aspx?nom=tintin&age=27]. Otrzymujemy następującą odpowiedź:

Teraz wysyłamy żądanie do adresu URL, w którym klucz [nom] występuje dwukrotnie:

Stwierdzamy, że obiekt [Request.QueryString("nazwa")) rzeczywiście był tablicą. W tym przypadku zapytania były wysyłane za pomocą metody GET. Używamy klienta [curl] do wysłania żądania POST:
E:\curl>curl --data nom=milou --data nom=tintin --data age=14 --data age=27 http://localhost/request2/main.aspx
<HTML>
<HEAD>
<title>Requête client</title>
</HEAD>
<body>
<P>Informations passées par le client :</P>
<form name="_ctl0" method="post" action="main.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwtMTI3MjA1MzUzMTs7PtCDC7NG4riDYIB4YjyGFpVAAviD" />
<P>QueryString :</P>
<P><select name="lstQueryString" size="6" id="lstQueryString">
</select></P>
<P>Form :</P>
<P><select name="lstForm" size="2" id="lstForm">
<option value="nom(0)=milou">nom(0)=milou</option>
<option value="nom(1)=tintin">nom(1)=tintin</option>
<option value="age(0)=14">age(0)=14</option>
<option value="age(1)=27">age(1)=27</option>
</select></P>
</form>
</body>
</HTML>
Widać, że klient otrzymuje standardowy kod HTML dla obu list na stronie. Pojawiają się informacje, których sami nie umieściliśmy, takie jak ukryte pole [_VIEWSTATE]. Informacje te zostały wygenerowane przez tagi <asp:xx runat="server>. Będziemy musieli nauczyć się je opanować.
4.3. Wdrożenie architektury MVC
4.3.1. Koncepcja
Zakończmy ten długi rozdział wdrożeniem aplikacji zbudowanej zgodnie z modelem MVC (Model-View-Controller). Taka aplikacja internetowa oparta na tym modelu wygląda następująco:

- klient kieruje swoje żądania do konkretnego elementu aplikacji zwanego kontrolerem
- kontroler analizuje żądanie klienta i powoduje jego wykonanie. W tym celu korzysta z pomocy klas zawierających logikę biznesową aplikacji oraz klas zapewniających dostęp do danych.
- w zależności od wyniku wykonania żądania kontroler decyduje się wysłać klientowi określoną stronę w odpowiedzi
W naszym modelu wszystkie żądania przechodzą przez jeden kontroler, który pełni rolę koordynatora całej aplikacji internetowej. Zaletą tego modelu jest to, że w kontrolerze można zgrupować wszystkie czynności, które muszą zostać wykonane przed każdym żądaniem. Załóżmy na przykład, że aplikacja wymaga uwierzytelnienia. Odbywa się ono tylko raz. Po pomyślnym zakończeniu uwierzytelniania aplikacja zapisuje w sesji informacje dotyczące użytkownika, który właśnie się uwierzytelnił. Ponieważ klient może wywołać stronę aplikacji bezpośrednio, bez uwierzytelniania się, każda strona będzie musiała sprawdzić w sesji, czy uwierzytelnienie rzeczywiście zostało przeprowadzone. Jeśli wszystkie żądania przechodzą przez jeden kontroler, to właśnie on może wykonać tę pracę. Strony, do których żądanie zostanie ewentualnie przekazane, nie będą musiały tego robić.
4.3.2. Kontrola aplikacji MVC bez sesji
Z tego, co widzieliśmy do tej pory, można wywnioskować, że plik [global.asax] mógłby pełnić rolę kontrolera. Wiemy bowiem, że wszystkie żądania przechodzą przez niego. Jest więc w idealnym miejscu, by wszystko kontrolować. Poniższa aplikacja wykorzystuje go właśnie w tym celu. Jego ścieżka wirtualna będzie brzmiała [http://localhost/mvc1/main.aspx]. Aby wskazać, czego oczekuje, klient dołączy do adresu URL parametr action=wartość. W zależności od wartości parametru [action] kontroler [global.asax] przekieruje żądanie na konkretną stronę:
- [main.aspx], jeśli parametr „action” nie jest zdefiniowany lub jeśli „action=main”
- [action1.aspx], jeśli action=action1
- [inconnu.aspx], jeśli „action” nie spełnia warunków z przypadków 1 i 2
Strony o nazwach [main.aspx, action1.aspx, inconnu.aspx] wyświetlają jedynie wartość [action], która spowodowała ich wyświetlenie. Poniżej wymieniamy osiem plików tej aplikacji i w razie potrzeby dodajemy do nich komentarze:
[global.asax]
[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)
' pobieramy akcję do wykonania
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' umieszczamy akcję w kontekście żądania
Context.Items("action") = action
' wykonujemy akcję
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
Uwagi:
- przechwytujemy wszystkie żądania klienta w procedurze [Application_BeginRequest], która jest automatycznie uruchamiana przy każdym nowym żądaniu skierowanym do aplikacji.
- W tej procedurze mamy dostęp do obiektu [Request], który jest odzwierciedleniem żądania HTTP wysłanego przez klienta. Ponieważ oczekujemy adresu URL w postaci [http://localhost/mvc1/main.aspx?action=xx], szukamy klucza [action] w kolekcji [Request.QueryString]. Jeśli go tam nie ma, domyślnie ustawiamy wartość parametru „action” na „main”.
- Wartość parametru [action] jest umieszczana w obiekcie [Context]. Podobnie jak obiekty [Application, Session, Request, Response, Server], obiekt ten jest globalny i dostępny w całym kodzie. Obiekt ten jest przekazywany między stronami, jeśli żądanie jest przetwarzane przez wiele stron, tak jak ma to miejsce w tym przypadku. Jest on usuwany natychmiast po wysłaniu odpowiedzi do klienta. Jego czas życia odpowiada zatem czasowi przetwarzania żądania.
- W zależności od wartości parametru [action] żądanie jest przekazywane do odpowiedniej strony. W tym celu wykorzystuje się obiekt globalny [Server], który dzięki swojej metodzie umożliwia przekazanie bieżącego żądania do innej strony. Jego pierwszym parametrem jest nazwa strony docelowej, a drugim wartość logiczna wskazująca, czy należy przekazać do strony docelowej kolekcje [QueryString] i [Form]. W tym przypadku odpowiedź brzmi „tak”.
Pliki [main.aspx] i [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
' pobieramy bieżącą akcję
action = Me.Context.Items("action").ToString
End Sub
End Class
Kontroler [main.aspx.vb] ogranicza się do pobrania wartości klucza [action] z kontekstu, przy czym wartość ta jest wyświetlana przez kod prezentacji. Celem jest tutaj pokazanie przepływu obiektu [Context] między różnymi stronami obsługującymi to samo żądanie klienta. Strony [action1.aspx] i [inconnu.aspx] działają w podobny sposób:
[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
' pobieramy bieżącą akcję
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
' pobieramy bieżącą akcję
action = Me.Context.Items("action").ToString
End Sub
End Class
Aby przeprowadzić test, powyższe dokumenty umieszczamy w folderze <application-path>, a następnie uruchamiamy Cassini z parametrami (<application-path>,/mvc1). Następnie wywołujemy adres URL [http://localhost/mvc1/main.aspx]:

Żądanie nie zawierało żadnych parametrów [action]. Kod kontrolera aplikacji [global.asax.vb] spowodował wygenerowanie strony [main.aspx]. Teraz wysyłamy żądanie dotyczące adresu URL [http://localhost/mvc1/main.aspx?action=action1]:

Kod kontrolera aplikacji [global.asax.vb] spowodował wygenerowanie strony [action1.aspx]. Teraz wysyłamy żądanie dotyczące adresu URL [http://localhost/mvc1/main.aspx?action=xx]:

Akcja nie została rozpoznana, a kontroler [global.asax.vb] wygenerował stronę [inconnu.aspx].
4.3.3. Kontrola aplikacji MVC z wykorzystaniem sesji
W większości przypadków różne żądania klienta kierowane do aplikacji muszą współdzielić informacje. Omówiliśmy już jedno z możliwych rozwiązań tego problemu: przechowywanie informacji, które mają być współdzielone, w obiekcie [Session] żądania. Obiekt ten jest bowiem współdzielony przez wszystkie żądania i może przechowywać informacje w postaci (klucz, wartość), gdzie klucz jest typu [String], a wartość może być dowolnym typem pochodnym od [Object].
W powyższym przykładzie różne strony powiązane z różnymi akcjami były wywoływane w procedurze [Application_BeginRequest] w pliku [global.asax.vb]:
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' pobieramy akcję do wykonania
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' umieszczamy akcję w kontekście żądania
Context.Items("action") = action
' wykonuje się akcję
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
Okazuje się, że w procedurze [Application_BeginRequest] obiekt [Session] jest niedostępny. To samo dotyczy strony, do której przekazywane jest wykonanie. W związku z tym ten szablon nie nadaje się do zastosowania w aplikacji z sesją. Rolę kontrolera może pełnić dowolna strona, na przykład [default.aspx]. Pliki [global.asax, global.asax.vb] znikają wówczas i są zastępowane przez pliki [default.aspx, default.aspx.vb]:
[default.aspx]
[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
' pobieramy czynność do wykonania
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' umieszczamy akcję w kontekście żądania
Context.Items("action") = action
' pobieramy poprzednią akcję, jeśli istnieje
Context.Items("actionPrec") = Session.Item("actionPrec")
If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
' zapisuje się bieżącą akcję w sesji
Session.Item("actionPrec") = action
' wykonuje się akcję
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
Aby uwidocznić mechanizm sesji, poszczególne strony będą wyświetlać, oprócz bieżącej akcji, również akcję poprzedzającą. W przypadku sekwencji akcji A1, A2, ..., An, gdy wystąpi akcja Ai, powyższy kontroler:
- umieszcza bieżącą akcję Ai w kontekście
- odnajduje w sesji poprzedzającą akcję Ai-1. W przypadku jej braku (jak w przypadku akcji A1) ustawia pusty ciąg znaków jako poprzednią akcję.
- umieszcza bieżącą akcję Ai w sesji, zastępując nią Ai-1
- przekazuje wykonanie do odpowiedniej strony
Trzy strony aplikacji to:
[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>
Ponieważ wszystkie trzy strony wyświetlają te same informacje [action, actionPrec], wszystkie trzy mogą mieć ten sam kontroler strony. Dlatego wszystkie zostały utworzone na podstawie klasy [main] z pliku [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
Protected action As String
Protected actionPrec As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' pobieramy bieżącą akcję
action = Me.Context.Items("action").ToString
' oraz poprzednią akcję
actionPrec = Me.Context.Items("actionPrec").ToString
End Sub
End Class
Powyższy kod służy wyłącznie do pobierania informacji umieszczonych w kontekście przez kontroler aplikacji [default.aspx.vb].
Wszystkie te pliki znajdują się w katalogu <application-path>, a Cassini uruchamia się z parametrami (<application-path>,/mvc2). Najpierw wysyłamy żądanie do adresu URL [http://localhost/mvc2]:

Adres URL [http://localhost/mvc2] wskazuje na folder. Wiemy, że w tym przypadku serwer zwraca dokument [default.aspx] z tego folderu, o ile istnieje. W tym przypadku nie określono żadnej akcji. W związku z tym wykonano akcję [main]. Przejdźmy do akcji [action1]:

Bieżąca i poprzednia akcja zostały poprawnie zidentyfikowane. Przejdźmy do akcji [xx]:

4.4. Conclusion
Mamy teraz podstawowe elementy, na których opiera się każda aplikacja ASP.ET. Pozostaje nam jednak wprowadzić jeszcze jedno ważne pojęcie: pojęcie formularza. Temu zagadnieniu poświęcony jest następny rozdział.