За последние 24 часа нас посетили 22183 программиста и 1077 роботов. Сейчас ищет 671 программист ...

Фоновый GZip-компрессор потока (служба, класс, буферизация)

Тема в разделе "Решения, алгоритмы", создана пользователем Pran, 4 мар 2011.

?

Оцените решение?

Голосование закрыто 13 июн 2011.
  1. Отлично

    0 голосов
    0,0%
  2. Неплохо

    0 голосов
    0,0%
  3. Ужасно

    0 голосов
    0,0%
  1. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Ранее использовал простенькую конструкцию следующего вида:

    PHP:
    1.  
    2. <?php
    3.  
    4.     // Использовать сжатие только в том случае, если оно поддерживается
    5.     // браузером клиента.
    6.     //
    7.     if(strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false) {
    8.  
    9.         function ki_compress($data) {
    10.  
    11.             // Максимальная степень сжатия.
    12.             //
    13.             return gzencode($data, 9);
    14.         }
    15.  
    16.         ob_start('ki_compress');
    17.         header('Content-encoding: gzip');
    18.     }
    19.     else {
    20.  
    21.         ob_start();
    22.     }
    23.  
    24. ?>
    25.  
    Основной недостаток данного подхода заключается в том, что заголовок Content-Encoding отсылается в момент старта и в случае отмены буферизации искажает восприятие потока клиентом. Необходимо решение, которое способно отследить момент непосредственной передачи сжатого содержимого.

    Если рассмотреть ситуацию применительно к ООП, то здесь всё решается довольно красиво:
    Предположим, что определена функция автозагрузки классов и существует некоторый класс Compressor. Допустим, что существует некоторый сценарий runtime.php, который подключается директивой php.ini к каждому вызванному на выполнение конечному сценарию: auto_prepend_file = 'runtime.php';

    В этом сценарии определена точка запуска для сервиса void Compressor::start(void). Для отключения буфера и компрессора достаточно произвести в конечном сценарии вызов void Compressor::stop(bool $flush = false);

    Примечание:
    в случае, если необходимо отправить данные до завершения сценария, следует использовать вызов Compressor::stop(true)


    runtime.php:
    PHP:
    1.  
    2. <?php
    3.  
    4.     Compressor::start(); # Фоновая служба сжатия вывода.
    5.     Session::start(); # Фоновая служба защиты от перехвата сеанса.
    6.  
    7. ?>
    8.  
    Реализация класса

    Пусть существует скрытый синглтон компрессора, который может уничтожить только среда CLR в конце жизненного цикла страницы. В момент высвобождения ресурсов вызывается метод класса __destruct, который мы используем для отправки нашего заголовка Content-Encoding - ведь доподлинно известно, что дальше последней строчки кода конструкций echo быть не может.

    PHP:
    1.  
    2. <?php
    3.  
    4.     // Copyright © Pran via PHP.ru, 2011
    5.     // Free for commercial and private usage.
    6.     //
    7.     class Compressor {
    8.  
    9.         private static $supported; # Сжатое содержимое поддерживается клиентом.
    10.         private static $enabled; # Буферизация включена для очередной порции данных.
    11.  
    12.         protected function __construct() {
    13.  
    14.             if (self::$supported = (strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false)) {
    15.  
    16.                 ob_start(array($this, 'compress'));
    17.             }
    18.             else {
    19.  
    20.                 ob_start();
    21.             }
    22.  
    23.             self::$enabled = true;
    24.         }
    25.  
    26.         final protected function __clone() {
    27.  
    28.             trigger_error('Clonning is not allowed in class ' . get_called_class(), E_USER_WARNING);
    29.         }
    30.  
    31.         final public static function compress($data) {
    32.  
    33.             return gzencode($data, 9);
    34.         }
    35.  
    36.         final public static function start() {
    37.  
    38.             static $instance;
    39.  
    40.             if (!$instance) {
    41.  
    42.                 $instance = new self();
    43.             }
    44.             else trigger_error('The compressor has been started earlier', E_USER_NOTICE);
    45.         }
    46.  
    47.         final protected static function flush() {
    48.  
    49.             // Если сжатие поддерживается клиентом,
    50.             // то будет отправлен соответствующий заголовок.
    51.             //
    52.             if (self::$supported) {
    53.  
    54.                 header('Content-Encoding: gzip');
    55.             }
    56.  
    57.             ob_end_flush();
    58.         }
    59.  
    60.         final public static function stop($flush = false) {
    61.  
    62.             // Предотвращает сбой при многократном вызове метода.
    63.             //
    64.             if (self::$enabled) {
    65.  
    66.                 if(!$flush) {
    67.  
    68.                     ob_end_clean();
    69.                 }
    70.                 else {
    71.  
    72.                     self::flush();
    73.                 }
    74.  
    75.                 self::$enabled = false;
    76.             }
    77.         }
    78.  
    79.         final public function __destruct() {
    80.  
    81.             if (self::$enabled) self::flush();
    82.         }
    83.     }
    84.  
    85. ?>
    86.  
    Применение в проекте
    Автоматически, ко всем сценариям. Сервис загрузки файлов попадает под исключение - при отдаче из базы (особенно при размере 100-200 МБ и более) компрессор отключается методом Compressor::stop, иначе либо выделенная оперативная память закончится, либо размер секции на выходе не будет соответствовать заголовку Content-Length.

    Проверка (test.php):
    PHP:
    1.  
    2. <?php
    3.  
    4.     // Защита сценария сработает только если start был вызван после stop, причём повторно.
    5.     // Два и более вызовов подряд не имеют принципиального значения.
    6.     //
    7.     Compressor::start();
    8.  
    9.     echo 'section one - compressed and/or buffered';
    10.  
    11.     // Очистить первую секцию.
    12.     // Эффект будет только в том случае, если stop был вызван после start.
    13.     // Два и более вызовов подряд не имеют принципиального значения.
    14.     //
    15.     Compressor::stop(); # true - отправить первую секцию клиенту.
    16.  
    17.     echo 'section two - plain text';
    18.  
    19. ?>
    20.  
     
  2. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Обнаружил неувязку - если возникает какая-либо ошибка, то метод __destruct не вызывается в конце выполнения и, в итоге, сжатое содержимое (в том числе и описание сбоя) отправляется без заголовка Content-Encoding.
     
  3. Koc

    Koc Активный пользователь

    С нами с:
    3 мар 2008
    Сообщения:
    2.253
    Симпатии:
    0
    Адрес:
    \Ukraine\Dnepropetrovsk
    хе-х. Не знал. Ну register_shutdown_function в помощь
     
  4. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Модификация, использующая register_shutdown_function, успешно работает под IIS (PHP через FastCGI). Буду признателен за проведение тестов под Apache.

    PHP:
    1.  
    2. <?php
    3.  
    4.     // Copyright © Pran via PHP.ru, 2011
    5.     // Free for commercial and private usage.
    6.     //
    7.     class Compressor {
    8.  
    9.         private static $supported; # Сжатое содержимое поддерживается клиентом.
    10.         private static $enabled; # Буферизация включена для очередной порции данных.
    11.  
    12.         protected function __construct() {
    13.  
    14.             if (self::$supported = (strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false)) {
    15.  
    16.                 ob_start(array(__CLASS__, 'compress'));
    17.  
    18.                 // Функция финализации будет поставлена в очередь выполнения только в том случае,
    19.                 // если клиент поддерживает сжатое содержимое.
    20.                 //
    21.                 register_shutdown_function(array(__CLASS__, 'finalize'));
    22.             }
    23.             else {
    24.  
    25.                 ob_start();
    26.             }
    27.  
    28.             self::$enabled = true;
    29.         }
    30.  
    31.         final protected function __clone() {
    32.  
    33.             trigger_error('Clonning is not allowed in class ' . get_called_class(), E_USER_WARNING);
    34.         }
    35.  
    36.         final public static function compress($data) {
    37.  
    38.             return gzencode($data, 9);
    39.         }
    40.  
    41.         final public static function start() {
    42.  
    43.             static $instance;
    44.  
    45.             if (!$instance) {
    46.  
    47.                 $instance = new self();
    48.             }
    49.             else trigger_error('The compressor has been started earlier', E_USER_NOTICE);
    50.         }
    51.  
    52.         final protected static function flush() {
    53.  
    54.             // Если сжатие поддерживается клиентом,
    55.             // то будет отправлен соответствующий заголовок.
    56.             //
    57.             if (self::$supported) {
    58.  
    59.                 header('Content-Encoding: gzip');
    60.             }
    61.  
    62.             ob_end_flush();
    63.         }
    64.  
    65.         final public static function stop($flush = false) {
    66.  
    67.             // Предотвращает сбой при многократном вызове метода.
    68.             //
    69.             if (self::$enabled) {
    70.  
    71.                 if(!$flush) {
    72.  
    73.                     ob_end_clean();
    74.                 }
    75.                 else {
    76.  
    77.                     self::flush();
    78.                 }
    79.  
    80.                 self::$enabled = false;
    81.             }
    82.         }
    83.  
    84.         public static function finalize() {
    85.  
    86.             self::stop(true);
    87.         }
    88.     }
    89.  
    90. ?>
    91.  
     
  5. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Замена классу Compressor, которую следует расположить в начале сценария (в моей практике - auto_prepend_file):

    PHP:
    1.  
    2. <?php
    3.  
    4.     if (strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false)
    5.     {
    6.         ob_start(function($data) {
    7.  
    8.             if ($data)
    9.             {
    10.                 header('Content-Encoding: gzip');
    11.                 return gzencode($data, 9);
    12.             }
    13.         });
    14.     }
    15.     else
    16.     {
    17.         ob_start();
    18.     }
    19.  
    20.     # При необходимости останавливаем через вызов ob_end_clean
    21.  
    22. ?>
    23.  
    ob_end_clean загадочным образом вызывает обработчик ob_start, передавая ему пустое содержимое (выявлено в PHP 5.3), поэтому введено условие проверки $data.

    В процессе работы не следует забывать про уровни вложения ob_start:

    PHP:
    1.  
    2. <?php
    3.  
    4.     ob_start(function(...)) # Блок из примера выше, сжатие на уровне 2
    5.  
    6.         ob_start(); # Уровень 3, несжатый «карман»
    7.         require_once ...
    8.         $tmp = ob_get_clean(); # Завершение уровня 3, вывод echo в переменную
    9.  
    10.  
    11.         ob_start(); # Уровень 3, несжатый «карман»
    12.         ob_end_clean(); # Завершение уровня 3
    13.  
    14.  
    15.     // ob_end_clean(); # Отмена сжатия
    16.  
    17. ?>
    18.  
    Отменить сжатие можно только в том случае, если ob_end_clean вызван для уровня 2