4. Основи розробки ASP.NET
4.1. Поняття веб-додатку ASP.NET
4.1.1. Вступ
Веб-додаток — це додаток, що об’єднує різні документи (HTML, код .NET, зображення, звуки тощо). Ці документи мають знаходитися в одному кореневому каталозі, який називається кореневим каталогом веб-додатка. З цим кореневим каталогом пов’язаний віртуальний шлях на веб-сервері. Ми вже знайомилися з поняттям віртуальної папки для веб-сервера Cassini. Це поняття існує також для веб-сервера IIS. Важливою відмінністю між цими двома серверами є те, що в певний момент IIS може мати довільну кількість віртуальних папок, тоді як веб-сервер Cassini має лише одну — ту, що була вказана під час його запуску. Це означає, що сервер IIS може обслуговувати кілька веб-додатків одночасно, тоді як сервер Cassini — лише один за раз. У наведених вище прикладах сервер Cassini завжди запускався з параметрами (<webroot>,/aspnet), які пов’язували віртуальну папку /aspnet із фізичною папкою <webroot>. Отже, веб-сервер завжди обслуговував один і той самий веб-додаток. Це не завадило нам створювати та тестувати різні незалежні сторінки в межах цього єдиного веб-додатка. Кожен веб-додаток має власні ресурси, які знаходяться у його фізичному кореневому каталозі <webroot>:
- папка [bin], у яку можна розміщувати попередньо скомпільовані класи
- файл [global.asax], який дозволяє ініціалізувати веб-додаток у цілому, а також середовище виконання для кожного з його користувачів
- файл [web.config], який дозволяє налаштовувати роботу додатка
- файл [default.aspx], який виконує роль вхідної точки додатка
- ...
Як тільки додаток використовує один із цих трьох ресурсів, йому потрібні власні фізичний та віртуальний шляхи. Адже немає жодних підстав для того, щоб два різні веб-додатки були налаштовані однаково. Усі наші попередні приклади можна було розмістити в одному додатку (<webroot>,/aspnet), оскільки вони не використовували жодного з вищезазначених ресурсів.
Повернімося до архітектури MVC, рекомендованої на початку цього розділу для розробки веб-додатку:

Веб-додаток складається з файлів класів (контролер, бізнес-класи, класи доступу до даних) та файлів представлення (документи HTML, зображення, звуки, таблиці стилів тощо). Усі ці файли будуть розміщені в одному кореневому каталозі, який ми іноді називатимемо <application-path>. Цей кореневий каталог буде пов’язаний із віртуальним шляхом <application-vpath>. Зв’язок між цим віртуальним шляхом та фізичним шляхом встановлюється шляхом налаштування веб-сервера. Ми бачили, що для сервера Cassini цей зв’язок встановлюється під час запуску сервера. Наприклад, у вікні командного рядка Cassini запускається так:
У папці <application-path> залежно від наших потреб ми знайдемо:
- папку [bin] для розміщення попередньо скомпільованих класів (dll)
- файл [global.asax], коли нам знадобиться виконати ініціалізацію — або під час запуску програми, або під час запуску сеансу користувача
- файл [web.config], коли нам знадобиться налаштувати додаток
- файл [default.aspx], коли нам знадобиться сторінка за замовчуванням у додатку
Щоб дотриматися цієї концепції веб-додатка, усі наступні приклади будуть розміщені в папці <application-path>, що належить до додатка, з якою буде пов’язана віртуальна папка <application-vpath>, оскільки сервер Cassini запускається таким чином, щоб пов’язати ці два параметри.
4.1.2. Налаштування веб-додатку
Якщо <application-path> є кореневим каталогом додатка ASP.NET, для його налаштування можна використати файл <application-path>\web.config. Цей файл має формат XML. Ось приклад:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Слід звернути увагу на те, що теги XML чутливі до регістру. Уся інформація про конфігурацію має розміщуватися між тегами <configuration> та </configuration>. Існує чимало розділів конфігурації, які можна використовувати. Тут ми розглянемо лише один із них — розділ <appSettings>, який дозволяє ініціалізувати дані за допомогою тегу <add>. Синтаксис цього тегу такий:
Коли веб-сервер запускає додаток, він перевіряє, чи в <application-path> є файл із назвою web.config. Якщо так, він зчитує його та зберігає інформацію в об’єкті типу [ConfigurationSettings], який буде доступний на всіх сторінках додатка, доки той залишається активним. Клас [ConfigurationSettings] має статичний метод [AppSettings]:

Щоб отримати значення ключа C з файлу конфігурації, потрібно написати ConfigurationSettings.AppSettings("C"). У результаті отримаємо рядок символів. Щоб використати попередній файл конфігурації, створимо сторінку [default.aspx]. Код VB у файлі [default.aspx.vb] матиме такий вигляд:
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
'отримується інформація про конфігурацію
nom = ConfigurationSettings.AppSettings("nom")
age = ConfigurationSettings.AppSettings("age")
End Sub
End Class
Як бачимо, під час завантаження сторінки отримуються значення параметрів конфігурації [nom] та [age]. Вони будуть відображені за допомогою коду візуалізації [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>
Для тестування помістіть файли [web.config], [default.aspx] та [default.aspx.vb] в одну папку:
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
Нехай <application-path> — це папка, у якій знаходяться три файли додатка. Сервер Cassini запускається з параметрами (<application-path>,/aspnet/config1). Ми запитуємо файли URL та [http://localhost/aspnet/config1]. Оскільки [config1] є папкою, веб-сервер шукатиме в ній файл [default.aspx] і відобразить його, якщо знайде. У цьому випадку він його знайде:

4.1.3. Додаток, сесія, контекст
4.1.3.1. Файл global.asax
Код файлу [global.asax] завжди виконується до того, як завантажиться сторінка, запитувана поточним запитом. Він повинен знаходитися в кореневому каталозі <application-path> додатка. Якщо файл [global.asax] існує, веб-сервер використовує його в різних моментах:
- під час запуску або завершення роботи веб-додатку
- під час запуску або завершення сеансу користувача
- під час початку користувацького запиту
Як і у випадку зі сторінками .aspx, файл [global.asax] можна написати різними способами, зокрема, розділивши код VB на код контролера та код представлення. Це вибір, який за замовчуванням робить Visual Studio, і ми тут зробимо так само. Зазвичай візуалізація не потрібна, оскільки цю роль виконують сторінки .aspx. Отже, вміст файлу [global.asax] зводиться до директиви, що посилається на файл із кодом контролера:
<%@ Application src="Global.asax.vb" Inherits="Global" %>
Зверніть увагу, що директива тепер має вигляд не [Page], а [Application]. Код контролера [global.asax.vb], пов’язаний із цією директивою та згенерований інструментом Visual Studio, має такий вигляд:
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)
' Спрацьовує під час запуску програми
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Спрацьовує під час запуску сеансу
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' Спрацьовує на початку кожного запиту
End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
' Викликається під час спроби автентифікації користувача
End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Спрацьовує при виникненні помилки
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
' Спрацьовує при завершенні сеансу
End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
' Спрацьовує при завершенні роботи програми
End Sub
End Class
Слід зазначити, що клас контролера походить від класу [HttpApplication]. У процесі роботи додатка відбувається кілька важливих подій. Вони обробляються процедурами, скелет яких наведено вище.
- [Application_Start]: нагадаємо, що веб-додаток «закритий» у віртуальному шляху. Додаток запускається, щойно клієнт запитує сторінку, розташовану в цьому віртуальному шляху. Тоді виконується процедура [Application_Start]. Це відбудеться лише один раз. У цій процедурі виконується вся необхідна для додатка ініціалізація, наприклад, створення об’єктів, термін існування яких збігається з терміном існування додатка.
- [Application-End]: виконується, коли додаток завершується. Кожному додатку присвоюється час бездіяльності, який можна налаштувати в [web.config]; після його закінчення додаток вважається завершеним. Отже, саме веб-сервер приймає це рішення відповідно до налаштувань додатка. Час бездіяльності додатка визначається як проміжок часу, протягом якого жоден клієнт не надсилав запиту на ресурс додатка.
- [Session-Start]/[Session_End]: До кожного клієнта прив’язується сесія, за винятком випадків, коли додаток налаштовано так, що він не використовує сесії. Клієнт — це не обов’язково користувач, який сидить перед екраном. Якщо користувач відкрив 2 браузери для взаємодії з додатком, він вважається двома клієнтами. Клієнт ідентифікується за допомогою сесійного токена, який він повинен додавати до кожного свого запиту. Цей сесійний токен — це унікальний набір символів, випадково згенерований веб-сервером. Два клієнти не можуть мати однаковий сесійний токен. Цей токен супроводжує клієнта таким чином:
- клієнт, який надсилає свій перший запит, не надсилає сесійного токена. Веб-сервер це фіксує та присвоює йому токен. Це початок сесії, і виконується процедура [Session_Start]. Це відбудеться лише один раз.
- Клієнт надсилає наступні запити, додаючи до них токен, що його ідентифікує. Це дозволяє веб-серверу знайти інформацію, пов’язану з цим токеном. Це забезпечує відстеження взаємозв’язку між різними запитами клієнта.
- Додаток може надати клієнту форму для завершення сеансу. У цьому випадку саме клієнт самостійно ініціює завершення свого сеансу. Буде виконано процедуру [Session_End]. Це відбудеться лише один раз.
- Клієнт може ніколи самостійно не ініціювати завершення своєї сесії. У цьому випадку, після певного періоду бездіяльності сесії, який також можна налаштувати за допомогою процедури [web.config], сесія буде завершена веб-сервером. Тоді буде виконана процедура [Session_End].
- [Application_BeginRequest]: ця процедура виконується щоразу, коли надходить новий запит. Отже, вона виконується при кожному запиті від будь-якого клієнта. Це зручне місце для перевірки запиту перед його передачею на сторінку, яку було запрошено. Можна навіть прийняти рішення перенаправити запит на іншу сторінку.
- [Application_Error]: виконується щоразу, коли виникає помилка, яка явно не обробляється кодом контролера [global.asax.vb]. Тут можна перенаправити запит клієнта на сторінку, що пояснює причину помилки.
Якщо жодна з цих подій не потребує обробки, файл [global.asax] можна проігнорувати. Саме так було зроблено в перших прикладах цього розділу.
4.1.3.2. Приклад 1
Розробимо додаток, щоб краще зрозуміти три етапи: запуск додатка, сеансу та клієнтського запиту. Файл [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)
' Спрацьовує під час запуску програми
' фіксується час
Dim startApplication As String = Date.Now.ToString("T")
' зберігається в контексті програми
Application.Item("startApplication") = startApplication
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Спрацьовує при запуску сеансу
' записується час
Dim startSession As String = Date.Now.ToString("T")
' додається до сеансу
Session.Item("startSession") = startSession
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' записується час
Dim startRequest As String = Date.Now.ToString("T")
' заноситься до сеансу
Context.Items("startRequest") = startRequest
End Sub
End Class
Основні моменти коду такі:
- веб-сервер надає класу [HttpApplication] з [global.asax.vb] певну кількість об’єктів:
- Додаток типу [HttpApplicationState] — представляє веб-додаток — надає доступ до словника об’єктів [Application.Item], доступного для всіх клієнтів додатка — забезпечує обмін інформацією між різними клієнтами — одночасний доступ кількох клієнтів до одних і тих самих даних для читання/запису вимагає синхронізації клієнтів.
- Сесія типу [HttpSessionState] — представляє конкретного клієнта — надає доступ до словника об’єктів [Session.Item], доступного для всіх запитів цього клієнта — дозволить зберігати інформацію про клієнта, яку можна буде використовувати під час його подальших запитів.
- Запит типу [HttpRequest] — представляє поточний запит клієнта HTTP
- Відповідь типу [HttpResponse] — представляє відповідь HTTP, яка наразі формується сервером для клієнта
- Сервер типу [HttpServerUtility] — надає допоміжні методи, зокрема для перенаправлення запиту на іншу сторінку, ніж та, що була передбачена спочатку.
- Контекст типу [HttpContext] — цей об’єкт створюється заново при кожному новому запиті, але є спільним для всіх сторінок, що беруть участь в обробці запиту; він дозволяє передавати інформацію між сторінками під час обробки запиту за допомогою свого словника Items.
- процедура [Application_Start] фіксує початок роботи додатка у змінній, що зберігається у словнику, доступному на рівні додатка
- процедура [Session_Start] фіксує початок сеансу у змінній, що зберігається у словнику, доступному на рівні сеансу
- процедура [Application_BeginRequest] фіксує початок запиту у змінній, що зберігається у словнику, доступному на рівні запиту (c.a.d доступна протягом усього часу його обробки, але втрачається після її завершення)
Цільовою сторінкою буде наступна сторінка [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>
На цій сторінці відображаються значення, обчислені її контролером [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
' отримуємо інформацію про додаток та сесію
jeton=Session.SessionId
startApplication = Application.Item("startApplication").ToString
startSession = Session.Item("startSession").ToString
startRequest = Context.Items("startRequest").ToString
End Sub
End Class
Контролер просто отримує три елементи інформації, розміщені відповідно в додатку, сесії та контексті за допомогою [global.asax.vb].
Ми тестуємо додаток наступним чином:
- файли збираються в одній папці <application-path>

- сервер Cassini запускається з параметрами (<application-path>,/aspnet/globalasax1)
- перший клієнт запитує URL-адресу [http://localhost/aspnet/globalasax1/main.aspx] і отримує такий результат:

- той самий клієнт надсилає новий запит (опція «Оновити» у браузері):

Можна помітити, що змінився лише час запиту. Це свідчить про дві речі:
- процедури [Application_Start] та [Session_Start] з [global.asax] не були виконані під час другого запиту.
- об’єкти [Application] та [Session], у яких зберігалися час запуску програми та час початку сеансу, все ще доступні для другого запиту.
- запускаємо другий браузер, щоб створити другого клієнта, і знову запитуємо ту саму URL-адресу:

Цього разу ми бачимо, що час сеансу змінився. Другий браузер, хоча й працює на тому самому комп’ютері, був розпізнаний як другий клієнт, і для нього було створено новий сеанс. Можна помітити, що обидва клієнти мають різні токени сеансу. Час початку роботи додатка не змінився, що означає:
- процедура [Application_Start] з [global.asax.vb] не була виконана
- об’єкт [Application], у якому зберігався час запуску додатка, доступний для другого клієнта. Отже, саме в цьому об’єкті слід зберігати інформацію, якою мають обмінюватися різні клієнти додатка, тоді як об’єкт [Session] слугує для зберігання інформації, якою мають обмінюватися запити одного й того самого клієнта.
4.1.3.3. Загальний огляд
Використовуючи те, що ми дізналися до цього моменту, ми можемо скласти першу схему, що пояснює роботу веб-сервера та веб-додатків, які він обслуговує:

Наведена схема показує сервер, що обслуговує два додатки, позначені як A та B, кожен із яких має по два клієнти. Веб-сервер здатний одночасно обслуговувати кілька веб-додатків. Ці додатки є повністю незалежними один від одного. Розглянемо додаток A. Обробка запиту від клієнта-1A до додатка A відбуватиметься наступним чином:
- клієнт 1A запитує у веб-сервера ресурс, що належить до домену додатка A. Це означає, що він запитує URL у формі [http://machine:port/VA/ressource], де VA — це віртуальний шлях до додатка A.
- Якщо веб-сервер виявляє, що це перший запит на ресурс додатка A, він запускає подію [Application_Start] з файлу [global.asax] додатка A. Буде створено об’єкт [ApplicationA] типу [HttpApplicationState]. Різні коди додатка зберігатимуть у цьому об’єкті дані з областю дії [Application], c.a.d — дані, що стосуються всіх користувачів. Об’єкт [ApplicationA] існуватиме доти, доки веб-сервер не завершить роботу додатка A.
- якщо веб-сервер додатково виявить, що має справу з новим клієнтом додатка A, він запустить подію [Session_Start] із файлу [global.asax] додатка A. Буде створено об’єкт [Session-1A] типу [HttpSessionState]. Цей об’єкт дозволить додатку A зберігати об’єкти області дії [Session], c.a.d, що належать конкретному клієнту. Об’єкт [Session-1A] існуватиме доти, доки клієнт 1A надсилатиме запити. Він дасть змогу відстежувати цього клієнта. Веб-сервер виявляє, що має справу з новим клієнтом, у двох випадках:
- клієнт не надіслав йому сесійний токен у заголовках HTTP свого запиту
- клієнт надіслав йому сесійний токен, який не існує (несправність клієнта або спроба злому) або який більше не існує. Сесійний токен дійсно втрачає чинність після певного періоду бездіяльності клієнта (за замовчуванням 20 хвилин для IIS). Цей час можна налаштувати.
- У будь-якому разі веб-сервер ініціює подію [Application_BeginRequest] із файлу [global.asax]. Ця подія запускає обробку клієнтського запиту. Часто цю подію не обробляють, а передають управління запитуваній клієнтом сторінці, яка й оброблятиме запит. Також можна використовувати цю подію для аналізу запиту, його обробки та вибору сторінки, яку слід надіслати у відповідь. Ми використаємо цю техніку для створення додатка, що відповідає архітектурі MVC, про яку ми вже говорили.
- Після проходження фільтра [global.asax] запит клієнта передається на сторінку .aspx, яка його обробить. Пізніше ми побачимо, що запит можна пропускати через фільтр, що складається з декількох сторінок. Остання з них буде відповідати за відправку відповіді клієнту. Сторінки можуть додавати до початкового запиту клієнта інформацію, яку вони обчислили. Вони можуть зберігати цю інформацію у колекції Context.Items. Адже всі сторінки, задіяні в обробці запиту клієнта, мають доступ до цього сховища даних.
- Код різних сторінок має доступ до сховищ даних, якими є об’єкти [ApplicationA], [Session-1A], ... Слід пам’ятати, що веб-сервер одночасно обробляє декількох клієнтів для додатка A. Усі ці клієнти мають доступ до об’єкта [Application A]. Якщо їм потрібно змінити дані в цьому об’єкті, необхідно виконати синхронізацію клієнтів. Крім того, кожен клієнт XA має доступ до сховища даних [Session-XA]. Оскільки це сховище зарезервоване саме для нього, синхронізація тут не потрібна.
- Веб-сервер одночасно обслуговує кілька веб-додатків. Між клієнтами цих різних додатків немає жодних перешкод.
З цих пояснень слід винести такі висновки:
- у певний момент часу веб-сервер обслуговує одночасно декількох клієнтів. Це означає, що він не чекає завершення одного запиту, щоб обробити інший. Отже, у момент часу T на обробці перебувають кілька запитів, що належать різним клієнтам для різних додатків. Коди обробки, що виконуються одночасно всередині веб-сервера, іноді називають потоками виконання.
- потоки виконання клієнтів різних веб-додатків не взаємодіють між собою. Існує ізоляція.
- Потоки виконання клієнтів одного й того самого додатка можуть обмінюватися даними:
- потоки виконання запитів двох різних клієнтів (з різними токенами сеансу) можуть обмінюватися даними за допомогою об’єкта [Application].
- Потоки виконання послідовних запитів одного й того самого клієнта можуть обмінюватися даними за допомогою об’єкта [Session].
- потоки виконання послідовних сторінок, що обробляють один і той самий запит від певного клієнта, можуть обмінюватися даними за допомогою об’єкта [Context].
4.1.3.4. Приклад 2
Розглянемо новий приклад, що ілюструє те, про що щойно йшлося. Зберемо в одній папці такі файли:
[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)
' Спрацьовує під час запуску додатка
' ініціалізується лічильник клієнтів
Application.Item("nbRequêtes") = 0
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Викликається при запуску сесії
' Ініціалізація лічильника запитів
Session.Item("nbRequêtes") = 0
End Sub
End Class
Принцип роботи програми полягає у підрахунку загальної кількості запитів, зроблених до програми, та кількості запитів на одного клієнта. Під час запуску програми [Application_Start] лічильник запитів, зроблених до програми, обнуляється. Цей лічильник розміщується в області дії [Application], оскільки його значення має збільшуватися кожним клієнтом. Коли клієнт вперше звертається до [Session_Start], лічильник запитів, зроблених цим клієнтом, обнуляється. Цей лічильник розміщується в області дії [Session], оскільки він стосується лише одного конкретного клієнта.
Після виконання [global.asax] буде виконано наступний файл [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>
Він відображає три параметри, розраховані його контролером:
- ідентифікаційні дані клієнта за допомогою його сесійного токена: [jeton]
- загальна кількість запитів, надісланих до додатка: [nbRequêtesApplication]
- загальна кількість запитів, зроблених клієнтом, ідентифікованим у пункті 1: [nbRequêtesClient]
Ці три показники обчислюються в [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
' ще один запит до програми
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
' ще один запит у сеансі
Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
' Ініціалізація змінних представлення
nbRequêtesApplication = Application.Item("nbRequêtes").ToString
jeton = Session.SessionID
nbRequêtesClient = Session.Item("nbRequêtes").ToString
End Sub
End Class
Під час виконання [main.aspx.vb] ми обробляємо запит певного клієнта. Ми використовуємо об’єкт [Application] для збільшення кількості запитів у додатку, а об’єкт [Session] — для збільшення кількості запитів того клієнта, запит якого ми зараз обробляємо. Нагадаємо, що якщо всі клієнти одного й того самого додатка використовують спільний об’єкт [Application], то кожен із них має власний об’єкт [Session].
Ми тестуємо додаток, розмістивши чотири попередні файли в папці, яку називаємо <application-path>, і запускаємо сервер Cassini з параметрами (<application-path>,/aspnet/webapplia). Запускаємо перший браузер і вводимо URL-адресу [http://localhost/aspnet/webapplia/main.aspx]:

Ми робимо другий запит за допомогою кнопки [Reload]:

Ми запускаємо другий браузер, щоб звернутися до того самого URL-адреси. Для веб-сервера це новий клієнт:

Можна помітити, що токен сеансу змінився, а отже, ми маємо нового клієнта. Це відображається у кількості запитів клієнта. Тепер повернемося до першого браузера та знову надішлемо запит на ту саму URL-адресу:

Кількість запитів, зроблених до додатка, підрахована правильно.
4.1.3.5. Про необхідність синхронізації клієнтів додатка
У попередньому додатку лічильник запитів до додатка збільшується у процедурі [Form_Load] на сторінці [main.aspx] наступним чином:
' ще один запит до додатка
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
Ця інструкція, хоч і проста, вимагає для виконання декількох інструкцій процесора. Припустимо, що їх потрібно три:
- зчитування лічильника
- інкрементування лічильника
- перезапис лічильника
Веб-сервер працює на багатозадачній машині, що означає, що кожному завданню виділяється процесор на кілька мілісекунд, після чого воно втрачає доступ до нього, а потім отримує його знову, коли всі інші завдання також отримали свій квант часу. Припустимо, що два клієнти A і B одночасно надсилають запит до веб-сервера. Припустимо, що клієнт A проходить першим, потрапляє в процедуру [Form_Load] з [main.aspx.vb], зчитує значення лічильника (=100), а потім переривається, оскільки його квант часу вичерпано. Припустимо тепер, що настала черга клієнта B і з ним сталося те саме: він встиг прочитати значення лічильника (=100), але не встиг його збільшити. Клієнти A і B мають лічильники, що дорівнюють 100. Припустимо, що знову настає черга клієнта А: він збільшує свій лічильник, встановлює його значення на 101, а потім завершує роботу. Настає черга клієнта Б, який має у своєму розпорядженні старе значення лічильника, а не нове. Тому він також встановлює значення лічильника на 101 і завершує роботу. Тепер значення лічильника запитів додатка є помилковим.
Щоб проілюструвати цю проблему, повернемося до попереднього додатка, який ми модифікуємо наступним чином:
- файли [global.asax], [global.asax.vb] та [main.aspx] не змінюються
- файл [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
' ще один запит до додатка та сесії
' зчитування лічильників
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' очікування 5 с
Thread.Sleep(5000)
' інкрементування лічильників
nbRequêtesApplication += 1
nbRequêtesClient += 1
' запис значень лічильників
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' ініціалізація змінних відображення
jeton = Session.SessionID
End Sub
End Class
Зчитування показань лічильників було розділено на чотири етапи:
- зчитування лічильника
- переведення потоку виконання в режим очікування
- інкрементування лічильника
- перезапис лічильника
Розглянемо знову наших двох клієнтів А і В. Між етапом зчитування та етапом інкрементування лічильників запитів ми примушуємо потік виконання зупинитися на 5 секунд. Це матиме безпосереднім наслідком те, що він втратить доступ до процесора, який буде передано іншому завданню. Припустимо, що клієнт A проходить першим. Він зчитає значення N лічильника і буде перерваний на 5 секунд. Якщо протягом цього часу процесор буде у розпорядженні клієнта B, він повинен зчитати те саме значення N лічильника. У підсумку обидва клієнти повинні відобразити однакове значення лічильника, що є ненормальним.
Ми тестуємо додаток, розмістивши чотири попередні файли в папці, яку називаємо <application-path>, і запускаємо сервер Cassini з параметрами (<application-path>,/aspnet/webapplib). Готуємо два різних браузери з URL-адресою [http://localhost/aspnet/webapplib/main.aspx]. Ми запускаємо перший браузер, щоб він запросив URL, а потім, не чекаючи на відповідь, яка надійде через 5 секунд, запускаємо другий браузер. Трохи більше ніж через 5 секунд ми отримуємо такий результат:

Ми бачимо:
- що маємо двох різних клієнтів (не однаковий токен сесії)
- що кожен клієнт зробив запит
- що лічильник запитів до додатка мав би показувати 2 в одному з двох браузерів. Це не так.
Тепер проведемо ще один експеримент. У тому самому браузері ми надсилаємо п’ять запитів на URL-адресу [http://localhost/aspnet/webapplib/main.aspx]. І знову ми надсилаємо їх один за одним, не чекаючи на результати. Коли всі запити виконано, для останнього ми отримуємо такий результат:

Можна помітити:
- що всі 5 запитів були розцінені як такі, що надійшли від одного й того самого клієнта, оскільки лічильник запитів клієнта дорівнює 5. Хоча це не показано вище, можна констатувати, що токен сесії для всіх 5 запитів є однаковим.
- що лічильник запитів, зроблених до додатка, працює коректно.
Який висновок можна зробити? Нічого остаточного. Можливо, веб-сервер не починає виконувати запит від клієнта, якщо у того вже є запит, що виконується? Отже, одночасного виконання запитів від одного й того самого клієнта ніколи не відбуватиметься. Вони виконуватимуться один за одним. Це питання потрібно перевірити. Воно може залежати від типу використовуваного клієнта.
4.1.3.6. Синхронізація клієнтів
Проблема, виявлена в попередньому додатку, є класичною (але не простою для вирішення) проблемою ексклюзивного доступу до ресурсу. У нашому конкретному випадку потрібно забезпечити, щоб два клієнти A і B не могли одночасно перебувати в такій послідовності коду:
- читання лічильника
- інкрементування лічильника
- перезапис лічильника
Таку послідовність коду називають критичною послідовністю. Вона вимагає синхронізації потоків, які мають виконувати її одночасно. Платформа .NET пропонує різні інструменти для забезпечення цієї синхронізації. Тут ми будемо використовувати клас [Mutex].

Тут ми будемо використовувати лише такі конструктори та методи:
створює об’єкт синхронізації M | |
Потік T1, який виконує операцію M.WaitOne(), запитує право володіння об’єктом синхронізації M. Якщо м’ютекс M не утримується жодним потоком (що має місце на початку), його «надають» потоку T1, який його запросив. Якщо трохи пізніше потік T2 виконає ту саму операцію, він буде заблокований. Адже м’ютекс може належати лише одному потоку. Блокування буде знято, коли потік T1 звільнить м’ютекс M, який він утримує. Таким чином, кілька потоків можуть бути заблоковані в очікуванні м’ютекса M. | |
Пот T1, який виконує операцію M.ReleaseMutex(), відмовляється від володіння м'ютексом M. Коли потік T1 втратить доступ до процесора, система зможе передати його одному з потоків, що очікують на м'ютекс M. Лише один з них отримає його у свою чергу, а інші, що очікують на м'ютекс M, залишаться заблокованими |
Мутекс M керує доступом до спільного ресурсу R. Потік запитує ресурс R за допомогою функції M.WaitOne() і повертає його за допомогою функції M.ReleaseMutex(). Критична ділянка коду, яка має виконуватися лише одним потоком одночасно, є спільним ресурсом. Синхронізація виконання критичного фрагмента може здійснюватися таким чином:
де M — це об’єкт Mutex. Звісно, ніколи не слід забувати звільняти Mutex, що став непотрібним, щоб інший потік міг у свою чергу увійти в критичну секцію; інакше потоки, що очікують на м’ютекс, який ніколи не звільняється, ніколи не отримають доступу до процесора. Крім того, слід уникати ситуації взаємного блокування (deadlock), в якій два потоки очікують один на одного. Розглянемо такі дії, що відбуваються послідовно в часі:
- потік T1 отримує право володіння м'ютексом M1, щоб отримати доступ до спільного ресурсу R1
- потік T2 отримує володіння м'ютексом M2, щоб отримати доступ до спільного ресурсу R2
- потік T1 запитує м'ютекс M2. Він заблокований.
- потік T2 запитує м'ютекс M1. Він заблокований.
У цьому випадку потоки T1 та T2 очікують один на одного. Така ситуація виникає, коли потоки потребують двох спільних ресурсів: ресурсу R1, що контролюється м'ютексом M1, та ресурсу R2, що контролюється м'ютексом M2. Одним із можливих рішень є одночасне запитування обох ресурсів за допомогою єдиного мьютекса M. Однак це не завжди можливо, зокрема якщо це призводить до тривалого блокування ресурсу, що вимагає значних ресурсів. Інший варіант полягає в тому, що потік, який володіє M1 і не може отримати M2, звільняє M1, щоб уникнути взаємного блокування.
Якщо застосувати на практиці те, що ми щойно дізналися, наш код виглядатиме так:
- файли [global.asax] та [main.aspx] не змінюються
- файл [global.asax.vb] набуває такого вигляду:
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)
' Викликається під час запуску програми
' ініціалізація лічильника клієнтів
Application.Item("nbRequêtes") = 0
' створення блокування синхронізації
Application.Item("verrou") = New Mutex
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Викликається під час запуску сеансу
' ініціалізація лічильника запитів
Session.Item("nbRequêtes") = 0
End Sub
End Class
Єдиною зміною є створення об'єкта [Mutex], який клієнти використовуватимуть для синхронізації. Оскільки він має бути доступним для всіх клієнтів, його розміщено в об'єкті [Application].
- Файл [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
' ще один запит для додатка та сеансу
' вхід у критичну секцію — отримання блокування синхронізації
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' запит на самостійний вхід у наступну критичну секцію
verrou.WaitOne()
' читання лічильників
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' очікування 5 с
Thread.Sleep(5000)
' інкрементування лічильників
nbRequêtesApplication += 1
nbRequêtesClient += 1
' запис значень лічильників
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' надається доступ до критичного сегмента
verrou.ReleaseMutex()
' ініціалізація змінних відображення
jeton = Session.SessionID
End Sub
End Class
Бачимо, що клієнт:
- просить про дозвіл увійти до критичної секції самостійно. Для цього він запитує ексклюзивне володіння м'ютексом [verrou]
- звільняє м'ютекс [verrou] наприкінці критичної секції, щоб інший клієнт міг у свою чергу увійти до критичної секції.
Ми тестуємо додаток, розмістивши чотири попередні файли в папці, яку називаємо <application-path>, і запускаємо сервер Cassini з параметрами (<application-path>,/aspnet/webapplic). Готуємо два різних веб-браузери з URL-адресою [http://localhost/aspnet/webapplic/main.aspx]. Запускаємо перший браузер, щоб він запросив URL, а потім, не чекаючи на відповідь, яка надійде через 5 секунд, запускаємо другий браузер. Трохи більше ніж через 5 секунд отримуємо такий результат:

Цього разу лічильник запитів додатка показує правильне значення.
З цієї тривалої демонстрації слід винести урок про абсолютну необхідність синхронізації клієнтів одного й того самого веб-додатку, якщо вони мають оновлювати елементи, спільні для всіх клієнтів.
4.1.3.7. Управління токеном сеансу
Ми вже неодноразово згадували про сесійний токен, яким обмінюються клієнт і веб-сервер. Нагадаємо його принцип роботи:
- клієнт надсилає перший запит на сервер. Він не надсилає токен сеансу.
- Через відсутність сесійного токена у запиті сервер розпізнає нового клієнта та присвоює йому токен. З цим токеном також пов’язаний об’єкт [Session], який використовуватиметься для зберігання інформації, що стосується саме цього клієнта. Токен супроводжуватиме всі запити цього клієнта. Він буде включений у заголовки HTTP у відповіді на перший запит клієнта.
- Тепер клієнт знає свій сесійний токен. Він надсилатиме його у заголовках HTTP кожного наступного запиту, який він надсилатиме до веб-сервера. Завдяки токену сервер зможе знайти об’єкт [Session], пов’язаний із цим клієнтом.
Щоб проілюструвати цей механізм, повернемося до попереднього додатка, змінивши лише один файл [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
' ще один запит для додатка та сеансу
' вхід у критичний розділ — отримання блокування синхронізації
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' запит на вхід у наступну секцію в поодинокому режимі
verrou.WaitOne()
' зчитування лічильників
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' очікування 5 с
Thread.Sleep(5000)
' інкрементування лічильників
nbRequêtesApplication += 1
nbRequêtesClient += 1
' запис значень лічильників
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' надано доступ до критичного розділу
verrou.ReleaseMutex()
' ініціалізація змінних відображення
jeton = Session.SessionID
End Sub
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' записування запиту клієнта у request.txt у папці додатка
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
End Class
Коли відбувається подія [Page_Init], ми зберігаємо запит клієнта в папці додатка. Нагадаємо кілька моментів:
- [TemplateSourceDirectory] — це віртуальний шлях до сторінки, що виконується,
- MapPath (TemplateSourceDirectory) — це відповідний фізичний шлях. Це дозволяє нам побудувати фізичний шлях до файлу, який потрібно створити,
- [Request] — це об’єкт, що представляє запит, який зараз обробляється. Цей об’єкт було побудовано на основі необробленого запиту, надісланого клієнтом, c.a.d — послідовності рядків тексту у вигляді:

- Request.Save([FileName]) зберігає весь запит клієнта (заголовки HTTP і, можливо, документ, що йде за ними) у файлі, шлях до якого передається як параметр.
Таким чином, ми зможемо точно дізнатися, яким саме був запит клієнта. Ми тестуємо додаток, розмістивши чотири попередні файли в папці, яку називаємо <application-path>, і запускаємо сервер Cassini з параметрами (<application-path>,/aspnet/session1). Потім за допомогою браузера ми надсилаємо запит на URL
[http://localhost/aspnet/session1/main.aspx]. Отримуємо такий результат:

Ми використовуємо файл [request.txt], збережений за допомогою [main.aspx.vb], щоб отримати доступ до запиту браузера:
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
Ми бачимо, що браузер надіслав запит на URL [/aspnet/session1/main.aspx], а також іншу інформацію, про яку ми вже говорили в попередньому розділі. У ньому не видно токена сеансу. Сторінка, отримана у відповідь, показує, що сервер створив токен сеансу. Поки що невідомо, чи отримав його браузер. Тепер надішлімо другий запит тим самим браузером (Reload). Ми отримуємо таку нову відповідь:

Відстеження сеансу дійсно відбувається, оскільки кількість запитів у сеансі була правильно збільшена. Тепер розглянемо вміст файлу [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
Можна помітити, що для цього другого запиту браузер надіслав серверу новий заголовок HTTP [Cookie:], який визначає інформацію під назвою [ASP.NET_SessionId] і має за значення сесійний токен, який ми бачили у відповіді на перший запит. Завдяки цьому токену веб-сервер пов’яже цей новий запит з об’єктом [Session], ідентифікованим токеном [y153tk45sise0lrhdzrf22m3], і знайде відповідний лічильник запитів.
Ми досі не знаємо, за допомогою якого механізму сервер надіслав токен клієнту, оскільки не маємо доступу до відповіді HTTP від сервера. Нагадаємо, що вона має ту саму структуру, що й запит клієнта, а саме набір рядків тексту у формі:

Ми мали нагоду скористатися веб-клієнтом, який надавав нам доступ до відповіді HTTP від веб-сервера, — клієнтом curl. Ми знову використовуємо його у вікні командного рядка, щоб надіслати запит на той самий URL, що й попередній браузер:
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>
Ми отримали відповідь на наше запитання. Веб-сервер надсилає токен сеансу у вигляді заголовка HTTP [Set-Cookie:]:
Зробимо той самий запит, не надсилаючи токен сеансу. Отримаємо таку відповідь:
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>
Оскільки ми не надіслали токен сеансу, сервер не зміг нас ідентифікувати і надав нам новий токен. Щоб продовжити розпочатий сеанс, клієнт повинен надіслати серверу отриманий токен сеансу. Ми зробимо це тут, використовуючи опцію [--cookie clé=valeur] у curl, яка згенерує заголовок HTTP [Cookie: clé=valeur]. Ми бачили, що браузер надіслав цей заголовок HTTP під час свого другого запиту.
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>
Слід звернути увагу на кілька моментів:
- лічильник клієнтських запитів дійсно збільшився, що свідчить про те, що сервер правильно розпізнав наш токен.
- токен сеансу, відображений на сторінці, дійсно відповідає тому, який ми надіслали
- токен сеансу більше не міститься в заголовках HTTP, що надсилаються веб-сервером. Справді, сервер надсилає його лише один раз: під час генерації токена на початку нового сеансу. Після того, як клієнт отримав свій токен, він сам вирішує, коли його використовувати для ідентифікації.
Ніщо не заважає клієнту використовувати кілька токенів сеансу, як показано в наступному прикладі з [curl], де ми використовуємо токен, отриманий під час нашого першого запиту (запит № 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>
Що означає цей приклад? Ми надіслали токен, отриманий трохи раніше. Коли веб-сервер створює токен, він зберігає його доти, доки клієнт, пов’язаний із цим токеном, продовжує надсилати йому запити. Після певного періоду бездіяльності (за замовчуванням 20 хвилин для IIS) токен видаляється. Попередній приклад показує, що ми використовували токен, який ще був активним.
Може виникнути цікавість дізнатися, якими були запити HTTP від клієнта [curl] під час усіх цих операцій. Ми знаємо, що вони були записані у файлі [request.txt]. Ось останній із них:
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
Тут дійсно є заголовок HTTP, який надсилає сесійний токен.
Інформація, що передається сервером через заголовки HTTP та [Set-Cookie:], називається файлами cookie. Сервер може використовувати цей механізм для передачі інформації, відмінної від сесійного токена. Коли сервер S передає кукі клієнту, він також вказує термін дії D цього кукі та пов’язаний з ним параметр U. Для клієнта це означає, що коли він надсилає серверу S запит на URL-адресу у вигляді /U/шлях, він може надіслати файл cookie, якщо не отримував його протягом часу, що перевищує D. Ніщо не заважає клієнту не дотримуватися цього кодексу поведінки. Натомість веб-браузери дотримуються цього кодексу. Деякі браузери надають доступ до вмісту файлів cookie, які вони отримують. Це стосується, зокрема, браузера Mozilla. Ось, наприклад, інформація, пов’язана з файлом cookie, надісланим сервером у попередньому прикладі:

У ньому міститься:
- назва файлу cookie [ASP.NET_SessionId]
- його значення [y153...m3]
- пристрій, з яким він пов’язаний — [localhost]
- URL-адресу, з якою він пов'язаний: [/]
- термін його дії — [at end of session]
Отже, браузер надсилатиме сесійний токен щоразу, коли він запитуватиме URL у формі [http://localhost/...], c.a.d. кожного разу, коли він надсилатиме запит на URL-адресу до веб-сервера машини [localhost]. Термін дії файлу cookie дорівнює тривалості сеансу. Для браузера це означає, що файл cookie ніколи не втрачає чинності. Він надсилатиме його кожного разу, коли надсилатиме запит на URL-адресу машини [localhost]. Отже, якщо браузер отримає сесійний токен у день D, його закрити, а наступного дня знову запустити, він надішле цей сесійний токен (який було збережено у файлі). Сервер отримає цей токен, якого у нього вже немає, оскільки термін дії сесійного токена на сервері обмежений (20 хвилин на IIS). Тому він розпочне нову сесію.
У браузері можна вимкнути використання файлів cookie. У цьому випадку клієнт отримує токен сеансу, але не надсилає його назад, що унеможливлює відстеження сеансу. Щоб продемонструвати це, ми вимикаємо використання файлів cookie у нашому браузері (у даному випадку — Mozilla):

Крім того, ми видаляємо всі існуючі файли cookie:

Зробивши це, ми перезапускаємо сервер Cassini, щоб почати з нуля, і за допомогою браузера знову запитуємо URL-адресу [http://localhost/aspnet/session1/main.aspx]:

Перевіримо, чи браузер зберіг файл cookie:

Ми бачимо, що браузер не зберіг файл cookie з токеном сеансу, який йому надіслав сервер. Отже, можна очікувати, що відстеження сеансу не відбуватиметься. Ми знову запитуємо той самий URL-адресу (Reload):

Результат відповідає очікуваному. Браузер не надіслав сесійний токен, який він, хоча й отримав, але не зберіг. Отже, сервер розпочав нову сесію з новим токеном. З цього прикладу можна зробити висновок, що наша політика відстеження сесій порушується, якщо користувач вимкнув використання файлів cookie у своєму браузері. Однак існує й інший спосіб, окрім файлів cookie, для обміну токеном сеансу між сервером і клієнтом. Дійсно, можна повідомити веб-серверу, що додаток працює без файлів cookie. Це робиться за допомогою конфігураційного файлу [web.config]:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<system.web>
<sessionState cookieless="true" timeout="10" />
</system.web>
</configuration>
Наведений вище файл конфігурації вказує, що додаток працюватиме без файлів cookie (cookieless="true") і що максимальний час бездіяльності сесійного токена становить 10 хвилин (timeout="10"). Після закінчення цього часу сесія, пов'язана з токеном, буде знищена. Процес обміну сесійним токеном між сервером і клієнтом відбуватиметься наступним чином:
- клієнт надсилає запит на URL-адресу [http://machine:port/V/chemin], де V — це віртуальна папка веб-сервера
- сервер генерує токен J і відповідає клієнту, щоб той перенаправився на URL-адресу [http://machine:port/V/(J)/chemin]. Таким чином, він розмістив токен у запитуваній URL-адресі, одразу після віртуальної папки V
- клієнт виконує це перенаправлення та надсилає запит на нову URL-адресу URL [http://machine:port/V/(J)/chemin].
- Сервер відповідає на цей запит і надсилає сторінку відповіді.
Проілюструємо ці різні моменти. Ми розміщуємо весь попередній додаток у новій папці <application-path>. У цю ж папку ми поміщаємо попередній файл [web.config]. Крім того, ми змінюємо код інтерфейсу [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>
<a href="main.aspx">Recharger l'application</a>
</body>
</HTML>
Це посилання веде на сторінку [main.aspx] і, отже, є еквівалентом кнопки (Оновити) у браузері. Сервер Cassini запускається з параметрами (<application-path>,/session2). Ми відхиляємося від нашої звичної практики, яка полягала у зазначенні віртуальної папки [/aspnet/XX]. Адже через вставлення токена сеансу в URL-адресу віртуальна папка повинна містити лише один елемент /XX. Спочатку ми використовуємо клієнт [curl] для запиту 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>
Ми бачимо, що сервер відповідає заголовком HTTP [HTTP/1.1 302 Found] замість [HTTP/1.1 200 OK]. Це заголовок, який вимагає від клієнта перенаправлення на URL-адресу, вказану в заголовку HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. У URL-адресі перенаправлення видно сесійний токен, який було вставлено. Браузер, отримавши цю відповідь, надсилає запит на нову URL-адресу, і цей процес відбувається прозоро для користувача, який не бачить нового запиту. У разі, якщо браузер не може самостійно обробити перенаправлення, за кодом HTTP, наведеним вище, надсилається документ HTML. У ньому міститься посилання на URL-адресу перенаправлення, на яке користувач може натиснути.
Тепер зробимо те саме з браузером, у якому вимкнено файли cookie. Ми знову запитуємо URL-адресу [http://localhost/session2/main.aspx]. Отримуємо таку відповідь від сервера:

По-перше, звернімо увагу, що URL-адреса, яку відображає браузер, не збігається з тією, яку ми запитували. Це свідчить про те, що відбулося перенаправлення. Дійсно, браузер завжди відображає URL-адресу URL останнього отриманого документа. Отже, якщо він не відображає URL-адресу [http://localhost/session2/main.aspx], це означає, що йому було наказано перенаправитися на іншу URL-адресу. Може відбутися кілька перенаправлень. URL-адреса, яку відображає браузер, — це URL-адреса останнього перенаправлення. Ми можемо помітити, що токен сеансу присутній у URL-адресі, яку відображає браузер. Це видно, оскільки цей токен також відображається нашою програмою на сторінці.
Нагадаємо код посилання, яке було розміщено на сторінці:
<a href="main.aspx">Recharger l'application</a>
Це відносне посилання, оскільки воно не починається зі знака /, який зробив би його абсолютним. Відносне до чого? Щоб зрозуміти це, потрібно повернутися до URL-адреси документа, що зараз відображається: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Відносні посилання, які містяться в цьому документі, будуть відносно шляху [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]. Отже, наведене вище посилання еквівалентне такому:
<a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>
Саме це показує браузер, якщо навести курсор миші на посилання:

Якщо ми натиснемо на посилання [Recharger l'application], то буде викликано URL-адресу
[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. Отже, сервер отримає токен сеансу і зможе знайти пов’язану з ним інформацію. Саме це показує відповідь браузера:

Запам’ятаємо: якщо нам потрібно відстежувати сесію у веб-додатку, але ми не впевнені, що браузери клієнтів цього додатка дозволять використання файлів cookie, то
- необхідно налаштувати додаток так, щоб він працював без файлів cookie
- сторінки додатка повинні містити відносні, а не абсолютні посилання
4.2. Отримання інформації з клієнтського запиту
4.2.1. Цикл «запит-відповідь» у веб-клієнт-серверній моделі
Нагадаємо тут про клієнтсько-серверний контекст веб-додатка:

Запит клієнта до веб-додатка обробляється наступним чином:
- клієнт відкриває TCP/IP-з’єднання з портом P веб-сервісу на машині M, на якій розміщено веб-додаток
- через це з’єднання він надсилає послідовність рядків тексту згідно з протоколом HTTP. Ця сукупність рядків утворює так званий запит клієнта. Він має такий вигляд:

Після відправлення запиту клієнт очікує на відповідь.
- Перший рядок заголовків HTTP визначає дію, яку потрібно виконати веб-серверу. Він може мати кілька форм:
- GET url HTTP/<версія>, де <версія> наразі дорівнює 1.0 або 1.1. У цьому випадку запит не містить частини [Document]
- POST url HTTP/<version>. У цьому випадку запит містить частину [Document], найчастіше це список даних, призначених для веб-додатка
- PUT url HTTP/<версія>. Клієнт надсилає документ у частині [Document] і хоче зберегти його на сервері за адресою url
Коли клієнт бажає передати інформацію веб-додатку, до якого він підключився, у нього є, в основному, два способи:
- (продовження)
- його запит має вигляд [GET url_enrichie HTTP/<version>], де url_enrichie має вигляд [url?param1=val1¶m2=val2&...]. Крім url, клієнт передає низку даних у форматі [clé=valeur].
- його запит має вигляд [POST url HTTP/<version>]. У частині [Document] він передає інформацію у тому ж форматі, що й раніше: [param1=val1¶m2=val2&...].
- На сервері весь ланцюжок обробки запиту клієнта має доступ до нього через глобальний об’єкт, який називається Request. Веб-сервер розмістив у цьому об’єкті весь запит клієнта у формі, яку ми зараз розглянемо. Запитана програма обробить цей об’єкт і сформує відповідь для клієнта. Ця відповідь доступна в глобальному об’єкті під назвою Response. Роль веб-програми полягає у створенні об’єкта [Response] на основі отриманого об’єкта [Request]. Ланцюг обробки також має у своєму розпорядженні глобальні об’єкти [Application] та [Session], про які ми вже згадували і які дозволять йому обмінюватися даними між різними клієнтами (Application) або між послідовними запитами одного й того самого клієнта (Session).
- Додаток надішле свою відповідь на сервер за допомогою об’єкта [Response]. Ця відповідь, потрапивши в мережу, матиме такий вигляд: HTTP:

Після відправлення цієї відповіді сервер закриє мережеве з’єднання на прийом (якщо тільки клієнт не вказав йому не робити цього).
- Клієнт отримає відповідь і, у свою чергу, закриє з’єднання (під час передачі даних). Що буде зроблено з цією відповіддю, залежить від типу клієнта. Якщо це веб-браузер, а отриманий документ має формат HTML, він буде відображений. Якщо клієнт — це програма, відповідь буде проаналізована та оброблена.
- Той факт, що після циклу «запит-відповідь» з’єднання, яке пов’язувало клієнта з сервером, закривається, робить протокол HTTP безстатусним. Під час наступного запиту клієнт встановить нове мережеве з’єднання з тим самим сервером. Оскільки це вже не те саме мережеве з’єднання, сервер не має жодної можливості (на рівні TCP/IP та HTTP) пов’язати це нове з’єднання з попереднім. Саме система сесійних токенів дозволить встановити таке зв’язування.
4.2.2. Отримання інформації, переданої клієнтом
Тепер розглянемо деякі властивості та методи об’єкта [Request], які дозволяють коду додатка отримати доступ до запиту клієнта і, отже, до інформації, яку він передав. Об’єкт [Request] належить до типу [HttpRequest]:

Цей клас має численні властивості та методи. Нас цікавлять властивості HttpMethod, QueryString, Form та Params, які дозволять нам отримати доступ до елементів інформаційного ланцюга [param1=val1¶m2=val2&...].
методи запиту клієнта: GET, POST, HEAD, ... | |
збір елементів ланцюжка запиту param1=val1¶m2=val2&.. з першого рядка HTTP [méthode]?param1=val1¶m2=val2&... де [méthode] може бути GET, POST, HEAD. | |
збірка елементів рядка запиту param1=val1¶m2=val2&.., що містяться в частині [Document] запиту (метод POST). | |
об'єднує кілька колекцій: QueryString, Form, ServerVariables, Cookies в одну колекцію. |
4.2.3. Приклад 1
Застосуємо ці елементи на першому прикладі. Додаток матиме лише один елемент [main.aspx]. Код представлення [main.aspx] буде таким:
<%@ 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>
На сторінці відображаються три значення [méthode, nom, age], обчислені її контролером [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
' запиту клієнта зберігається у файлі request.txt у папці додатка
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
' отримуємо параметри запиту
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
Коли сторінка завантажується (Form_Load), інформація [nom, age] отримується із запиту клієнта. Її шукають у двох колекціях: [QueryString] та [Form]. . Крім того, у файлі [Page_Init] ми зберігаємо запит клієнта, щоб мати змогу перевірити, що саме він надіслав. Ми розміщуємо ці два файли в папці <application-path> і запускаємо сервер Cassini з параметрами (<application-path>,/request1), а потім за допомогою браузера надсилаємо запит на URL-адресу
[http://localhost/request1/main.aspx?nom=tintin&age=27]. Отримуємо таку відповідь:

Інформація, передана клієнтом, була отримана правильно. Запит браузера, збережений у файлі [request.txt], має такий вигляд:
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
Бачимо, що браузер відправив запит GET. Щоб відправити запит POST, ми скористаємося клієнтом [curl]. У вікні командного рядка вводимо таку команду:
щоб відобразити заголовки HTTP у відповіді | |
щоб надіслати інформацію «param=значення» за допомогою POST |
Відповідь сервера така:
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>
Сервер знову успішно отримав параметри, надіслані цього разу файлом POST. Щоб переконатися в цьому, можна перевірити вміст файлу [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
Клієнт [curl] успішно виконав POST. Тепер поєднаємо ці два методи передачі інформації. Ми вставляємо [age] у запитуваний URL-адресу, а [nom] — у відправлений документ:
Запит, надісланий за допомогою [curl], має такий вигляд (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
Бачимо, що вік передано в запитуваному URL-адресі. Його можна отримати в колекції [QueryString]. Ім'я передано в документі, надісланому на цю URL-адресу. Його можна отримати в колекції [Form]. Відповідь, отримана клієнтом [curl]:
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
Зрештою, не надсилаємо жодної інформації на сервер:
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>
Читачеві пропонується ще раз ознайомитися з кодом контролера [main.aspx.vb], щоб зрозуміти цю відповідь.
4.2.4. Приклад 2
Клієнт може надіслати кілька значень для одного й того самого ключа. Що ж станеться, якщо в попередньому прикладі ми запитаємо URL-адресу [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou], де двічі зустрічається ключ [nom]? Спробуймо це в браузері:

Наш додаток успішно отримав обидва значення, пов’язані з ключем [nom]. Відображення є дещо оманливим. Воно було отримано за допомогою команди
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
Метод [ToString] згенерував рядок [tintin,milou], який і було відображено. Це приховує той факт, що насправді об’єкт [Request.QueryString("nom")] є масивом рядків {"tintin","milou"}. Наступний приклад ілюструє це. Сторінка-презентація [main.aspx] матиме такий вигляд:
<%@ 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>
На цій сторінці є нововведення, які використовують так звані серверні елементи управління. Вони позначаються атрибутом [runat="server"]. Поки що зарано вводити поняття «серверний елемент управління». Достатньо знати, що тут:
- сторінка містить два списки (теги <asp:listbox>)
- що ці списки є об’єктами (lstQueryString, lstForm) типу [ListBox], які будуть створені контролером сторінки
- що ці об’єкти існують лише на веб-сервері. Під час відправлення відповіді вони будуть перетворені на звичайні теги HTML, які клієнт зможе зрозуміти. Таким чином, об’єкт [listbox] буде перетворений (це також називають «рендерингом») на теги HTML <select> та <option>.
- Головна мета цих об’єктів полягає в тому, щоб позбавити код представлення від будь-якого коду VB, який залишається обмеженим контролером.
Контролер [main.aspx.vb], відповідальний за побудову двох об’єктів [lstQueryString] та [lstForm], має такий вигляд:
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
' запиту клієнта зберігається у файлі request.txt у папці додатка
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
' отримуємо весь набір даних з 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
' Спочатку список інформації порожній
Dim infos As New ArrayList
' отримуємо ключі з колекції
Dim clés() As String = data.AllKeys
' проходимо по масиву ключів
Dim valeurs() As String
For Each clé As String In clés
' значення, пов’язані з ключем
valeurs = data.GetValues(clé)
' лише одне значення?
If valeurs.Length = 1 Then
infos.Add(clé + "=" + valeurs(0))
Else
' кілька значень
For ivalue As Integer = 0 To valeurs.Length - 1
infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
Next
End If
Next
' повертаємо результат
Return infos
End Function
End Class
Основні моменти цього коду такі:
- у [Form_Load] сторінка отримує дві колекції: [QueryString] та [Form]. Вона використовує функцію [getValeurs], щоб помістити вміст цих двох колекцій у два об’єкти типу [ArrayList], які міститимуть символьні рядки типу [clé=valeur], якщо ключ колекції пов’язаний з одним значенням, або [clé(i)=valeur], якщо ключ пов’язаний з кількома значеннями.
- Кожен з об’єктів [ArrayList] потім прив’язується до одного з об’єктів [ListBox] на сторінці презентації за допомогою двох інструкцій:
- [ListBox.DataSource=ArrayList] та [ListBox.DataBind]. Остання інструкція переносить елементи з [DataSource] до колекції [Items] об’єкта [ListBox]
Слід зауважити, що жоден із двох об’єктів [ListBox] не створюється явно за допомогою операції [New]. Звідси можна зробити висновок, що за наявності тегу <asp:listbox id="xx">...<asp:listbox/> веб-сервер самостійно створює об’єкт [ListBox], на який посилається атрибут [id] цього тегу.
- Функція [getValeurs] використовує об’єкт типу [NameValueCollection], який їй передається як параметр, для отримання результату типу [ArrayList].
Ми розміщуємо обидва попередні файли в папці <application-path> і запускаємо сервер Cassini з параметрами (<application-path>,/request2), а потім надсилаємо запит на URL-адресу
[http://localhost/request2/main.aspx?nom=tintin&age=27]. Отримуємо таку відповідь:

Тепер надсилаємо запит на URL-адресу, де ключ [nom] зустрічається двічі:

Ми бачимо, що об’єкт [Request.QueryString("nom")) дійсно був масивом. У цьому випадку запити надсилалися методом GET. Ми використовуємо клієнт [curl] для виконання запиту 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>
Як бачимо, клієнт отримує стандартний код HTML для обох списків на сторінці. З’являється інформація, яку ми самі не вводили, наприклад приховане поле [_VIEWSTATE]. Ця інформація була згенерована тегами <asp:xx runat="server>. Нам доведеться навчитися ними користуватися.
4.3. Впровадження архітектури MVC
4.3.1. Концепція
Завершимо цей довгий розділ реалізацією додатка, побудованого за моделлю MVC (Model-View-Controller). Такий веб-додаток, побудований за цією моделлю, виглядає так:

- клієнт надсилає свої запити до певного компонента додатка, який називається контролером
- контролер аналізує запит клієнта та забезпечує його виконання. Для цього він використовує класи, що містять бізнес-логіку додатка, та класи доступу до даних.
- залежно від результату виконання запиту контролер вирішує, яку саме сторінку надіслати клієнту у відповідь
У нашій моделі всі запити проходять через єдиний контролер, який є «диригентом» усього веб-додатка. Перевага цієї моделі полягає в тому, що в контролері можна об’єднати все, що потрібно зробити перед кожним запитом. Припустимо, наприклад, що додаток вимагає автентифікації. Вона виконується лише один раз. Після успішної аутентифікації додаток збере в сесії інформацію про користувача, який щойно пройшов аутентифікацію. Оскільки клієнт може безпосередньо завантажити сторінку додатка без аутентифікації, кожна сторінка повинна перевіряти в сесії, чи дійсно аутентифікація відбулася. Якщо всі запити проходять через єдиний контролер, саме він може виконувати цю роботу. Сторінки, до яких, можливо, буде передано запит, не повинні цього робити.
4.3.2. Управління додатком MVC без сесії
З того, що ми бачили досі, можна припустити, що файл [global.asax] міг би виконувати роль контролера. Адже відомо, що всі запити проходять через нього. Отже, він має всі можливості для контролю. Наступний додаток використовує його саме для цієї мети. Його віртуальний шлях буде [http://localhost/mvc1/main.aspx]. Щоб вказати, чого він хоче, клієнт додасть до URL-адреси параметр action=значення. Залежно від значення параметра [action] контролер [global.asax] перенаправить запит на певну сторінку:
- [main.aspx], якщо параметр action не визначено або якщо action=main
- [action1.aspx], якщо action=action1
- [inconnu.aspx], якщо action не підпадає під випадки 1 і 2
Сторінки [main.aspx, action1.aspx, inconnu.aspx] просто відображають значення [action], яке спричинило їхнє відображення. Нижче наведено перелік восьми файлів цього додатка з коментарями, де це необхідно:
[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)
' отримуємо дію, яку потрібно виконати
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' встановлюємо дію в контекст запиту
Context.Items("action") = action
' виконуємо дію
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
Важливі моменти:
- ми перехоплюємо всі запити клієнта в процедурі [Application_BeginRequest], яка автоматично виконується при запуску кожного нового запиту до додатка.
- У цій процедурі ми маємо доступ до об’єкта [Request], який є відображенням запиту HTTP від клієнта. Оскільки ми очікуємо URL-адресу у форматі [http://localhost/mvc1/main.aspx?action=xx], ми шукаємо ключ [action] у колекції [Request.QueryString]. Якщо його там немає, за замовчуванням встановлюємо значення дії, рівне «main».
- Значення параметра [action] поміщається в об’єкт [Context]. Як і об’єкти [Application, Session, Request, Response, Server], цей об’єкт є глобальним і доступним у будь-якому коді. Цей об’єкт передається від сторінки до сторінки, якщо запит обробляється кількома сторінками, як це буде в даному випадку. Він видаляється, щойно відповідь надіслано клієнту. Отже, термін його існування дорівнює тривалості обробки запиту.
- Залежно від значення параметра [action] запит передається на відповідну сторінку. Для цього використовується глобальний об’єкт [Server], який завдяки своєму методу дозволяє передати поточний запит на іншу сторінку. Його перший параметр — це назва цільової сторінки, другий — логічне значення, що вказує, чи слід передавати на цільову сторінку колекції [QueryString] та [Form]. У цьому випадку — так.
Файли [main.aspx] та [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
' отримуємо поточну дію
action = Me.Context.Items("action").ToString
End Sub
End Class
Контролер [main.aspx.vb] просто отримує значення ключа [action] з контексту, яке відображається кодом презентації. Тут ми прагнемо продемонструвати передачу об’єкта [Context] між різними сторінками, що обробляють один і той самий запит клієнта. Сторінки [action1.aspx] та [inconnu.aspx] працюють аналогічно:
[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
' отримуємо поточну дію
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
' отримуємо поточну дію
action = Me.Context.Items("action").ToString
End Sub
End Class
Для тестування попередні документи розміщуються в папці <application-path>, а Cassini запускається з параметрами (<application-path>,/mvc1). Ми запитуємо URL-адресу [http://localhost/mvc1/main.aspx]:

Запит не надіслав жодних параметрів [action]. Код контролера додатка [global.asax.vb] спричинив виведення сторінки [main.aspx]. Тепер ми запитуємо URL-адресу [http://localhost/mvc1/main.aspx?action=action1]:

Код контролера додатка [global.asax.vb] згенерував сторінку [action1.aspx]. Тепер ми запитуємо URL-адресу [http://localhost/mvc1/main.aspx?action=xx]:

Дія не була розпізнана, і контролер [global.asax.vb] вивів сторінку [inconnu.aspx].
4.3.3. Управління додатком MVC із сесією
У більшості випадків різні запити клієнта до додатка повинні обмінюватися інформацією. Ми розглянули можливе рішення цієї проблеми: зберігати інформацію, що підлягає обміну, в об’єкті [Session] запиту. Цей об’єкт дійсно є спільним для всіх запитів і здатний зберігати інформацію у формі (ключ, значення), де ключ має тип [String], а значення — будь-який тип, похідний від [Object].
У наведеному вище прикладі різні сторінки, пов’язані з різними діями, викликалися в процедурі [Application_BeginRequest] файлу [global.asax.vb]:
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' отримуємо дію, яку потрібно виконати
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' встановлюємо дію в контекст запиту
Context.Items("action") = action
' виконується дія
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
Виявляється, що в процедурі [Application_BeginRequest] об’єкт [Session] недоступний. Те саме стосується сторінки, на яку передається виконання. Тому цей шаблон не можна використовувати для додатка з сесією. Ми можемо призначити контролером будь-яку сторінку, наприклад [default.aspx]. Файли [global.asax, global.asax.vb] при цьому зникають і замінюються файлами [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
' отримуємо дію, яку потрібно виконати
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' встановлюємо дію в контекст запиту
Context.Items("action") = action
' отримуємо попередню дію, якщо вона існує
Context.Items("actionPrec") = Session.Item("actionPrec")
If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
' поточна дія зберігається в сесії
Session.Item("actionPrec") = action
' виконується дія
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
Щоб продемонструвати механізм сеансу, на різних сторінках, окрім поточної дії, буде відображатися також попередня дія. Для послідовності дій A1, A2, ..., An, коли відбувається дія Ai, контролер, наведений вище:
- додає поточну дію Ai до контексту
- знаходить у сесії попередню дію Ai-1. У разі її відсутності (як у випадку дії A1) встановлює порожній рядок для попередньої дії.
- включає поточну дію Ai до сесії, замінивши нею Ai-1
- передає виконання на відповідну сторінку
Три сторінки додатка такі:
[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>
Оскільки на всіх трьох сторінках відображається одна й та сама інформація [action, actionPrec], для них усіх трьох можна використовувати один і той самий контролер сторінки. Тому всі вони походять від класу [main] із файлу [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
' отримуємо поточну дію
action = Me.Context.Items("action").ToString
' та попередню дію
actionPrec = Me.Context.Items("actionPrec").ToString
End Sub
End Class
Вищезазначений код просто отримує інформацію, розміщену в контексті контролером додатка [default.aspx.vb].
Усі ці файли розміщуються в <application-path>, а Cassini запускається з параметрами (<application-path>,/mvc2). Спочатку запитується URL-адреса [http://localhost/mvc2]:

URL-адреса [http://localhost/mvc2] вказує на папку. Ми знаємо, що в цьому випадку сервер повертає документ [default.aspx] з цієї папки, якщо він існує. Тут не було вказано жодної дії. Тому виконалася дія [main]. Перейдемо до дії [action1]:

Поточна та попередня дії були правильно ідентифіковані. Перейдемо до дії [xx]:

4.4. Conclusion
Тепер ми маємо базові елементи, на основі яких будується будь-який додаток ASP.ET. Однак нам залишається ввести ще одне важливе поняття: поняття форми. Цьому присвячений наступний розділ.