Для иллюстрации, каким образом можно реализовать сжатие со стороны сервера, мы выбрали язык PHP, как наиболее распространенный и простой в понимании. Тем, кому язык знаком, знают, что в нем реализованы два встроенных механизма, позволяющие полностью автоматизировать процесс, но, к сожалению, они никак не помогают бороться с ошибками браузера.
<?php class mod_compress { // настройки // уровень сжатия static public $deflatelevel = 9; // минимальный размер ответа (в байтах), который будет // сжат (0 — нет ограничений) static public $minsize = 0; // максимальный размер static public $maxsize = 500000; // внутренняя переменная — поддерживает ли браузер сжатие static private $supported = null; static public function init() { // поддерживает ли браузер gzip? if (!self::issupportgzip()) { return self::$supported = false; } // каков тип ответа, тело которого будет сжимать? $type = self::detecttype(); if ($type == 'unknown') { return self::$supported = false; } $ua = $_SERVER['HTTP_USER_AGENT']; // сжатие браузер поддерживает, теперь нужно // исключить браузер, который поддерживает его // с ошибками — MSIE< 6.0SP2 if (preg_match('/MSIE [4-6](?:.(?!Opera|SV1))+/', $ua)) { return self::$supported = false; } // если требуется сжимать CSS/JS, то нужно // отфильтровать небезопасные браузеры if ( $type == 'notsafe' && preg_match('@Chrome/2|Konqueror|Firefox/(?:[0-2]\.|3\.0)@', $ua) ) { return self::$supported = false; } return self::$supported = true; } // посмотрим — поддерживает ли браузер gzip static private function issupportgzip() { foreach (preg_split('/\s*,\s*/', $_SERVER['HTTP_ACCEPT_ENCODING']) as $method) { // некоторые браузеры указывают вес // (предпочтения) методов сжатия, например, // "bzip2;q=0.9, gzip;q=0.1" // говорит о том, что браузер // хотел бы, чтобы ему отдавали контент, сжатый // методом bzip2, // но он поддерживает и gzip $method = explode(';', $method, 2); // но так как мы поддерживаем только gzip, вес // мы игнорируем if ($method[0] == 'gzip' || $method[0] == 'x-gzip') { return true; } } return false; } // отделим "безопасные" типы от "небезопасных" static private function detecttype() { // поддерживаются не всеми браузерами $notsafe = array('text/css', 'text/javascript', 'application/javascript', 'application/x-javascript', 'text/x-js', 'text/ecmascript', 'application/ecmascript', 'text/vbscript', 'text/fluffscript'); // поддерживаются всеми браузерами $safe = array('text/html', 'image/x-icon', 'text/plain', 'text/xml', 'application/xml', 'application/rss+xml'); foreach (headers_list() as $header) { if (stripos($header, 'content-type') === 0) { $header = preg_split('/\s*:\s*/', $header); $type = strtolower($header[1]); if (in_array($type, $safe)) return 'safe'; if (in_array($type, $notsafe)) return 'notsafe'; return 'unknown'; } } // в случае, если Content-type не задан, считаем, // что это text/html return 'safe'; } // проверка ограничний на размер static private function checksize($len) { if ($minsize && $len < $minsize) return false; if ($maxsize && $len > $maxsize) return false; return true; } // проверка, прошел ли запрос через прокси static private function checkproxy() { // в версии HTTP 1.1 есть обязательный заголовок, // который выставляет прокси, — Via, в HTTP/1.0 // такого признака нет return $_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0' || isset($_SERVER['HTTP_VIA']); } // обработчик, который решит, будет ли сжат контент, // и сожмет его static public function handler($content, $stage) { // проверим — нужно ли сжимать (не срабатывают ли // ограничения на размер) if ($content == '' || !self::checksize(strlen($content))) { return $content; } if (self::$supported === null) { self::init(); } // браузер не поддерживается, высылаем оригинал if (self::$supported === false) { return $content; } // этот заголовок скажет прокси-серверу и браузеру, // что возвращаемое нами будет иметь разное // содержимое в зависимости от типа браузера // и заголовка типа кодирования header('Vary: User-agent, Content-encoding'); // запрещаем прокси сохранять содержимое, // для обхода прокси, не справляющихся // с кэшированием сжатого контента if (self::checkproxy()) { header('Cache-control: private'); } return self::compress($content); } static public function compress($content) { // сжимаем текст gzip'ом $content = gzencode ($content, $deflatelevel,FORCE_GZIP); // не забываем отдать длину сжатого потока, // это *очень* важно: некоторые браузеры не // обрабатывают сжатый поток без указания длины header('Content-length: ' . strlen($content)); // …и метод его кодирования header('Content-encoding: gzip'); return $content; } } ob_start(array('mod_compress', 'handler')); // здесь вывод вашего скрипта
Как видим, задача не такая уж и сложная. Точкой входа в класс является метод handler, который мы передаем функции ob_start, — она и позаботится о том, чтобы весь дальнейший вывод попадал в нашу функцию. Так как класс устанавливает заголовки, нужно, чтобы вызов ob_start произошел в вашей программе как можно раньше, до любого вывода данных в браузер.
Класс также можно доделать, чтобы поддержать метод bzip2 (нужно проверять вхождения "bzip2", "x-bzip" или "bzip"), если ответ не является проксируемым. Для сжатия bzip2 в PHP есть одноименный модуль. Его также можно улучшить: заменить проверку наличия в заголовке gzip и сжатие на вызов ob_gzhandler, но мы этого делать не стали намеренно чтобы продемонстрировать, как это делается, если читатель захочет портировать код на другой язык.
Алгоритм работы класса следующий:
Браузер является настолько мощной платформой, поддерживающей такое количество разнообразных технологий, что одно и то же зачастую можно сделать несколькими способами, некоторые из которых красивы в силу своей изощренности, а другие — практичны, например, позволяют обойти ошибки браузеров.
Мы рассмотрим два метода реализации сжатия, которые можно использовать, если встроенное сжатие в клиентском браузере реализовано с ошибками. Первый метод — это сжатие через тег canvas, второе — сжатие JavaScript, реализованное на самом JavaScript.
Первый метод предложил Джейкоб Седелин в своем блоге "Nihilogic" (http://blog.nihilogic.dk/2008/05/compression-using-canvas-and-png.html); о реализации второго метода, возможно (тут трудно установить истину), впервые задумался один из авторов этой книги, реализовав в 2001 году в рамках проекта JUnix (http://junix.kzn.ru) простенькое сжатие, которое называлось jzip.
canvas — предложенный фирмой Apple тег, который на данный момент входит в HTML5. Он позволяет при помощи JavaScript создавать на ограниченном тегом участке растровые изображения.
Идея сжатия, реализуемого при помощи этого тега, проста — каждый байт сжимаемого содержимого представляется как точка изображения любого формата сжатия без потерь, поддерживаемого браузером (GIF, PNG). Это изображение запрашивается с сервера и подгружается в тег canvas методом drawImage (поддерживается браузерами Firefox 1.5 и выше, Safari 2.0 и выше, Opera 9.0 и выше, а также Google Chrome).
Как легко догадаться, из canvas изображение поточечно считывается, каждая точка представляется как символ, и полученная строка выполняется как JavaScript.
Уровень сжатия таким способом кода колеблется в условных пределах от 20 до 50%. Например, библиотека jQuery версии 1.2.3 сжимается с 53 Кб до 17, что экономит 32% (для сравнения: gzip сжимает ее до 15,5 Кб, bzip2 — до 14,6 Кб).
Особенно привлекательно в этом методе то, что часть JavaScript, ответственная за получение кода на стороне клиента, очень небольшая:
var codeimg = new Image() // когда картинка загрузится, вызовется эта функция codeimg.onload = function () { var code = '' // переменная, куда будет собираться код var size = 119 // размер изображения (оно квадратное) var canvas = document.createElement("canvas") canvas.width = canvas.height = canvas.style.width = canvas.style.height = size var inner = canvas.getContext("2d") // загружаем изображение в созданный нами CANVAS inner.drawImage(codeimg) // забираем содержимое и переводим его в символы var data = inner.getImageData(0, 0, size, size) for (var i = 0, len = data.length; i<len; i+="4" if="" (data=""[i=""] > 0) code += String.fromCharCode(data[i]) } eval(code) } // указываем URL изображения, где у нас хранится код codeimg.src = 'image-with-our-code.png'
Серверная часть также очень проста, реализацию на PHP можно посмотреть в блоге автора метода, так же легко она реализуется на любом другом языке — в квадратное изображение, сторона которого равна квадратному корню из значения длины файла, в каждую точку ставится по коду символа из передаваемого контента.
Таким же методом можно паковать и CSS — в конце вместо eval нужно лишь создать в DOM тег<style> с соответствующим содержимым.
Само изображение, получающееся в результате, ничего интересного собой не представляет — обычный бинарный шум.
У метода есть и недостатки. Во-первых, автор не использует цветовую составляющую для передачи каких-либо данных, и тут есть простор для экспериментов. Во-вторых, на текущий момент распаковка средних по размеру (200-500 Кб) данных занимает несколько секунд. В-третьих, метод поддерживается не всеми браузерами (например, не поддерживается IE). И в-четвертых, автор не позаботился о поддержке UTF-8 (что, впрочем, можно исправить).
Поскольку общее время, через которое скрипт будет доступен, равно сумме времени загрузки и времени, потраченного на его распаковку, нужно очень внимательно относиться к использованию этого метода — возможно, результатом его применения станет лишь увеличение времени ожидания.
Не исключено, что ситуацию можно исправить за счет использования цветовых компонент изображения. В этом случае размер передаваемых (а значит, и обрабатываемых скриптом) данных должен уменьшиться.
На клиентской стороне на JavaScript реализуют только распаковку данных, сжатие происходит на сервере и может быть реализовано на любом языке программирования.
Ничего необычного в реализации какого-либо алгоритма нет: JavaScript — такой же язык программирования, но есть специфика: браузеры на данный момент содержат недостаточно оптимизированные интерпретаторы, и это необходимо учитывать, иначе распаковка будет выполняться удручающе медленно.
Впрочем, на этом фронте есть улучшения. Новые интерпретаторы языков JavaScript браузеров Firefox, Safari и Google Chrome показывают впечатляющие результаты. Скажем, реализация на JavaScript сжатия LZW (Lempel-Ziv-Welch, используется в GIF и PDF) (http://zapper.hodgers.com/labs/?p=90) распаковывает в этих браузерах библиотеку Prototype 1.6.0.2 (это 123 Кб) на среднем ноутбуке менее чем за одну десятую секунды.
Впрочем, десятая "Опера", последняя на данный момент, показывает куда менее интересное время — полсекунды, а Internet Explorer 8.0 — 0,3 секунды.
В будущем, если там еще будут встречаться проблемы с реализацией gzip у браузеров, можно будет использовать реализации достаточно сложных методов сжатия на JavaScript, а в настоящем же приходится довольствоваться чем-то менее ресурсоемким.
Например, программа "Packer" Дина Эдвардса
(http://dean.edwards.name/packer/), использует алгоритм, который автор назвал "Base62", потому что в кодировании используется 62 символа — большие и маленькие латинские буквы плюс цифры.
Сжимаемый файл разбивается на слова, слова сортируются по частоте их употребления (сначала — наиболее употребительные), им присваиваются номера, которые кодируются алфавитом в 62 символа. Далее происходит замена — в коде слова заменяются их номерами в шестидятитид-вухричной системе. На клиенте осуществляется обратная замена.
Этот алгоритм работает достаточно быстро для того, чтобы накладные расходы на распаковку скрадывались улучшенным временем загрузки.