Модификация выходного потока 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) <> "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") <> "" 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 & "<script type=""text/javascript"">" & vbCrLf & "<!--" & vbCrLf) 00154: Dim Pos2 = TargetStr.IndexOf(vbCrLf & "// -->" & vbCrLf & "</script>" & vbCrLf) 00155: Dim Pos3 = TargetStr.IndexOf("<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:
- Ставим галку Compress Application Files на вкладке Web Sites. Однако это далеко не все, как думают многие (в том числе и я поначалу).
- В узле Web Service добавляем сервис (задачу в IIS) работающую с библиотекой C:\WINDOWS\system32\inetsrv\gzip.dll. Сервис после добавления, естественно, должен работать. Но и это, увы не все.
- Наконец, главное. Надо указать IIS сжимать ASPX-файлы, ибо именно это и требуется. Это можно сделать только вручную. Для этого входим в Метабазу IIS (C:\WINDOWS\system32\inetsrv\MetaBase.xml) и вручную там указываем, что желаем сжимать ASPX-Файлы.
И вот только теперь можно насладиться ДЕСЯТИКРАТНЫМ УМЕНЬШЕНИЕМ РАЗМЕРА ASPX-СТРАНИЦ при котором все преотлично работает, причем даже с ASP2!.
|