Расширение конвейера HTTP

139

Конвейер событий приложения не ограничивается только запросами к веб-формам .aspx. Он также применяется и в случае, если создаются специальные обработчики, обслуживающие файлы специальных типов.

Зачем может потребоваться создавать специальный обработчик? В большинстве случаев в этом нет никакой необходимости. Однако иногда все-таки удобно иметь низкоуровневый интерфейс, по-прежнему предоставляющий доступ к полезным объектам вроде Response и Request, но при этом не использующий всю модель веб-формы с ее многочисленными элементами управления.

Одним из примеров может служить ситуация, когда необходимо создать веб-ресурс, динамически визуализирующий специальную графику. В такой ситуации понадобится получить запрос, проверить параметры URL-адреса и вернуть необработанные данные изображения в виде файла JPEG или GIF. За счет отказа от использования полной модели веб-элементов управления можно сократить накладные расходы, поскольку ASP.NET не придется выполнять много шагов (таких как создание объектов веб-страниц, сохранение данных состояния представления и т.д.).

Подключаемая архитектура ASP.NET позволяет реализовать подобные сценарии удивительно просто. Например, новые обработчики для файлов специальных типов можно "подключать", просто добавляя соответствующие конфигурационные параметры. Однако сначала необходимо разобраться с тем, как работает конвейер HTTP.

Обработчики HTTP

Каждый поступающий в приложение ASP.NET запрос обрабатывается специально предназначенным для этого компонентом, который называется обработчиком HTTP (HTTP handler). Обработчики HTTP играют главную роль в структуре обработки запросов ASP.NET. Для обслуживания файлов разных типов в ASP.NET используются разные обработчики HTTP. Например, для веб-страниц применяется обработчик, который создает объекты страницы и ее элементов управления, запускает код и визуализирует окончательный HTML.

Регистрировать свои обработчики HTTP можно двумя способами. Если используется встроенный в Visual Studio веб-сервер, старая версия IIS или версия IIS 7.x в классическом режиме, обработчики HTTP должны добавляться в раздел <httpHandlers> элемента <system.web> внутри файла web.config. Ниже показано это место:

<system.web>
    <httpHandlers>
      ...
    </httpHandlers>
  </system.web>

Внутри раздела <httpHandlers> можно размещать элементы <add> для регистрации новых обработчиков и элементы <remove> для отмены регистрации существующих обработчиков. Ключевой набор определенных подобным образом обработчиков можно видеть в корневом файле web.config. Ниже показан фрагмент этого файла:

<!-- Часть файла web.config на локальной машине  -->
<httpHandlers>
            <add path="eurl.axd" verb="*" type="System.Web.HttpNotFoundHandler" validate="True" />
            <add path="trace.axd" verb="*" type="System.Web.Handlers.TraceHandler" validate="True" />
            <add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="True" />
            <add verb="*" path="*_AppService.axd" type="System.Web.Script.Services.ScriptHandlerFactory, 
                      System.Web.Extensions, Version=4.0.0.0, Culture=neutral, 
                      PublicKeyToken=31bf3856ad364e35" validate="False" />
            <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, 
                      System.Web.Extensions, Version=4.0.0.0, Culture=neutral, 
                      PublicKeyToken=31bf3856ad364e35" validate="False"/>
            ...
</httpHandlers>

Когда используется IIS 7.x в интегрированном режиме (режим по умолчанию), описанный выше метод регистрации обработчиков HTTP не работает. В такой ситуации IIS считывает раздел <system.webServer> и использует обработчики, определенные в его подразделе <handlers>:

<system.webServer>
    <handlers>
      ...
    </handlers>
  </system.webServer>

Подобно разделу <httpHandlers>, новые обработчики HTTP регистрируются добавлением элементов <add> в раздел <handlers>. Это небольшое изменение в конфигурационном файле подчеркивает более серьезную перемену в способе работы IIS. В версиях, предшествующих IIS 7 (и в случае запуска IIS 7.x в классическом режиме), при получении каждого запроса сначала производится проверка, с какими программами сопоставлен запрашиваемый тип файла. Если тип файла сопоставлен с ASP.NET, IIS передает этот файл механизму ASP.NET, который затем считывает информацию, касающуюся обработчиков, из файла web.config и решает, что делать дальше с данным запросом.

Недостаток такого подхода в том, что весь процесс зависит от первоначальной регистрации файла. Если механизм ASP.NET не зарегистрирован для определенного типа файлов, специальный обработчик или модуль HTTP при запросе файла такого типа запустить не удастся.

Версия IIS 7.x более интеллектуальна. В интегрированном режиме она справляется с задачей по отправке запроса надлежащему обработчику HTTP и всегда считывает информацию об обработчиках из раздела <system.WebServer>. Если попытаться зарегистрировать обработчики в разделе <httpHandler>, то при запуске приложения будет отображена страница с ошибкой IIS. Это позволяет предотвратить угрозу безопасности из-за наличия веб-приложения, в котором определенные обработчики реализованы, но в действительности не используются. (Добавив в раздел <system.webServer> элемент <validationvalidateIntegratedModeConfiguration="false"/>, это поведение можно отключить, и тогда IIS 7.x будет принимать раздел <httpHandler>. Однако поступать подобным образом не рекомендуется.)

Создание специального обработчика HTTP

Если нужно работать на более низком уровне, чем предлагает модель веб-форм, чтобы обеспечить специализированный вид обработки, можно реализовать собственный обработчик HTTP.

Для построения специального обработчика HTTP необходимо просто создать класс, реализующий интерфейс IHttpHandler. Этот класс можно поместить в каталог App_Code или скомпилировать как часть автономной DLL-сборки (другими словами, в виде отдельного проекта библиотеки классов) и добавить в веб-приложение ссылку на него.

Интерфейс IHttpHandler требует от класса реализации двух членов:

ProcessRequest()

ASP.NET вызывает этот метод при получении запроса. Именно здесь обработчики HTTP выполняют всю работу. Получить доступ к внутренним объектам ASP.NET (таким как Request, Response и Server) можно посредством объекта HttpContext, передаваемого этому методу

IsReusable

После того как метод ProcessRequest() завершит свою работу, ASP.NET проверяет это свойство, чтобы узнать, может ли данный экземпляр обработчика HTTP использоваться повторно. Значение true указывает, что объект обработчика HTTP может использоваться повторно для другого запроса такого же типа, а значение false — что объект обработчика HTTP должен быть отброшен

Ниже показан пример создания простейшего обработчика HTTP. Он просто возвращает фиксированный блок HTML-разметки с сообщением:

using System;
using System.Web;

public class SimpleHandler : IHttpHandler
{
    public void ProcessRequest(System.Web.HttpContext context)
    {
        HttpResponse response = context.Response;
        response.Write("<html><body><h1>Файл обработан SimpleHandler");
        response.Write("</h1></body></html>");
    }

    public bool IsReusable
    {
        get { return true; }
    }
}

Если вы создаете это расширение как проект библиотеки классов, понадобится добавить ссылку на сборку System.Web.dll, содержащую большое количество классов ASP.NET. Без этой ссылки вы не сможете применять такие типы, как IHttpHandler и HttpContext.

Конфигурирование специального обработчика HTTP

После того как класс обработчика HTTP создан и сделан доступным веб-приложению (за счет помещения его в каталог App_Code или добавления ссылки), расширение готово к использованию. Следующим действием будет изменение файла web.config для веб-приложения с целью регистрации обработчика HTTP. Пример показан ниже:

<system.web>
    <httpHandlers>
      <add verb="*" path="test.simple"
           type="SimpleHandler,HttpExtensions"/>
    </httpHandlers>
</system.web>

При регистрации обработчика HTTP указываются три важных детали. Атрибут verb определяет тип HTTP-запроса - POST или GET (для всех типов запросов используется *). Атрибут path показывает расширение файла, который будет вызывать обработчик HTTP. В этом примере раздел web.config связывает класс SimpleHandler с именем файла test.simple.

Наконец, атрибут type идентифицирует класс обработчика HTTP. Эта идентификация состоит из двух частей. Первая часть представляет полностью квалифицированное имя класса (в этом примере — HttpExtensions.SimpleHandler). За этой частью следует запятая и имя DLL-файла сборки, содержащей данный класс (в рассматриваемом примере — HttpExtensions.dll). Обратите внимание, что расширение .dll подразумевается всегда, поэтому указывать его не нужно.

Если вы используете подход с App_Code вместо отдельно скомпилированной сборки, можете вообще опустить имя DLL-файла, поскольку ASP.NET сгенерирует его автоматически.

Visual Studio не позволяет запускать свой обработчик HTTP напрямую. Вместо этого придется сначала запустить веб-проект, а затем ввести URL-адрес, включающий элемент test.simple:

Запуск специального обработчика HTTP

Использование обработчиков HTTP, не нуждающихся в конфигурировании

В ASP.NET также поддерживается альтернативный подход, который позволяет избегать регистрации обработчиков HTTP и не заботиться о настройке их параметров в конфигурационном файле — использование распознаваемого расширения .ashx. Какая бы версия IIS не применялась (даже интегрированный веб-сервер Visual Studio), все запросы, оканчивающиеся расширением .ashx, автоматически воспринимаются как запросы, обслуживаемые специальным обработчиком HTTP.

Для создания файла .ashx в Visual Studio выберите в меню Website (Веб-сайт) (или в меню Project (Проект) для веб-проекта) пункт Add New Item (Добавить новый элемент) и укажите Generic Handler (Общий обработчик). Затем введите подходящее имя и щелкните на кнопке Add (Добавить) для создания обработчика.

Файл .ashx начинается с директивы WebHandler. Эта директива указывает на класс, который должен предоставляться через этот файл. Например:

<%@ WebHandler Language="C#" Class="SimpleHandler" %>

Имя класса может соответствовать классу в каталоге App_Code или классу в ссылаемой сборке. В качестве альтернативы можно определить класс прямо в файле .ashx (под директивой WebHandler). В любом случае, когда пользователь запросит файл .ashx, будет выполнен соответствующий класс обработчика HTTP. Если вы сохраните предыдущий пример в виде файла simple.ashx, то всякий раз при запросе клиентом simple.ashx будет выполняться специальный веб-обработчик. Более того, тип файла .ashx зарегистрирован в IIS, поэтому конфигурировать IIS при развертывании приложения не понадобится.

Использовать конфигурационный файл или файл .ashx — дело личных предпочтений. Однако файлы .ashx обычно применяются для более простых расширений, которые разрабатываются для одного веб-приложения. Конфигурационные файлы также предоставляют некоторую степень гибкости. Например, можно зарегистрировать обработчик HTTP, чтобы работать с запросами, заканчивающимися данным расширением, в то время как файл .ashx будет обслуживать запрос со специфическим именем файла.

Также можно зарегистрировать обработчик HTTP для множества приложений (регистрируя его в файле web.config и устанавливая сборку в GAC). Чтобы добиться того же эффекта с помощью файла .ashx, потребуется скопировать файл .ashx в каждый виртуальный каталог.

Создание более функционального обработчика HTTP

В предыдущем примере обработчик HTTP просто возвращал блок статической HTML-разметки. Однако допустимо создавать и более сложные обработчики. Например, можно прочитать данные, отправленные странице или заданные в строке запроса, и применить их для настройки сгенерированных выходных данных. Ниже показан пример отображения исходного кода для запрашиваемого файла. В нем используется поддержка ввода-вывода файла из пространства имен System.IO:

using System;
using System.Web;
using System.IO;

public class SourceHandler : IHttpHandler
{
    public void ProcessRequest(System.Web.HttpContext context)
    {
        // Упростить доступ к объектам HttpContext
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;
        HttpServerUtility server = context.Server;

        response.Write("<html><body>");

        // Получить имя запрашиваемого файла
        string file = request.QueryString["file"];
        try
        {
            // Открыть файл и отобразить его содержимое по одной строке за раз. 
            response.Write("<b>Исходный код файла " + file + "</b><br>");
            StreamReader r = File.OpenText(server.MapPath(Path.Combine("./", file)));
            string line = "";
            while (line != null)
            {
                line = r.ReadLine();

                if (line != null)
                {
                    // Заменить дескрипторы и другие специальные 
                    // символы соответствующими HTML-сущностями, 
                    // чтобы они могли должным образом отображаться, 
                    line = server.HtmlEncode(line);

                    // Заменить пробелы и символы табуляции 
                    // неразрываемыми пробелами						
                    line = line.Replace(" ", " ");
                    line = line.Replace("\t", "     ");

                    // В более сложном средстве просмотра исходного кода может применяться 
                    // цветовое кодирование
                    response.Write(line + "<br>");
                }
            }
            r.Close();
        }
        catch (ApplicationException err)
        {
            response.Write(err.Message);
        }
        response.Write("</html></body>");
    }

    public bool IsReusable
    {
        get { return true; }
    }
}

Этот код просто находит требуемый файл, читает его содержимое и за счет применения небольшой замены строк (например, замена пустого пространства неразрываемыми пробелами, а разрывов строк — элементом <br/>) и HTML-кодирования строит представление, безопасно отображаемое в браузере.

Теперь можно сопоставить обработчик с расширением файла:

<system.webServer>
    <handlers>
      <add verb="*" path="test.simple" name="httpSimpleFile" type="SimpleHandler"/>
      <add verb="*" path="source.simple" name="httpSourceCode" type="SourceHandler"/>
    </handlers>
</system.webServer>

Чтобы протестировать этот обработчик, воспользуйтесь URL-адресом следующего вида:

http://localhost:[Порт]/[Веб-сайт]/source.simple?file=***.cs

После этого обработчик HTTP отобразит исходный код для файла .cs:

Использование более сложного обработчика HTTP

Создание обработчика HTTP для содержимого, отличного от HTML

Некоторые из наиболее интересных обработчиков HTTP генерируют не HMTL-содержимое, а содержимое других типов, например, изображения. Это дает возможность не полагаться на фиксированные файлы, а извлекать или генерировать содержимое программным путем. Например, можно прочитать содержимое большого ZIP-файла из записи в базе данных и с помощью метода Response.BinaryWrite() отправить его клиенту.

Можно даже двинуться еще дальше и построить обработчик HTTP для динамического создания ZIP-архива, состоящего из нескольких файлов меньшего размера. В обоих случаях для клиента, использующего обработчик HTTP, все будет выглядеть так, как будто браузер загружает обычный файл, хотя в действительности содержимое обслуживается с помощью кода ASP.NET.

В следующем примере демонстрируется обработчик HTTP, который имеет дело с файлами изображений. Данный обработчик не создает содержимое изображений динамически, однако он использует код для выполнения другой важной задачи. Всякий раз, когда изображение запрашивается, этот обработчик HTTP проверяет в нем заголовок ссылающейся страницы. Этот заголовок предоставляет имя хоста, которое показывает, находится ли ссылка на данное изображение на одной из страниц вашего сайта или она расположена на каком-то другом сайте.

Если страница, использующая изображение, находится на другом сайте, возникает потенциальная проблема. Эта страница не только похищает ваше изображение, но еще и создает дополнительную нагрузку на ваш сервер. Причина в том, что всякий раз, когда кто-то просматривает этот другой сайт, изображение запрашивается с вашего сервера. Если похищенное изображение появляется на каком-то популярном веб-сайте, тогда дополнительная нагрузка на сервер может оказаться очень высокой и значительно уменьшить доступную пропускную способность, необходимую для обслуживания ваших собственных страниц.

Эта проблема — похищение пропускной способности за счет ссылок на ресурсы, находящиеся на вашем сервере — неформально называется личингом (leeching). Она является главным источником "головной боли" для популярных веб-сайтов, обслуживающих огромные объемы отличного от HTML содержимого (например, сайтов обмена фотографиями, таких как Flickr). Многие веб-сайты противодействуют этой проблеме тем же способом, что и описанный ниже обработчик HTTP, а именно — отказываются обслуживать изображение или заменяют его фиктивным рисунком, если заголовок ссылающейся страницы показывает, что запрос поступил с другого веб-сайта.

Ниже приведен обработчик HTTP, реализующий такое решение в ASP.NET. Для работы этого кода понадобится импортировать пространства имен System.Globalization и System.I0:

using System;
using System.Web;
using System.IO;
using System.Globalization;

public class ImageGuardHandler : IHttpHandler
{
    public void ProcessRequest(System.Web.HttpContext context)
    {
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;

        string imagePath = null;

        // Проверить, находится ли запрашивающая изображение страница на вашем сайте
        if (request.UrlReferrer != null)
        {
            // Выполнить для ссылающейся страницы сравнение без учета регистра
            if (String.Compare(request.Url.Host, request.UrlReferrer.Host,
                true, CultureInfo.InvariantCulture) == 0)
            {
                // Отправивший запрос хост является допустимым. Разрешить обслуживание изображения 
                // (если оно существует)
                imagePath = request.PhysicalPath;
                if (!File.Exists(imagePath))
                {
                    response.Status = "Изображение не найдено";
                    response.StatusCode = 404;
                    return;
                }
            }
        }

        if (imagePath == null)
        {
            // Нет действительных изображений для обслуживания. Вернуть вместо запрашиваемого изображения 
            // рисунок с предупреждением. Вместо жесткого кодирования это изображение 
            // можно было бы извлечь из файла web.config (с использованием 
            // раздела <appSettings> или специального раздела)
            imagePath = context.Server.MapPath("~/Images/notAllowed.gif");
        }

        // Установить тип содержимого в соответствующий тип изображения
        response.ContentType = "image/" +
            Path.GetExtension(imagePath).ToLower();
        response.WriteFile(imagePath);
    }


    public bool IsReusable
    {
        get { return true; }
    }
}

Чтобы этот обработчик защищал файлы изображений, его понадобится зарегистрировать для обслуживания файлов соответствующих типов. Ниже показаны параметры в файле web.config, которые настраивают обработчик на обслуживание файлов типа .gif и .png:

<system.webServer>
    <handlers>
      <add verb="*" path="*.gif" type="ImageGuardHandler" name="gifImageGuard"/>
      <add verb="*" path="*.png" type="ImageGuardHandler" name="pngImageGuard"/>
    </handlers>
</system.webServer>

Данное решение по устранению проблемы личинга далеко от идеала, но позволяет останавливать непрофессиональных личеров. Пользователь, разбирающийся в программировании, сможет легко обойти его с помощью небольшого JavaScript-сценария.

Некоторые веб-разработчики создают гораздо более сложные системы. Например, можно динамически генерировать код временной метки и присоединять его к ссылкам на изображения при каждом запросе страницы. Затем обработчик HTTP может отказывать в обслуживании изображений, если временная метка устарела, предполагая, что ссылка была скопирована и повторно используется на другой странице спустя некоторое время после ее создания. Тем не менее, ни один из подобных приемов не может никого остановить от создания копии рисунка и его обслуживания непосредственно на своем сайте.

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

Можно также создавать обработчики HTTP, работающие в асинхронном режиме. Это значит, что для выполнения своей работы они создают новый поток, а не используют один из рабочих потоков ASP.NET. Такой прием улучшает масштабируемость в ситуациях, когда должна выполняться задача, занимающая много времени, но не требующая множества ресурсов центрального процессора (ЦП). Классическим примером могут быть ожидание чтения какого-то исключительно медленного сетевого ресурса.

Количество рабочих потоков, которые могут запускаться в ASP.NET одновременно, является фиксированным (и обычно равно 25). При достижении этого предела дополнительные запросы будут помещаться в очередь, даже при наличии свободного времени ЦП. Благодаря асинхронным обработчикам, дополнительные запросы смогут приниматься, поскольку эти обработчики создают для каждого запроса новый поток, а не используют рабочий процесс.

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

Пройди тесты
Лучший чат для C# программистов