• Microsoft .NET
  • ASP.NET
  • ASP.NET AJAX: используем закладки и браузерную кнопку 'Назад'

Модификация ответа HTTP (HTTP Response) при помощи фильтров

ОГЛАВЛЕНИЕ

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

Вы можете создать свои собственные response filters при помощи нескольких строк кода и связать их с соответствующим типом страницы (к примеру, ASP.NET-ресурсы, создающие HTML), либо для всех ASP.NET-ресурсов. Если вы используете интегрированный режим IIS 7 , то вы можете заставить ваш фильтр работать с выходными данными любого типа. Данная статья предоставляет обзор фильтров ответа и демонстрирует два фильтра: простой фильтр, который вырезает пробелы для того, чтобы уменьшить размер переданной разметки, и фильтр, который добавляет сообщение об авторских правах в нижней части всех страниц. Вы можете скачать эти два фильтра, а также приложение с примерами в C# и Visual Basic, в конце статьи. Читайте далее, чтобы узнать больше об этом!

Краткий обзор фильтров ответа

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

Поток - это объект, который  расширяет класс Stream в пространстве имен System.IO. Вкратце, поток функционирует как буфер, где данные могут быть записаны и считаны. Существует множество встроенных в .NET Framework потоков, которые используют различные хранилища в качестве буфера. К примеру, класс MemoryStream является потоком, который хранит свой буфер в памяти, а FileStream - в файле на жестком диске.

Потоки имеют источник входных данных и место выходных данных. Данные получены при помощи метода Read и добавляются к потоку при помощи вызова метода Write. Следующая диаграмма демонстрирует поток, используемый объектом HttpWriter - объект HttpWriter записывает  в поток данные с обрабатываемой страницы , которые потом  считываются и отправляются клиенту.


Одной из полезных возможностей потоков является то, что они могут быть организованы в цепи. Вы можете связать два или более потока, тем самым выходные данные одного будут являтся входными для другого. Фильтры ответов предоставляют разработчикам способ связывания специализированного потока в цепь с потоком, используемым объектом HttpWriter, как это показывает следующая диаграмма.


Используя данную технику, вы можете создать специализированный поток (или потоки) , обрабатывающий выходные данные до того, как они будут отосланы финальному потоку и возвращены запрашиваемому клиенту. Такие специализированные потоки называются фильтрами ответов (response filters). Фильтры ответов являются полезным инструментом для пересылки обработанного результата страницы. Они могут быть использованы для компресии ненужных пробелов, добавления стандартной разметки ко всем страницам (например, такой, как оповещение о правах или JavaScript для инструментов, как Google Analytics), переписывания разметки страницы для того, чтобы создавать совместимые с XHTML страницы, а также для того, чтобы применять шифрование (encryption) и компрессию (compression) к данным выходного потока. Далее статья демонстрирует способ создания и использования фильтров ответа и рассматривает два простых примера таких фильтров: один из них удаляет пробелы, а другой добавляет сообщение о правах в нижней части каждой страницы. Полный код данных фильтров ( как в Visual Basic, так и C#) доступен в конце данной статьи.

Построение простого фильтра ответа для удаления ненужных пробелов

Приложение, доступное в конце статьи, включает в себя два примера фильтров ответа. Первый из них , который мы сейчас рассмотрим - фильтр удаления ненужных пробелов (WhitespaceFilter).

Фильтр по удаления пробелов не должен быть использован в реальном приложении

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

<script type="text/javascript">
   // Show a messagebox
   alert('Hello, world!');
</script>

Данный код будет сжат в следующий :

<script type="text/javascript">// Show a messagebox alert('Hello, world!');</script> 

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

Как уже было упомянуто в данной статье, фильтры ответа это потоки. В качестве результата, при создании фильтра ответа вы должны создать класс, который унаследует класс Stream. Класс Stream является абстрактным классом, который определяет основную функциональность для всех потоков. Если ваш фильтр ответа будет напрямую наследовать данный класс, то вам понадобится реализовать несколько методов. Лучшим решением будет расширение класса MemoryStream , который уже определяет данные стандартные методы. Вот оболочка нашего фильтра WhitespaceFilterVB. (Чтобы увидеть версию данного кода на языке C# , загрузите приложение, предоставленное в конце статьи.)

Imports Microsoft.VisualBasic
Imports System.IO
Imports System.Text
Imports System.Text.RegularExpressions

Public Class WhitespaceFilterVB
   Inherits MemoryStream

   ...
End Class

Как демонстрирует диаграмма, указанная выше, поток в цепи получает входную информацию и затем выводит ее в другой поток. Поскольку фильтр ответа служит в качестве потока цепи, нам необходимо указать фильтру ответа, в какой поток он должен выводить данные. Это можно обработать путем создания частной переменной экземпляра типа Stream (outputStream) и затем, создав конструктор, который принимает Stream в качестве входных данных.

Public Class WhitespaceFilterVB
   Inherits MemoryStream

   Private outputStream As Stream = Nothing

   Public Sub New(ByVal output As Stream)
      outputStream = output
   End Sub

   ...
End Class

Последним шагом будет перегрузка метода Write базового класса. Методу Write передается буфер бита, готовый к отсылке в выходной поток. Из данного метода мы можем исследовать буфер и выполнить любые изменения до отсылки в выходной поток. Имейте в виду, что данный метод Write может быть вызван несколько раз с одной и той же страницы. Вспомните, что объект HttpWriter связывает данные в несколько частей и затем записывает их в выходной поток. Каждый раз, как HttpWriter высылает порцию данных, будет вызван метод Write вашего фильтра ответа

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

 Public Class WhitespaceFilterVB
   Inherits MemoryStream

   ...

   Private tabsRe As New Regex("\t", RegexOptions.Compiled Or RegexOptions.Multiline)
   Private carriageReturnRe As New Regex(">\r\n<", RegexOptions.Compiled Or RegexOptions.Multiline)
   Private carriageReturnSafeRe As New Regex("\r\n", RegexOptions.Compiled Or RegexOptions.Multiline)
   Private multipleSpaces As New Regex(" ", RegexOptions.Compiled Or RegexOptions.Multiline)
   Private spaceBetweenTags As New Regex(">\s<", RegexOptions.Compiled Or RegexOptions.Multiline)

   Public Overrides Sub Write(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer)
      ' Преобразование содержимого буфера в строку
      Dim contentInBuffer As String = UTF8Encoding.UTF8.GetString(buffer)

      ' Вырезание всех пробелов, удаление всех символов табуляции, замена всех символов возврата каретки пробелом и компрессия множества подряд идущих пробелов

      contentInBuffer = tabsRe.Replace(contentInBuffer, String.Empty)
      contentInBuffer = carriageReturnRe.Replace(contentInBuffer, "><")
      contentInBuffer = carriageReturnSafeRe.Replace(contentInBuffer, " ")

      While (multipleSpaces.IsMatch(contentInBuffer))
         contentInBuffer = multipleSpaces.Replace(contentInBuffer, " ")
      End While

      contentInBuffer = spaceBetweenTags.Replace(contentInBuffer, "><")

      outputStream.Write(UTF8Encoding.UTF8.GetBytes(contentInBuffer), offset, UTF8Encoding.UTF8.GetByteCount(contentInBuffer))
   End Sub
End Class


Применение фильтра ответа (Response Filter)

Объект Response имеет свойство Filter , которое используется для связки фильтров ответа в цепь. Вы можете установить данное свойство тремя способами:

  • С определенной веб-страницы. Со страницы вы можете установить свойство Response.Filter. Это работает для определенных страниц, которые имеют следующий код. Добавьте следующий код к обработчику события Page_Load страницы и примените фильтр ответа:
    Response.Filter = new ResponseFilterClassName(Response.Filter)

    В случае с фильтром WhitespaceFilterVB код будет выглядеть следующим образом:

    Response.Filter = new WhitespaceFilterVB(Response.Filter)
  • Из соответствующего обработчика события в Global.asax. Фильтры ответа применяются после события PostReleaseRequestState объекта HttpApplication. Поэтому, вы можете добавить фильтр ответа во время данного события или раньше. К примеру, следующий обработчик события в Global.asax добавляет фильтр WhitespaceFilterVB ко всем страницам с содержимым типа "text/html" (ContentType):
    Sub Application_PostReleaseRequestState(ByVal sender As Object, ByVal e As EventArgs)
       If Response.ContentType = "text/html" Then
          Response.Filter = New WhitespaceFilterVB(Response.Filter)
       End If
    End Sub
  • Из HTTP Module. HTTP Module являются управляемыми классами, которые могут отвечать на события приложений (такие, как PostReleaseRequestState). HTTP Module может быть использован для установки свойства Response.Filter.

Загрузите пример, доступный в конце данной статьи, чтобы увидеть фильтр WhitespaceFilterVB в действии. Домашняя страница примера (Default.aspx) содержит два элемента TextBox - первый показывает оригинальное содержимое страницы WhitespaceFilterDemo.aspx, что включает в себя символы перехода каретки, табуляцию и множество пробелов - все в 2,603 битах. Второй элемент TextBox на странице демонстрирует содержимое страницы WhitespaceFilterDemo.aspx, просматриваемое в обозревателе. Избыток пробелов был исправлен, сократив размер обрабатываемого содержимого до 2,282 бита. Следующий рисунок демонстрирует страницу Default.aspx в действии.

Связываем множество фильтров ответа в цепи

Как мы только что увидели, существует возможность связывания специализированного фильтра ответа между объектом HttpWriter и выходящим потоком, но вас ничего не остановит от связывания всего лишь одного потока. Приложение данной статьи включает в себя второй фильтр ответа, названный AddCopyrightFilter , который внедряет сообщение о правах в нижней части всех страниц, к которым он был применен. В частности, он добавляет следующее содержимое сразу же после закрывающего тега </body>:

<div class="Copyright">
   Copyright Scott Mitchell CurrentYear
</div>

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

Фильтр AddCopyrightFilter расширяет класс MemoryStream и перегружает его метод Write , но работает немного по-другому, чем WhitespaceFilter. Вместо обработки каждой части данных, которые приходят в метод Write, AddCopyrightFilter буферизирует входящие данные в StringBuilder. Как только  буфер содержит "</html>", он внедряет сообщение о правах и высылает полный текст буфера в следующей поток цепи. (Данное поведение предполагает, что страницы, с которыми работает фильтр, заканчиваются текстом "</html>". Если данный фильтр будет использоваться на странице, которая не заканчивается тегом "</html>" , то клиенту ничего не будет выслано!)

Фильтры ответа AddCopyrightFilter и WhitespaceFilter могут быть связаны в цепь. К примеру, вы можете установить свойство Response.Filter следующим образом :

Response.Filter = new AddCopyrightFilter(new WhitespaceFilter(Response.Filter))

Приложение, доступное в конце статьи, не использует в точности тот код, который я указал выше, но вместо этого реализует цепь фильтров связывая AddCopyrightFilter в Global.asax и WhitespaceFilter в WhitespaceFilterDemo.aspx класса с фоновым кодом страницы.

Вывод

Фильтры ответа (Response filters) это механизм, которым разработчик может программно исследовать и модифицировать поток выходящих данных после того, как содержимое веб-страницы было обработано, но до того, как данные были возвращены клиенту. Фильтры ответа могут быть использованы для компрессии либо модификации выходящих данных, как к примеру добавление сообщения о правах или стандартный текст. Данная статья рассмотрела два примера фильтров ответа: один удалял лишние пробелы из обработанного HTML-кода, а другой добавлял сообщение о правах на сайт в нижней части всех страниц, к которым был применен фильтр.

Веселого программирования!

Scott Mitchell

Скачать исходники примеров к статье