Андрей Ниденс
"Открытые Системы"
Windows 2000 Magazine, #06/2002
Алгоритм gZip поможет сберечь полосу пропускания
Использование в протоколе НТТР 1.1. сжатия с помощью алгоритма gZip позволяет добиться увеличения скорости передачи пользователю сгенерированных страниц. По моим наблюдениям, благодаря этой возможности объем передаваемых по сети данных можно уменьшить в 10 раз, тем самым сокращая время ожидания. Не во всех версиях IIS имеет встроенную поддержку компрессии по алгоритму gZip (http://support.microsoft.com/default.aspx?scid=kb;en-us;Q276488), позволяющую сжимать не только статические, но и динамически сформированные страницы.
Вопросы конфигурирования и использования встроенной в IIS компрессии обсуждались на многих форумах, и я пришел к выводу, что настройка данной возможности IIS - занятие не для начинающих. Можно ли произвести сжатие не встроенными средствами IIS? После поиска в Internet я нашел пару компаний, разрабатывающих ISAPI-фильтры, которые позволяют сжимать данные в обход стандартных методов IIS. При дальнейшем изучении продуктов этих компаний оказалось, что они дорого стоят и сложно конфигурируются. Что же делать начинающему Web-мастеру, работающему с IIS? Попробуем выполнить компрессию своими руками.
Конечно, метод, который предлагается ниже, не претендует на звание самого эффективного и вряд ли его стоит применять на сайтах, которые обслуживают в день сотни тысяч клиентов. Однако для небольших компаний, не сильно загруженных сайтов и внутренних приложений в сети компании он годится. Мы использовали его для приложений и проверяли на клиентах, подключенных на скорости 9600 бод. В качестве клиентской программы использовались браузеры IE старше версии 5.0, а также Netscape Navigator 4.72.
Для написания кода нам понадобится только один "внешний" компонент: стандартный gZip, переписанный с сайта www.gzip.org (http://www.gzip.org/gzip124xN.zip). GZip.exe можно переписать в один из системных каталогов (я записал его в winnt), чтобы его было удобно вызывать. Все остальные объекты, которые мы будем использовать, разработаны компанией Microsoft.
Для начала проведем маленький эксперимент - попробуем создать в корневом каталоге IIS (по умолчанию это c:\inetpub\wwwroot) каталог test, затем войдем в консоль Internet Information Services и создадим для каталога test-приложение (см. Экран 1).
Экран 1. Назначение каталогу приложения.
Поместим в созданный каталог файл test.html со следующим содержанием:
<HTML> <HEAD> <TITLE>Test</TITLE> </HEAD> <BODY> 1234567890<br> 1234567890<br> 1234567890<br> 1234567890<br> 1234567890<br> </BODY> </HTML>
Вызовем оболочку MS-DOS, перейдем в каталог, в который мы записали test.html, и сожмем его с помощью gZip.
gzip.exe -c -n -a -9 test.html " test.gz
Получим сжатый файл test.gz. Можно сравнить размеры двух файлов - исходный файл test.html занимал у меня на диске 304 байт, после компрессии test.gz стал занимать 85 байт. Уменьшение почти в три с половиной раза!!! В продолжение эксперимента создадим в том же каталоге страничку test.asp (см. Листинг 1).
Листинг 1. Тестовая asp-страница.
<%@ LANGUAGE=>VBScript> %> <% Response.AddHeader <content-encoding>, <gzip> Const adTypeBinary = 1 Dim strFilePath Set objStream = Server.CreateObject(<ADODB.Stream>) objStream.Open objStream.Type = adTypeBinary objStream.LoadFromFile Request.ServerVariables (<APPL_PHYSICAL_PATH>)+>test.gz> Response.BinaryWrite objStream.Read objStream.Close Set objStream = Nothing %>
Если все сделано правильно, то вызов
http://localhost/test/test.html и
http://localhost/test/test.asp выдаст в окне браузера одну и ту же страницу. Разница только в том, что при вызове http://localhost/test/test.asp количество информации, переданной от сервера к клиенту почти вдвое меньше.
В коде, размещенном в файле test.asp, упоминается объект ADODB.Stream, который используется для чтения с диска сжатого файла test.gz. Подробнее об этом объекте можно прочесть на сайте Microsoft по адресу: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q276488.
Теперь реализуем более полную версию программы сжатия для asp-страниц. В первую очередь нужно позаботиться о том, чтобы можно было вызывать из ASP компонент WScript.Shell и выполнять с его помощью вызов gZip. Для этого c помощью приложения Computer Management необходимо создать учетную запись для нового пользователя, которую затем указать в консоли Internet Information Services как учетную запись анонимного пользователя, на закладке Directory Security для нашего приложения, созданного в IIS (см. Экран 2).
Экран 2. Новая учетная запись для анонимного пользователя.
Теперь переходим непосредственно к программированию. Необходимо позаботиться о том, чтобы генерируемая для пользователя страница не сразу передавалась клиенту, а кэшировалась в файле на сервере для дальнейшего сжатия. Создаем на нашем сервере файл output.inc, содержимое которого представлено в Листинге 2.
Листинг 2. Программа сжатия отсылаемого файла.
<% Const OpenFileForReading = 1 Const OpenFileForWriting = 2 Const OpenFileForAppending = 8 Const adTypeBinary = 1 function CreateTempFile(name) if InStr(LCase(Request.ServerVariables(<HTTP_ACCEPT_ENCODING>)), LCase(<gzip>))=0 then CreateTempFile =>> else Randomize pPath=Request.ServerVariables(<APPL_PHYSICAL_PATH>) if Len(name)=0 then filename= pPath +>c_>+CStr(Int(1001 * Rnd)) else filename= pPath +name end if filename=Replace(filename,>\\>,>\>) Set fso = CreateObject(<Scripting.FileSystemObject>) Set tf = fso.CreateTextFile(filename+>.txt>, True) tf.Close Set tf = Nothing Set fso = Nothing CreateTempFile = filename end if end function sub Write(filename,str) if Len(filename)=0 then Response.write(str) else Dim fso, tf Set fso = CreateObject(<Scripting.FileSystemObject>) Set tf = fso.OpenTextFile(filename+>.txt>, OpenFileForAppending, True, TristateFalse) tf.Write(CStr(str)) tf.Close Set tf = Nothing Set fso = Nothing end if end sub function Compress(filename) if Len(filename)>0 then command=>cmd /c gzip.exe -c -n -a -9 <+filename+>.txt > <+filename+>.gz> Set WshShell = CreateObject(<WScript.Shell>) WshShell.Run command,0,true Set WshShell=Nothing end if end function function Send(filename) if Len(filename)>0 then Response.AddHeader <content-encoding>, <gzip> Set objStream = Server.CreateObject (<ADODB.Stream>) objStream.Open objStream.Type = adTypeBinary objStream.LoadFromFile filename+>.gz> Response.BinaryWrite objStream.Read objStream.Close Set objStream = Nothing end if end function function DeleteFiles(filename) if Len(filename)>0 then Set fso = CreateObject(<Scripting.FileSystemObject>) fso.DeleteFile(filename) Set fso = Nothing end if end function %>
В принципе, код довольно прост, но все-таки хочется пояснить, для чего нам понадобились подобные функции и подпрограмма. Функция CreateTempFile предназначена для создания пустого временного файла, в который мы направим весь поток формируемых для клиента данных. Эта функция возвращает имя созданного файла. Если браузер клиента не умеет работать с gZip-компрессией, возвращается пустое значение. Это означает, что генерируемые данные нужно отправлять клиенту без сжатия. Подпрограмма Write осуществляет запись строки данных str в файл с именем filename или передает строку данных клиенту, если filename содержит пустую строку. Когда страница будет готова, необходимо вызвать функцию Compress - она сжимает исходный файл, содержащий сгенерированную страницу. Функция Send завершает вывод информации клиенту - она непосредственно передает клиенту полученный в результате сжатия файл. Можно приступить к испытаниям программы сжатия. Создаем новый файл test1.asp (см. Листинг 3).
Листинг 3. Страница test1.asp для проверки программы сжатия.
<%@ LANGUAGE=>VBScript> %> <!-- #INCLUDE VIRTUAL=>/test/output.inc> --> <% Response.Buffer = TRUE file=>> file=CreateTempFile(<>) call Write(file,><html><head><title>Test< /title></head><body><table border='0'>>) color=>#d0fdc8> for i=0 to 1999 if color=>#d0fdc8> then color=>#9ef988> else color=>#d0fdc8> end if call Write(file,><tr>>) call Write(file,><td bgcolor='>+color+>'> Row <+CStr(i)+></td>>) call Write(file,><td bgcolor='>+color+>'>> +CStr(Rnd(i))+></td>>) call Write(file,></tr>>) next call Write (file,></table></body></html>>) call Compress(file) call Send(file) call DeleteFiles(file&>.txt>) call DeleteFiles(file&>.gz>) %>
Текст этого файла специально написан так, чтобы мы получили выходной файл большого размера. Загружаем в браузере написанный test1.asp. Кажется, ничего особенного не происходит - мы видим обычную таблицу длиной 2000 строк (см. Экран 3).
Экран 3. Результат работы test1.asp.
Временно закомментируем в файле test1.asp две строчки
call DeleteFiles(file&".txt") call DeleteFiles(file&".gz")
И еще раз вызовем в браузере страницу test1.asp. В нашем каталоге на сервере появилось два новых файла, причем файл с расширением gz значительно меньше файла txt (у меня исходный файл имел размер 217 Кбайт, а сжатый - 17 Кбайт, почти в 13 раз меньше). Именно этот маленький файл и отправляется клиенту при запросе страницы test1.asp. Интересно содержимое полученного файла с расширением txt. Если открыть его, мы увидим, что, хотя был написан хорошо размеченный текст test1.asp, в полученном файле txt нет лишних пробелов, символов табуляции, переводов строк и т. д. Конечно, все это помогает разработчику ориентироваться в структуре HTML-документа, однако совершенно не нужно для браузера, который прекрасно разбирает даже написанный в одну строку код HTML. Такой побочный эффект от использования нашей программы компрессии позволяет дополнительно уменьшить объем передаваемой информации. Теперь можно попробовать подключиться к нашему серверу с помощью модема и вызвать test1.asp с удаленного компьютера.
Каковы плюсы и минусы описанного выше метода? Выигрыш в размере предаваемой информации - это плюс. Однако нам пришлось выполнить лишние операции на сервере с жестким диском и использовать лишнее процессорное время - это минус. Конечно, диск - относительно медленное устройство, но следует помнить, что модем пользователя - устройство еще более медленное, и часто время, потраченное на передачу несжатого документа, намного больше, чем время, необходимое для сжатия.
Приведенная схема - это только набросок, ее можно развить, добавив отслеживание актуальности страницы, что позволит избежать повторного создания и сжатия страниц. К примеру, если сайт, который мы разрабатываем, не содержит динамически изменяемых данных (например, если он представляет собой сборник статей, каждая из которых будет содержаться в обычном HTML-файле), то можно использовать метод оптимизации, основанный на нашей методике. После создания статической страницы ее можно сжать с помощью gZip, а полученный сжатый файл передавать клиентам сайта вместо первичных данных. Нам только придется позаботиться о том, чтобы в случае внесения изменений в исходную информацию происходило автоматическое обновление соответствующего сжатого файла. Попробуем решить эту задачу. Допишем в наш output.inc следующую функцию (см. Листинг 4).
Листинг 4. Добавление для output.inc.
function CompareModifyDate(filename1, filename2) if InStr(LCase(Request.ServerVariables(<HTTP_ACCEPT_ENCODING>)), LCase(<gzip>))=0 then CompareModifyDate=1 else on error resume next Set fso = CreateObject(<Scripting.FileSystemObject>) Set File1 = fso.GetFile(filename1) Set File2 = fso.GetFile(filename2) if (File1.DateLastModified>File2.DateLastModified) or (File2.Size=0) then CompareModifyDate=1 else CompareModifyDate=2 end if Set File2 = Nothing Set File1 = Nothing Set fso = Nothing end if end function
Данная функция предназначена для сравнения даты последней модификации двух файлов, имена которых передаются в качестве параметров. Одним из этих файлов будет файл с исходной информацией (имя файла - первый параметр функции), а вторым - сжатый файл (имя файла - второй параметр функции). В том случае, если исходный файл более новый или если сжатый файл имеет нулевую длину (например, его просто нет на диске), функция возвращает значение 1.
Попробуем написать "самосжимающуюся" статическую страницу. Страницу test1.asp нужно переделать в test2.asp так, как показано в Листинге 5.
Листинг 5. Самосжимаемая asp-страница
<%@ LANGUAGE=>VBScript> %> <!-- #INCLUDE VIRTUAL=>/test/output.inc> --> <% ' Основная идея в том, чтобы проверить, является ли дата создания этого файла более поздней, ' чем дата создания сжатого файла. ' Если да, надо создать сжатый файл заново ' Если нет - то надо передавать клиенту существующий сжатый файл Response.Buffer = TRUE file=>test2> file=CreateTempFile(file) compare=CompareModifyDate(file&>.asp>,file&>.gz>) if CInt(compare)=1 then 'Создание нового содержимого и компрессия данных 'Здесь можно вставлять вызовы функции Write, с помощью которых 'формировать текст нашей <статической> страницы '''''''''''''''''''''''''''''''''''''''''''''''''''''' call Write(file,><html><head><title>Test< /title></head><body><table border='0'>>) color=>#d0fdc8> for i=0 to 1999 if color=>#d0fdc8> then color=>#9ef988> else color=>#d0fdc8> end if call Write(file,><tr>>) call Write(file,><td bgcolor='>+color+>'>Row <+CStr(i)+></td>>) call Write(file,><td bgcolor='>+color+>'>> +CStr(2000-i)+></td>>) call Write(file,></tr>>) next call Write (file,></table></body></html>>) '''''''''''''''''''''''''''''''''''''''''''''''''''''' 'Компрессия данных call Compress(file) end if 'Удаление промежуточного файла call DeleteFiles(file&>.txt>) 'Отправка сжатых данных call Send(file) %>
Из комментариев, которые вставлены в текст этой страницы, становится ясно, как происходит "самосжимание". К сожалению, написанная страница отслеживает только собственную актуальность, и этот код пригоден лишь для сжатия "статических" страниц. Если же используются динамически изменяемые данные, например при формировании прайс-листа или форума, придется вместо функции CompareModifyDate написать функцию, которая будет проверять необходимость обновления архивного файла, основываясь на данных, предназначенных для вывода на экран.
АНДРЕЙ НИДЕНС - заместитель директора департамента информатики в Объединенном Грузинском банке. С ним можно связаться по адресу: root@ugb.com.ge