Внутри ASP.NET

Автор: Mika Soukhov
The RSDN Group

Источник: RSDN Magazine #6-2003
Опубликовано: 07.08.2004
Версия текста: 1.0
Вступление
От неуправляемого к управляемому. От IIS к ASP.NET
ISAPI расширения
ASP.NET Hosting Runtime
HTTP-конвейеры
Инициализация HttpApplication
Обработка запроса
Завершение работы HttpApplication
Заключение

Вступление

Что такое ASP.NET? Одни скажут, что это среда для разработки крупных порталов, другие – средство для быстрого создания Web-сервисов. Для третьих ASP.NET – это инструмент разработки сайтов, адаптированных для работы на мобильных устройствах. И все они окажутся правы.

Более того, хотя ASP.NET в первую очередь и создана для решения упомянутых выше задач, возможности её этим отнюдь не ограничиваются. Благодаря развитой, расширяемой архитектуре, новые возможности в ASP.NET можно добавлять почти до бесконечности. Но для того, чтобы эти добавления не производились бездумно и не превращались в почти шаманские камлания, желательно представлять, каким же образом устроена ASP.NET, и что у неё внутри.

От неуправляемого к управляемому. От IIS к ASP.NET

ISAPI расширения

Во всех новых версиях Windows (XP, 2000 и 2003) есть компонент Internet Information Services (IIS). Получив запрос, IIS “смотрит”, к какому типу этот запрос относится (*.asp, *.shtml или *.aspx). Если IIS не способен самостоятельно обработать запрос (например, *.html или *.htm он может обрабатывать самостоятельно), то он передает его соответствующему ISAPI-расширению. Чтобы увидеть все доступные расширения, нужно:

В списке “Application Extensions” (в этой статье приводятся названия, используемые в Windows Server 2003) будут указаны все ISAPI-расширения IIS, установленные на текущий момент. Среди них наверняка будет и aspnet_isapi.dll (предполагается, что на компьютере читателя установлен IIS, а после него – .NET Framework, при другой последовательности установки ISAPI-расширение не будет зарегистрировано).

ПРИМЕЧАНИЕ

Иногда необходимо вручную зарегистрировать ASP.NET в IIS, к примеру, IIS установлен позднее .NET Framework, или, скажем, после обновления .NET Framework IIS не “увидел” изменений. Самый простой способ решить эту проблему – воспользоваться утилитой aspnet_regiis.exe. Она сможет самостоятельно зарегистрировать расширение aspnet_isapi.dll и сконфигурировать IIS.

Так что же это за расширение?

ASP.NET Hosting Runtime

Это – ISAPI-расширение (aspnet_isapi.dll), и предназначено оно для получения запросов, адресованных ASP.NET-приложениям (*.aspx *.asmx и т.д.), а также создания рабочих процессов aspnet_wp.exe, обратывающих запросы (aspnet_wp.exe – процесс, в котором, собственно, и работает ASP.NET, в Windows 2003 этот процесс носит более звучное имя – w3wp.exe).

Получив такой запрос, расширение проверяет настройки в секции <processModel> конфигурационного файла machine.config, находящегося в каталоге “%SystemRoot%\Microsoft.NET\Framework\v%CLRVersion%\CONFIG. Если уже существующий процесс aspnet_wp.exe не удовлетворяет хотя бы одному требованию, указанному в настройках, то aspnet_isapi.dll создает новый процесс с “правильными” настройками. При этом старый рабочий процесс продолжает существовать до окончания обработки выполняемых запросов, после чего удаляется.

ПРИМЕЧАНИЕ

В ASP.NET появилась модель рабочих процессов «Web Garden», которая позволяет на мультипроцессорных серверах создавать несколько экземпляров рабочих процессов. Чтобы разрешить поддержку этой модели, нужно установить атрибут webGarden секции <processModel> в true. При этом также рекомендуется задать значение атрибута cpuMask. Это битовая маска, значение которой задается в шестнадцатеричном формате. Маска указывает, какие из доступных процессоров будут участвовать в работе. Например, если маска установлена в 0x0D (в двоичном виде – 1101), то процессор номер 1 (нумерация идет справа и начинается с нуля) не будет участвовать в работе. Если webGarden установлен в false, то значение cpuMask игнорируется. Следует отметить, что Windows 2003 позволяет создавать несколько экземпляров w3wp.exe и для однопроцессорной машины.

При создании этого процесса через командную строку ему можно передать следующие параметры:

Параметр Описание
IIS-Process-ID Идентификатор процесса IIS.
This-Process-Unique-ID Идентификатор процесса, использующийся при включенном режиме «Web Garden».
Number-of-Sync-Pipes Число каналов (pipes), по которым может поступать информация.
RPC_C_AUTHN_LEVEL_XXX Уровень аутентификации для модели безопасности COM, по умолчанию - CONNECT.
RPC_C_IMP_LEVEL_XXX Уровень имперсонации для модели безопасности COM, по умолчанию - IMPERSONATE.
CPU-Mask Битовая маска доступных процессоров.
Max-Worker-Threads Максимальное число рабочих потоков в пуле на каждый процессор.
Max-IO-Threads Максимальное число I/O потоков в пуле на каждый процессор.
Таблица 1

Aspnet_isapi.dll общается с процессом aspnet_wp.exe через асинхронные именованные каналы (pipes). Через них aspnet_isapi.dll производит мониторинг рабочего процесса. Параметры сервера, получаемые как значение свойства HttpRequest.ServerVariables, также передаются через эти каналы. Имена каналов формируются автоматически, при их формировании используются количество каналов (Number-of-Sync-Pipes) и идентификатор процесса (IIS-Process-ID).

ПРИМЕЧАНИЕ

При использовании модели «Web Garden» в работе принимает участие параметр This-Process-Unique-ID. Но тут есть один нюанс. Дело в том, что aspnet_isapi.dll не знает, какой ID будет у процесса aspnet_wp.exe, так как данная информация становится известна только после создания этого процесса (она возвращается в параметре lpProcessInformation функции CreateProcess). Поэтому вместо идентификатора передается только что сгенерированный GUID.

В поставку ASP.NET входит aspnet_filter.dll. Этот фильтр предназначен для работы с “cookieless”-сессиями. В таких сессиях их идентификаторы передаются не в теле сообщения (http-заголовок “Cookie”), а в конце URL. Существование таких сессий обусловлено тем, что некоторые HTTP-клиенты в целях безопасности не работают с Cookie-файлами. Фильтр aspnet_filter.dll получает и разбирает строку запроса, после чего дописывает идентификатор сессии в тело сообщения под новым http-заголовком “AspFilterSessionId”.

Aspnet_state.exe – это сервис, предназначенный для хранения состояния сессий ASP.NET. Чтобы сохранять состояние сессии с помощью этого сервиса, нужно установить в конфигурационном файле значение атрибута mode секции <sessionState> в значение StateServer. Сохранение данных сессии в отдельном процессе обеспечивает большую надежность системы. Это позволяет не заботиться о процессе сбора мусора (не путать со сборкой мусора в .NET), который проводит aspnet_isapi.dll. Ведь aspnet_isapi.dll может с легкостью удалить процесс aspnet_wp.exe, в котором находятся все ваши приложения и их данные. Это может произойти по ряду причин, таких как: истечение времени ожидания ответа от aspnet_wp.exe, превышение порогового значения объёма используемой памяти и т.п.

Если предполагаются длительные и ресурсоемкие операции на сервере, то желательно увеличить некоторые значения, находящиеся в секции <processModel> файла machine.config, чтобы aspnet_isapi.dll не удалила по ошибке исправно работающее приложение. Стоит заметить, что значения по умолчанию, выставленные в этой секции, в большинстве случаев оказываются оптимальными. Поэтому, если возникает необходимость изменения пороговых значений, самое время задуматься о рефакторинге приложения.


Рисунок 1. Процесс активации ASP.NET-среды IIS-ом.

ПРИМЕЧАНИЕ

В Windows Server 2003 слушающий сокет (HTTP Listener), ранее интегрированный в IIS, реализован на уровне ядра (см. рисунок 1, модуль HTTP.SYS). Получая запросы, он не сразу передает их сервису, а сначала проверяет расширение запроса. Этим занимается модуль Web Administration Service (WAS). Если запрос предназначается ASP.NET-приложению, WAS создает рабочий процес w3wp.exe и передает в него запрос, в ином случае – в IIS. Благодаря этому обработка данных происходит значительно быстрее, нежели это происходило бы при лишнем обращении через границы процесса IIS.

Интересный факт – можно выставить для IIS 6.0 уровень изоляции, при котором ASP.NET будет работать через IIS. Для этого нужно:

Теперь данные, находящиеся в секции <processModel> файла machine.config, не будут учитываться, и настройки нужно будет производить только через оснастку IIS Manager. Для этого в контекстном меню секции Application Pool нужно выбрать пункт меню “Properties”. В появившемся окне свойств можно увидеть настройки, в том числе и те, которые выставляются в <processModel>.

HTTP-конвейеры

Инициализация HttpApplication

Хостинг ASP.NET

Что же представляет собою aspnet_wp.exe? Это unmanaged-приложение, являющееся хостом CLR. aspnet_wp.exe активирует CLR через CorBindToRuntimeEx, после чего создается домен для CLR. Далее происходит процесс инициализации среды ASP.NET. Для этого создается еще один отдельный домен с помощью метода ApplicationHost.CreateApplicationHost. В него передается тип класса, через который будет происходить процесс передачи и получения данных из обычного .NET-приложения в среду ASP.NET. В данном случае это класс ISAPIRuntime. Так же при создании домена передаются физический и виртуальный пути к ASP.NET-приложению.

Новый домен будет создан при первом обращении к какому-нибудь виртуальному пути. Количество создаваемых доменов не ограничено ничем, кроме объёма доступной памяти. Это настраивается в атрибуте memoryLimit секции <processModel>, и по умолчанию равно 60% от общего объема памяти.

При создании домена для ASP.NET среды проверяются настройки из файла machine.config. Значения настроек выбираются и из локального файла конфигурации web.config, который находится в папке приложения. Если одинаковые настройки встретились в обоих файлах, то, как правило, используются настройки из файла web.config.

Настройки из web.config могут заместить многие, хотя и не все, настройки из machine.config. Так, например, значения из секции <processModel>, используются в процессе создания нового рабочего процесса, который, в свою очередь, создает домен, конфигурируемый файлом web.config. Другими словами, домен с конкретным конфигурационным файлом web.config будет создан много позже считывания значений из <processModel> конфигурационного файла machine.config. Поэтому значения секции <processModel>, записанные в файле web.config, будут проигнорированы.

Некоторые настройки создаются путем объединения всех конфигурационных файлов, лежащих в виртуальных папках, которые сервер находит, просматривая виртуальный путь, к которому осуществляется запрос. Соответственно, machine.config – конфигурирующий файл корневого каталога всех виртуальных папок. Поэтому все, что можно разрешить на верхнем уровне, можно запретить на нижнем. Например, мое приложение называется MyWebApp. В его директории есть вложенная папка с названием MyWebAppSecurity. В обеих папках лежат файлы web.config. И если теперь разрешить всем пользователям обращаться анонимно к файлам каталога MyWebApp, то для MyWebApp/MyWebAppSecurity это можно запретить. Настройки авторизации прописываются в секции <authorization>.

После создания домена вызыватся метод ISAPIRuntime.ProcessRequest. Внутри этого метода (мы уже находимся в домене запрашиваемого ASP.NET-приложения) создается объект, класс которого является наследником HttpWorkerRequest. Этот объект передается в метод HttpRuntime.ProcessRequest. На данный момент существуют три класса, реализующие HttpWorkerRequest и используемые ISAPIRuntime.

Класс Описание
ISAPIWorkerRequestOutOfProc ASP.NET работает с IIS 5.0
ISAPIWorkerRequestInProcForIIS6 ASP.NET работает через IIS 6.0 (под Windows 2003)
ISAPIWorkerRequestInProc ASP.NET работает без IIS 6.0 (под Windows 2003)
Таблица 2. Классы, реализующие HttpWorkerRequest

Вот отсюда, по большому счету, и начинается ASP.NET. Давайте разберем поподробнее, что именно происходит в недрах HttpRuntime. Для этого я привел UML-диаграмму последовательности вызовов для более наглядного представления происходящего внутри классов из пространства имен System.Web.


Рисунок 2. Процесс активации и инициализации объекта HttpApplication.

При первом вызове статического метода HttpRuntime.ProcessRequest производится инициализация очередей запросов, мониторинга состояния домена, создания профайлера (Profiler) для вывода трассировочной информации, настройки которого хранятся в секции <trace> и т.д. Также в этот момент производится загрузка всех сборок с расширением dll, находящихся в папке bin. После этого HttpRuntime подписывается на событие выгрузки домена, в обработчике которого будет вызван метод Application_End. Домен может быть выгружен в нескольких случаях. Первый – это когда произошла ошибка на ранних этапах инициализации HttpRuntime или при завершении обработки запроса. Второй – если произошли изменения в конфигурационных файлах (web.config, machine.config или файлы конфигурации, устанавливающие уровни политики безопасности в секции <trustLevel>). Третий – когда была удалена или переименована директория bin. И, наконец, четвертый – принудительно был вызван метод HttpRuntime.UnloadAppDomain.

Закончив процесс инициализации, HttpRuntime создает обработчик события, извещающего об окончании отсылки ответа, и передает его клиентской программе через вызов метода HttpWorkerRequest.SetEndOfSendNotification. В этом обработчике производится освобождение ресурсов, занятых объектами HttpRequest и HttpResponse. Так что программистам, работающим с HttpWorkerRequest, настоятельно рекомендуется вызывать этот метод, чтобы предотвратить утечку ресурсов.

HttpRuntime создает экземпляр HttpContext, который, в свою очередь, инициализирует HttpRequest и HttpResponse. Затем через фабрику HttpApplicationFactory создается новый экземпляр HttpApplication. В него загружается его состояние, то есть объект класса HttpApplicationState. После этого происходит процесс инициализации созданного экземпляра HttpApplication.

Так для чего нужен HttpApplication? Фактически этот объект является олицетворением всех ASP.NET-сущностей, таких, как: Web-страница (*.aspx), User control (*.ascx), Web-сервис (*.asmx) или HTTP-обработчик (*.ashx). При обращении к ним создается отдельный экземпляр HttpApplication. Все события, вся обработка информации происходят в нем, и все методы конечного объекта (Page, WebService и т.д.) вызываются прямо или косвенно из этого класса. Иными словами, HttpApplication – это мозг всего ASP.NET.

Парсинг и компиляция ASP.NET-файлов

При первом вызове метода HttpApplicationFactory.GetApplicationInstance происходит процесс парсинга и компиляции ASP.NET-файлов проекта. Основную работу выполняет класс BuildManager из пространства имен System.Web.Compilation.


Рисунок 3. Процесс парсинга и компиляции ASP.NET-файлов.

Экземпляр HttpApplication вызывает статический метод BuildManager.GetGlobalAsaxType. В этом методе проверяется, нужна ли перекомпиляция. Чтобы разобраться, как среда ASP.NET решает, нужна ли перекомпиляция сборок, давайте зайдем в каталог “%SystemRoot%\Microsoft.NET\Framework\v%CLRVersion%\Temporary ASP.NET Files”. Здесь находятся подкаталоги с названиями Web-приложений. Вот примерная структура дерева каталогов, которую можно увидеть, зайдя в любую папку ASP.NET-приложения:

\Microsoft .NET
  \Framework
    \v%CLRVersion%
      \Temporary ASP.NET Files
        \WebService
          \a45ede38
            \336d7fb3

Имена двух последних уровней являются текстовым представлением хеш-значений всех сборок, содержащих код приложения.

ПРИМЕЧАНИЕ

Временная папка “Temporary ASP.NET Files” может и не находиться в каталоге “%SystemRoot%\. Чтобы изменить путь к временным файлам ASP.NET-приложений, нужно изменить в реестре значение ключа Path (в HTLM\Software\Microsoft\ASP.NET\v%CLRVersion%\).

В таких папках находятся XML-файлы вида [filename].[hashcode].compiled следующего содержания:

<preserve resultType="2" assembly="Code" 
  hash="a433b8acd5ef24" usesExistingAssembly="true" type="Service_asmx">
  <filedep name="/WebService/Service.asmx"/>
</preserve>

Именно по хеш-значениям ASP.NET определяет, изменился ли файл, и требуется ли перекомпиляция.

Если необходима перекомпиляция, то BuildManaged создает экземпляр класса BuildProviderHost и передает ему коллекцию объектов, реализующих IBuildProvider. Список классов этих объектов регистрируется в секции <buildProviders>. Провайдеры классифицируются по расширению обрабатываемых ими файлов. Эти расширения определяются значением атрибута extension в секции <buildProviders>. После этого экземпляр класса BuildProviderHost выбирает нужный IBuildProvider и вызывает его метод GenerateCode, передавая в него ссылку на самого себя. С помощью IBuildProvider создается экземпляр производного класса BaseParser, который занимается парсингом *.as*x-файлов.

После окончания парсинга класс, реализующий IBuildProvider, вызывает методы класса BuildProviderHost, осуществляющие компиляцию. Этим методам передаются результаты работы парсера и другие данные, которые могут потребоваться позже. Это могут быть название файла, название типа и т.д. Какой именно компилятор нужно использовать, задается в секции <compilers>.

ПРИМЕЧАНИЕ

Каждый домен позволяет перекомпилировать себя определенное количество раз. Этот предел задается в секции <compilation> атрибутом numRecompilesBeforeAppRestart. По умолчанию он равен 15. При этом происходит выгрузка всего домена, так как CLR позволяет выгружать только домены, а не отдельные сборки.

Global.asax

После разбора и компиляции файла фабрика классов HttpApplicationFactory получила описание класса (ссылку на объект System.Type), производного от HttpApplication, и начинает искать в нем с помощью технологии “отражения” (reflection) специальные методы Application_OnStart, Application_OnEnd, Session_OnStart, Session_OnEnd (или эквивалентные им Application_Start, Application_End, Session_Start, Session_End). Они будут вызываться самой фабрикой HttpApplicationFactory. Затем производится поиск оставшихся методов вида Application_Error, Application_BeginRequest, Application_EndRequest и их привязка к событиям объекта HttpApplication.

Заметьте, что класс Global из файла global.asax не содержит явной подписки на события. Эта подписка производится неявно. Не зная этого, можно произвести подписку вручную, а потом удивляться тому, что события приходят дважды. Что ж, еще одним секретом меньше в нашем мире.

ПРИМЕЧАНИЕ

Если вы хотите «привязать» вручную класс Global к соответствующим событиям, не пользуясь при этом дизайнером Visual Studio, нужно давать имена методам, обрабатывающим события, в соответствии со следующим шаблоном: Application_[On]%EventName%.

Пару слов о событии Application_Error. Если в момент обработки произошло исключение, то оно передается в метод HttpContext.AddError, где сохраняется до окончания обработки запроса. Для этого в HttpContext находится динамический список, где сохраняются объекты типа Exception. Чтобы получить все такие исключения, можно, вызвать свойство HttpContext.AllErrors.

Создав экземпляр HttpApplication, фабрика HttpApplicationFactory вызывает событие Application_Start, что сигнализирует о начале работы приложения.

Http-модули

После запуска приложения происходит инициализация созданного экземпляра класса HttpApplication. В начале создаются Http-модули (классы, реализующие интерфейс IHttpModule), перечень которых находится в секции <httpModules>. В файле machine.config зарегистрированы такие модули, как OutputCacheModule, SessionStateModule, SessionIDModule, FormsAuthenticationModule и т.д. Наиболее интересным является модуль, отвечающий за состояние данных сессии. Без сессий не могло быть и речи о безопасности. Все известные типы аутентификаций (Form, Windows) основаны на механизме сессий. Созданием сессий и занимается модуль SessionStateModule.

SessionStateModule хранит свои настройки в секции <sessionState>. Атрибут mode этой секции указывает на то, как будет храниться сессия. Он может принимать одно из следующих значений:

Значение Описание
Off Сессия не поддерживается.
InProc Хранить данные локально в памяти рабочего процесса.
StateServer Хранить данные в процессе aspnet_state.exe.
SQLServer Хранить данные в SQL-сервере.
Custom Хранить данные в другом хранилище.

Соответственно, в .NET Framework есть классы, реализующие приведенные выше модели хранения. Это InProcSessionStateStore, OutOfProcSessionStateStore, SqlSessionStateStore. Но, как и многое другое, эти классы объявлены как private.

Чтобы реализовать собственное хранилище, нужно создать класс, реализующий интерфейс ISessionStateStore, а в атрибуте mode выставить значение Custom, а в дополнительном атрибуте type указать полное имя типа вашего класса.

SessionStateModule подписывается на три события HttpApplication: асинхронное событие изменения состояния сессии (AcquireRequestStateAsync), событие сохранения состояния сессии (ReleaseRequestState), событие окончания запроса (EndRequest).

В обработчике AcquireRequestStateAsync модуль сохраняет состояние сессии в экземпляр класса HttpContext. Для этого он создает экземпляр класса HttpSessionState и добавляет его со значением ключа “AspSession” в коллекцию HttpContext.Items. Но в программе для получения состояния сессии в целях удобства лучше использовать свойство HttpContext.Session. При создании сессии вызывается событие Session_Start.

В обработчике ReleaseRequestState проверяется, был ли вызван метод HttpSessionState.Abandon. Если да, то вызывается метод HttpApplication.EndSession (который передает управление в метод Global.Session_End).

ПРИМЕЧАНИЕ

Время жизни сессии по умолчанию равно 20 минутам. Это значение можно изменить, отредактировав значение атрибута timeout секции <sessionState> конфигурационного файла.

Далее SessionStateModule сохраняет состояние сессии в хранилище и удаляет ее из HttpContext вместе с идентификатором сессии. Если придет новый запрос, SessionStateModule восстановит состояние сессии в HttpContext. С первого взгляда это может показаться лишним, зачем, мол, что-то удалять и снова восстанавливать? Но не забывайте, что экземпляры HttpApplication (и, соответственно, HttpContext, экземпляр которого содержится в HttpApplication) хранятся в пуле. И поэтому, если придет запрос от другого клиента, нам не нужно будет сначала удалять состояние старой сессии, а затем восстанавливать.

ПРИМЕЧАНИЕ

Если использовать в качестве хранилища сессий SQL Server или State Server, то никакого события об окончании сессии не придет. Это логично, ведь если даже сессия вашего приложения будет уничтожена или будет выгружен сам рабочей процесс, данные все равно останутся не тронутыми. Это функциональность очень полезна, когда Web Services объединяются в так называемые “Фермы” (Web Farm), когда несколько машин, нагрузка по которым распределяется через технологию Load Balansing, образуют один глобальный Web-сервис. Но в этой, с первого взгляда, очень полезной функциональности кроется довольно существенная опасность. Дело в том, что как уже описывалось выше, рабочий процесс может быть удален, как только у него случится внутренний сбой. Соответственно, если у вас используется всего одно Web-приложение, работающее с SQL Server, то никто не сможет послать ему команду об осбовождении данных и удалении записей из таблиц. Именно для решения этой проблемы в поставку .NET входят *.sql файлы (они находятся в корне каталога, где установлен Framework). Эти файлы создают базу, в которой будут храниться состояния сессий. Но вместе с базой устанавливается и SQL-задание (JOB – действие, которое активизируется по расписанию). Это задание смотрит время последнего подключения и, если был привышен временной лимит, задание считает, что данная сессия более не нужна, и удаляет данные из базы. На самом деле, вам в любом случае нужно будет выполнить эти скрипты, ибо они создают базу, с которой будет работать SqlSessionStateStore (если вы, конечно, не хотите устанавливать все вручную). Так что стоит упомянуть одну важную особенность. Дело в том, что существуют два установочных сценария, InstallSqlState.sql и InstallPersistSqlState.sql. Разница между ними в том, что первый устанавливает TempDB, базу, которая будет унижтожена при выключении компьютера. Второй сценарий, InstallPersistSqlState.sql, устанавливает полноценную БД ASPState.

Еще один достойный рассмотрения модуль – SessionIDModule. Он отвечает за процесс создания идентификатора сессии для выполняющегося за ним модуля SessionStateModule. Если используется “cookieless” сессия (значение атрибута cookieless секции <sessionState> равно true), то этот модуль подписывается на перехват события BeginRequest. В обработчике из http-заголовка “AspFilterSessionId”, созданного, как было сказано выше, фильтром aspnet_filter.dll, извлекается номер сессии. Обработчик записывает полученное значение в HttpContext.Items с ключом “AspCookielessSession”. Если же значение cookieless было равно false (это означает, что номер сессии будет записан в cookie, и будет передаваться в специальном http-заголовке “Cookie”), данный модуль ничего перехватывать не будет, а идентификатор сессии будет выбираться непосредственно из модуля SessionStateModule.

ПРИМЕЧАНИЕ

Как вы могли догадаться, порядок регистрации модулей в конфигурационном файле тоже имеет значение. Ведь если бы модуль SessionIDModule стоял после SessionStateModule, то он и подписался бы на событие BeginRequest позднее, чем SessionStateModule. В таком случае ни о каких cookieless-сессиях не могло бы быть и речи.

Модуль SessionIDModule появится только в ASP.NET 2.0 (на момент написания этой статьи мне была доступна бета-версия Visual Studio codename Whidbey). До этого он был интегрирован с модулем SessionStateModule. Хорошо это или плохо (особенно, если принимать предыдущее замечание), решать вам, но знать заранее не помешает.

Завершающим модулем в цепочке всегда ставится DefaultAuthenticationModule. Он не указывается ни в каких конфигурационных файлах, так как автоматически добавляется классом конфигурации HttpModulesConfiguration. Предназначен этот модуль для создания IPrincipal (используемого по умолчанию). Такой принципал не имеет ни имени, ни типа аутентификации, ни ролей, и используется только потому, что в ASP.NET-приложениях свойство HttpContext.Principal не может возвращать null (подробнее про безопасность можно почитать в статье Тимофея Казакова "Механизмы безопасности в .NET", опубликованной в нашем журнале, #4’2003.

Формирование конвейера

Следующий этап инициализации объекта HttpApplication – формирование массива действий, которые будут им последовательно выполнены.

На рисунке 4 представлены события HttpApplication в том порядке, в котором они будут вызваны.


Рисунок 4. Порядок исполнения “шагов” HttpApplication.

В ASP.NET 1.1 некоторых показанных на рисунке событий нет, так как они появятся только в ASP.NET 2.0. Так что вы можете не удивляться, если пока не найдете о них ничего в документации. Описание событий для версий 1.1 и ниже доступно в MSDN, поэтому я привел пояснения только к тем событиям, которые будут доступны в новой версии.

На этом процесс инициализации объекта HttpApplication заканчивается. После этого инициализированный объект начинает обработку запросов.

Обработка запроса


Рисунок 5. Вызов метода HttpApplication.BeginProcessMessage.

HttpRuntime вызывает метод HttpApplication.BeginProcessMessage. Этот метод является реализацией интерфейса IHttpAsyncHandler, и используется для инициализации асинхронного вызова. После этого экземпляр HttpApplication начинает поочередно выполнять действия, список которых составлен на предыдущем этапе. Именно в этот момент вызываются обработчики событий, приведенные на рисунке 4, в модулях (Http Modules). Вы наверняка заметили неописанные действия MapHandlerExecutionStep, CallHandlerExecutionStep, CallFilterExecutionStep? Я опишу их подробнее.

MapHandlerExecutionStep – на этом шаге создается объект-обработчик полученной информации, то есть класс, реализующий IHttpHandler. На рисунке 6 показана UML-диаграмма создания этого обработчика.


Рисунок 6. Создание обработчика.

По запросу, посланному пользователем (*.asmx, *.ashx, *.aspx и т.д.), создается фабрика обработчиков (IHttpHandlerFactory). Все фабрики записаны в секции <httpHandlers>. Далее создается фабрика протоколов, используемых при запросе. В случае Web-сервиса этой фабрикой является SoapServerProtocolFactory. С помощью созданного экземпляра ServerProtocolFactory создается объект ServerProtocol, который передается конструктору обработчика. После создания ссылка на обработчик помещается в свойство HttpContext.Handler.

Для регистрации обработчиков используется секция <httpHandlers> конфигурационного файла. Чтобы добавить обработчик, нужно создать подсекцию <add> с атрибутами, приведёнными ниже:

Имеется также возможность управлять созданием обработчиков. Для этого вместо обработчиков в секции <httpHandlers> регистрируются их фабрики (классы, реализующие интерфейс IHttpHandlerFactory). Так как фабрика создает нужный впоследствии обработчик, то, реализовав свою собственную фабрику, можно контролировать и регулировать процесс создания обработчиков. Обычно в machine.config указываются именно фабрики обработчиков (WebServiceHandlerFactory, HttpRemotingHandlerFactory, PageHandlerFactory и т.д.).

ПРИМЕЧАНИЕ

Если вы регистрируете обработчик, а не фабрику, то в HttpApplication для него создается HandlerFactoryWrapper. Вероятно, это сделано разработчиками ASP.NET из соображений единообразия и не несет дополнительной функциональности.

CallHandlerExecutionStep – это вызов обработчика, созданного на предыдущем шаге. На рисунке 7 показана диаграмма, демонстрирующая этот вызов (на примере вызова Web-сервиса, WebServiceHandler). Экземпляр этого класса занимается сериализацией и десериализацией SOAP-сообщений и, конечно же, вызовом Web-метода.


Рисунок 7. Вызов Web-метода.

В процессе выполнения обработчик обращается к объекту HttpRequest для получения входящих данных и заголовков HTTP-запроса. На рисунке 8 показано, что происходит при этих вызовах внутри класса HttpRequest.


Рисунок 8. Загрузка данных запроса в классе HttpRequest.

HttpRequest в цикле вызывает метод HttpWorkerRequest.ReadEntityBody, дочитывая входящую информацию, посланную в теле POST-запроса. После этого он фильтрует входящий поток. Этот фильтр может быть установлен в свойстве HttpRequest.Filter. Затем полученные данные десериализуются и вызывается Web-метод. На рисунке 7 отмечено, что на данном этапе может быть задействована COM+-транзакция. Это зависит от свойства TransactionOption атрибута WebMethodAttribute Web-метода. Далее в выходной поток сериализуется ответ Web-сервиса.

Тут я хочу упомянуть о еще одной маленькой, но очень важной детали. Часто в форумах, посвященных ASP.NET, можно услышать вопрос “А почему в моей реализации IHttpHandler значение сессии равно null?”. Дело в том, что если вашему обработчику нужно работать с состоянием сессии, он должен реализовать интерфейс IRequiresSessionState. Данный интерфейс не имеет ни методов, ни свойств, ни событий, и является своеобразным маркером. Модуль SessionStateModule, проверив, что текущий обработчик реализует этот интерфейс, немедленно сообщает HttpApplication о прекращении асинхронной обработки события AcquireRequestState, и HttpApplication, в свою очередь, начинает исполнять его последовательно.

CallFilterExecutionStep – вызов фильтров исходящего потока (HttpResponse.Filter). На рисунке 9 представлено, что происходит при этом.


Рисунок 9. Окончание обработки запроса в классе HttpResponse.

Как видно из диаграммы (см. рисунок 9), именно здесь вызываются последние события HttpApplication PreSendRequestHeaders, PreSendRequestContent в цепочке делегатов.

Завершение работы HttpApplication

Вот мы и добрались до этапа завершения работы HttpApplication и освобождения ресурсов. Отработавший экземпляр HttpApplication помещается в пул таких же объектов для того, чтобы в следующий раз не создавать его заново, повторяя достаточно большой процесс инициализации. Но если максимальный размер пула уже достигнут, а он, кстати, “как показывают опыты советских ученых”, имеет жестко определённое значение, равное 100 (это можно увидеть в методе RecycleNormalApplicationInstance класса HttpApplicationFactory при помощи любого дизассемблера или декомпилятора), то объект в пул не помещается и уничтожается. При уничтожении объекта вызывается метод IHttpModule.Dispose для всех модулей, используемых приложением.

Заключение

Надеюсь, что этот материал поможет вам разобраться в механизмах работы ASP.NET и более осмысленно использовать эту технологию в своей работе. Хочу выразить особую благодарность следующим людям: моему брату, Игорю Сухову, за неоценимую помощь в написании статьи, и Тимофею Казакову - за предоставление академической информации и конструктивную критику.


Эта статья опубликована в журнале RSDN Magazine #6-2003. Информацию о журнале можно найти здесь