(ASP.NET) ASP.NET (2006 год)

Модификация выходного потока Web-приложения

На этой страничке я расскажу как можно что-то изменить непосредственно в выходном потоке, сформироованным движком ASP2.

Для такой модификации к ASP2-конвееру подключается свой модуль на одно из событий ASP2-конвеера. Меня несколько удивляет, что точки подключения у разных программистов получаются разные, однако в своем способе подключения я уверен, ибо только он прошел все мои тесты. Я считаю, что это надо делать в событии PostRequestHandlerExecute.

Сама по себе модификация может быть разная. Например дописывание подвала на страничку. Хотя теперь это особого смысла не имеет (при наличии MasterPage). Странно, что именно этот малоактуальный пример опубликован у Экспозито. Лично меня заинтересовало уплотнение выходного потока и избавление его от лишних тегов. Средняя экономия, которую я вижу на своих тестах - составляет 15%. Это немало и за это стоит бороться, особенно если учесть тяжеловесность ASP2 страничек, а также если убирать не только конец строки, а например пустые теги ALT у рисунков, ненужные идентификаторы, которые добавляет движок ASP2, то я думаю, можно и 30% экономии достичь.

К сожалению тут все не так просто. Во-первых, через выходной поток идут и обращения к WebResource.AXD, который вынимает из сервера рисунки и скрипты. Response, исходящие от WEB-сервера, на реквесты к WebResource.AXD, трогать совершенно нельзя, клиентский скрипт в браузере мгновенно падает (хотя логическое обьяснение этого мне непонятно). Во-вторых, сами скрипты, находящиеся на страничке - тоже нельзя модифицировать, ибо работать они перестают. В первую очередь это относится к динамическим меню, пейджеру GridView и популированию дерева. Кроме того, нельзя трогать строки с ViewState.

Выходной поток ASP2-движок формирует блоками размером примерно по 25-30 килобайт, причем режет их совершенно произвольно, баквально посредине тега. Поэтому полный разбор очередного фрагмента HTML затруднен, учитывая, что там могут быть начало или конец клиентского скрипта, строки состояния или много таких вложений сразу. По идее этот разбор можно было бы сделать и регулятным выражением, например так (по идее, это найденное мною в инете выражение должно удалять пробелы)
Dim Reg1 As System.Text.RegularExpressions.Regex = New Regex("(?<=[^])\t{2,}|(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,11}(?=[<])|(?=[\n])\s{2,}")
TargetStr = Reg1.Replace(TargetStr, "")

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

00001: Imports System
00002: Imports System.Web
00003: Imports System.IO
00004: 
00005: Public Class Handler1 : Implements IHttpModule
00006: 
00007:     Public Sub Init(ByVal context As System.Web.HttpApplication) Implements IHttpModule.Init
00008:         AddHandler context.<b>PostRequestHandlerExecute</b>, AddressOf SetFilter
00009:     End Sub
00010: 
00011:     Public Sub Dispose() Implements IHttpModule.Dispose
00012:         '
00013:     End Sub
00014: 
00015:     Private Sub SetFilter(ByVal sender As Object, ByVal e As System.EventArgs)
00016:         Dim Response As HttpResponse = CType(sender, HttpApplication).Response
00017:         Dim Request As HttpRequest = CType(sender, HttpApplication).Request
00018:         Dim Application As HttpApplicationState = CType(sender, HttpApplication).Application
00019:         'а вот Session так достать нельзя - он тут есть не всегда
00020:         If Request.Url.Segments(Request.Url.Segments.Length - 1) &lt;&gt; "WebResource.axd" Then
00021:             Select Case Response.ContentType
00022:                 Case Nothing 'image/gif
00023:                     'со стандартным фильтром
00024:                 Case "text/html"
00025:                     'подключаем наш фильтр для уплотнения
00026:                     Dim DumpDirectory As String = Application("DumpDirectory")
00027:                     If DumpDirectory = Nothing Then
00028:                         Response.Filter = New StdFilter(Response.Filter)
00029:                     Else
00030:                         Response.Filter = New StdFilter(Response.Filter, Response.ContentEncoding, Response.ContentType, Application("DumpDirectory") & Guid.NewGuid.ToString & "_" & Request.Url.Segments(Request.Url.Segments.Length - 1), False)
00031:                     End If
00032:                 Case "application/x-javascript"
00033:                     'не уплотнять, падает сразу
00034:             End Select
00035:         Else
00036:             'только для трассировки Java-скриптов - работать так не будет, меню и прочее в браузере падает
00037:             If Application("DumpWebResource") And Application("DumpDirectory") &lt;&gt; "" Then
00038:                 Response.Filter = New StdFilter(Response.Filter, Response.ContentEncoding, Response.ContentType, Application("DumpDirectory") & Guid.NewGuid.ToString & "_" & Request.Url.Segments(Request.Url.Segments.Length - 1), True)
00039:             End If
00040:         End If
00041:     End Sub
00042: 
00043: End Class
00044: 
00045: Public Class StdFilter
00046:     Inherits Stream
00047:     Dim m_fs As FileStream = Nothing
00048:     Dim m_sink As Stream = Nothing
00049:     Dim m_position As Long = Nothing
00050:     Dim m_CurEncoding As Encoding = Encoding.UTF8
00051:     Dim m_ContentType As String = "text/html"
00052:     Dim m_TraceOnly As Boolean = False
00053: 
00054:     Sub New(ByVal sink As Stream)
00055:         m_sink = sink
00056:     End Sub
00057: 
00058:     Sub New(ByVal sink As Stream, ByVal Coding As Encoding)
00059:         m_sink = sink
00060:         m_CurEncoding = Coding
00061:     End Sub
00062: 
00063:     Sub New(ByVal sink As Stream, ByVal Coding As Encoding, ByVal ContentType As String)
00064:         m_sink = sink
00065:         m_CurEncoding = Coding
00066:         m_ContentType = ContentType
00067:     End Sub
00068: 
00069:     Sub New(ByVal sink As Stream, ByVal Coding As Encoding, ByVal ContentType As String, ByVal DumpFileName As String)
00070:         m_sink = sink
00071:         m_CurEncoding = Coding
00072:         m_ContentType = ContentType
00073:         m_fs = New FileStream(DumpFileName, FileMode.OpenOrCreate, FileAccess.Write)
00074:     End Sub
00075: 
00076:     Sub New(ByVal sink As Stream, ByVal Coding As Encoding, ByVal ContentType As String, ByVal DumpFileName As String, ByVal TraceOnly As Boolean)
00077:         m_sink = sink
00078:         m_CurEncoding = Coding
00079:         m_ContentType = ContentType
00080:         m_fs = New FileStream(DumpFileName, FileMode.OpenOrCreate, FileAccess.Write)
00081:     End Sub
00082: 
00083:     Public Overrides ReadOnly Property CanRead() As Boolean
00084:         Get
00085:             Return True
00086:         End Get
00087:     End Property
00088: 
00089:     Public Overrides ReadOnly Property CanSeek() As Boolean
00090:         Get
00091:             Return False
00092:         End Get
00093: 
00094:     End Property
00095: 
00096:     Public Overrides ReadOnly Property CanWrite() As Boolean
00097:         Get
00098:             Return False
00099:         End Get
00100:     End Property
00101: 
00102:     Public Overrides ReadOnly Property Length() As Long
00103:         Get
00104:             Return 0
00105:         End Get
00106:     End Property
00107: 
00108:     Public Overrides Property Position() As Long
00109:         Get
00110:             Return m_position
00111:         End Get
00112:         Set(ByVal Value As Long)
00113:             m_position = Value
00114:         End Set
00115:     End Property
00116: 
00117:     Public Overrides Function Seek(ByVal offset As Long, ByVal direction As SeekOrigin) As Long
00118:         Return 0
00119:     End Function
00120: 
00121:     Public Overrides Sub SetLength(ByVal length As Long)
00122:         m_sink.SetLength(length)
00123:     End Sub
00124: 
00125:     Public Overrides Sub Close()
00126:         m_sink.Close()
00127:         If Not (m_fs Is Nothing) Then
00128:             m_fs.Close()
00129:         End If
00130:     End Sub
00131: 
00132:     Public Overrides Sub Flush()
00133:         m_sink.Flush()
00134:     End Sub
00135: 
00136:     Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Int32, ByVal count As Int32) As Int32
00137:         Return m_sink.Read(buffer, offset, count)
00138:     End Function
00139: 
00140:     Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Int32, ByVal count As Int32)
00141:         If Not (m_fs Is Nothing) Then
00142:             'трассировка
00143:             m_fs.Write(buffer, 0, count)
00144:         End If
00145:         '
00146:         If m_TraceOnly Then
00147:             'требовалась только трассировка
00148:             m_sink.Write(buffer, 0, count)
00149:         Else
00150:             'уплотнение
00151:             Dim Str1 As New StringBuilder(m_CurEncoding.GetString(buffer, offset, count))
00152:             Dim TargetStr As String = Str1.ToString
00153:             Dim Pos1 = TargetStr.IndexOf(vbCrLf & "&lt;script type=""text/javascript""&gt;" & vbCrLf & "&lt;!--" & vbCrLf)
00154:             Dim Pos2 = TargetStr.IndexOf(vbCrLf & "// --&gt;" & vbCrLf & "&lt;/script&gt;" & vbCrLf)
00155:             Dim Pos3 = TargetStr.IndexOf("&lt;input type=""hidden"" name=""__VIEWSTATE")
00156:             If Pos1 = -1 And Pos2 = -1 And Pos3 = -1 Then
00157:                 'такой блок можно чистить как угодно 
00158:                 Do While Str1.ToString.Contains("  ")
00159:                     Str1.Replace("  ", " ")
00160:               Loop
00161:                 Str1.Replace(vbCr, "")
00162:                 Str1.Replace(vbTab, "")
00163:                 Str1.Replace(vbLf, "")
00164:                 TargetStr = Str1.ToString
00165:                 Dim Compress As Single = count / TargetStr.Length
00166:                 'типичный размер блока 25 тыс символов, коэффициент уплотнения - 15%
00167:                 m_sink.Write(m_CurEncoding.GetBytes(TargetStr), offset, m_CurEncoding.GetBytes(TargetStr).Length)
00168:             Else
00169:                 'в этом блоке текста один или много выделенных ASP2 скриптов, начало или хвост скрипта, с вьюстейтами тоже проблемы
00170:                 'сюда можно вставить полную разборку фрагмента HTML и уплотнение уже по избранным тегам
00171:                 m_sink.Write(buffer, 0, count)
00172:             End If
00173:         End If
00174:     End Sub
00175: End Class

Как видите, тут все просто. Создается собственный поток, который подменяет стандартный поток, используемый движком ASP2 для вывода сформированного HTML. Все пересчитывается с учетом кодировки, тк анализировать мы может строки, содержащие символы, а сам по себе сырой HTML-поток ASP2-конвеер формирует в кодировке UTF8. Помимо обширных комментариев я добавил также трассировку всех данных, сформированных движком ASP2. Это могут быть скрипты или рисунки. Как я уже говорил выше, с такой трассировкой странички работать не будут. Это просто дешевая замена утилите IeWatch или аналогичной и возможность все-таки увидеть полностью, глубоко запрятанные в недра ASP2 ее основные рабочие скрипты. Управление трассировкой я деляю в Global.Asax, a конфигурование я вынес админам в Web.config.

Кстати, на предпоследнем рисунке вы можете посмотреть, как я формирую ключевые слова для странички, поднимая их из SQL и добавляя их в MasterPage. В базу они тоже попадают не с бухты-барахты, но это уже отдельная история.



В заключение рассмотрим еще одну распространенную технологию модификации выходного потока. Как вы знаете, браузер поддерживает автоматическую упаковку HTML. Это практически зипование, поэтому для текстового файла оно предельно эффективно - тексты пакуются практически в 10 раз. Посмотреть это можно просто - запустить Fiddler один раз с Accept-encoding:gzip , а потом без оного. Как вы видите - разница десятикратная. К великому сожалению клиентские скрипты падают так же, как и в предыдущем случае. Текст этой упаковки (не мой, стандартный) построен по такому же принципу, как мой текст выше, и на всякий случай я его выкладываю тут тоже, хотя применимость его для ASP2 весьма и весьма сомнительна (без меню, деревьев, табличного пейджера и множества других штучек, использующих клиентские скрипты).

00001: Imports System
00002: Imports System.Web
00003: Imports System.IO.Compression
00004: 
00005: Public Class Handler2 : Implements IHttpModule
00006: 
00007:     Public Sub Init(ByVal context As System.Web.HttpApplication) Implements IHttpModule.Init
00008:         AddHandler context.BeginRequest, New EventHandler(AddressOf Me.context_BeginRequest)
00009:     End Sub
00010: 
00011:     Private Sub context_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
00012:         Dim application1 As HttpApplication = TryCast(sender, HttpApplication)
00013:         If Me.IsEncodingAccepted("gzip") Then
00014:             application1.Response.Filter = New GZipStream(application1.Response.Filter, CompressionMode.Compress)
00015:             Me.SetEncoding("gzip")
00016:         ElseIf Me.IsEncodingAccepted("deflate") Then
00017:             application1.Response.Filter = New DeflateStream(application1.Response.Filter, CompressionMode.Compress)
00018:             Me.SetEncoding("deflate")
00019:         End If
00020:     End Sub
00021: 
00022:     Private Function IsEncodingAccepted(ByVal encoding As String) As Boolean
00023:         Return ((Not HttpContext.Current.Request.Headers.Item("Accept-encoding") Is Nothing) AndAlso HttpContext.Current.Request.Headers.Item("Accept-encoding").Contains(encoding))
00024:     End Function
00025: 
00026:     Private Sub SetEncoding(ByVal encoding As String)
00027:         HttpContext.Current.Response.AppendHeader("Content-encoding", encoding)
00028:     End Sub
00029: 
00030:     Public Sub Dispose() Implements IHttpModule.Dispose
00031:         '
00032:     End Sub
00033: End Class

Впрочем, при хостинге сайтов на IIS 6.0 все идеи насчет уплотнения выходного потока - идеи чисто теоретические. Ровно в три щелчка IIS 6.0 позволяет настроить уплотнение прямо на уровне IIS:



Комментарии к этой страничке ( )
ссылка на эту страничку: http://www.vb-net.ru/asp2/14/index.htm
<Назад>  <Назад>  <Назад>  <На главную>  <В раздел ASP>  <В раздел NET>  <В раздел SQL>  <В раздел Разное>  <Написать автору>  < Поблагодарить>
Московская хельсинская группа   Радио Свобода  Новая газета   The New Times (Новое время)