Сжатие трафика HTTP

Андрей Ниденс
"Открытые Системы"
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