Дневник Шестеро Михаила - April 24th, 2014

Apr. 24th, 2014

06:21 am - Сжатие потока информации PHP-HTTP-Qt

Это второе дополнение к моей предыдущей статье «Шифровка потока информации PHP-HTTP-Qt». Для того что бы разобраться с тем что здесь написано, следует сначала ознакомиться с ней.

Вопрос о возможности их сжатия потока естественно возникает в связи с постоянной передачей больших объёмов разряженных данных (в формате XML) множеству клиентов. Тем более что у конечных пользователей программ-клиантов может быть и не быстрый доступ в Интернет, и сжатие в этом случае может дать существенный выйгрыш в скорости работы.
Я делаю сжатие/разжатие сразу в паре с шифровкой/дешифровкой и так же в связи с шифровкой о нём сейчас рассказываю, тем более что это делается и на стороне сервера и на стороне клиента подобным образом. Но вы, естественно, можете попробовать сделать сжатие/разжатие и без шифровки.
Для сдатия в PHP я использовал всё поточный фильтр zlib.deflate. На стороне клиента я применил класс QtIOCompressor из подзаброшенной ныне коллекции-библиотеки Qt Solutions.
Что бы XML лучше ужался, конечно, надо сжать его до шифровки, а не после.
В моём коде сжатие происходит опционально, в зависимости от значения переменной $compress в PHP и .. в коде клиента C++.

Итак, установка сжимающего фильтра на PHP (естественно, соответствующий фильтр должен быть доступен; проверка: phpinfo() ) выглядит так:
if ($compress=="RawZip")
{
  
$compress_alg "zlib.deflate"
}
else
{
  
$compress_alg false
}
if (
$compress_alg)
{
  
stream_filter_append($f$compress_algSTREAM_FILTER_WRITE);
}

В исходнике в статье про шифрование место, где фильт вставляется у меня помечено комментарием: ...[*]....

В программе клиенте поток-декомпрессор вставляется так:

    if (use_compression()) // if use decompressor
    {   // the input stream is compressed
        m_decompressor = new QtIOCompressor(m_decrypter); // QtIOCompressor *m_decompressor;
        m_decompressor->setStreamFormat(QtIOCompressor::RawZipFormat);
        m_decompressor->open(QIODevice::ReadOnly);

        m_xmlsource = new QXmlInputSource(m_decompressor);
    }
    else
    {   // the input stream isn't compressed
        m_xmlsource = new QXmlInputSource(m_decrypter);
    }
(Соответствующее место в исходнике я помечетил комменарием // [**]).

Таким образом информация проходит всю цепочку из таблицы MySQL через MySQLi — PHP — сжатие — шифровку — сеть (HTTP) — расшифровку — распаковку — SAX-парсер до GUI-виджета таблицы конвеерно!
Слоты для «массажа» района дешифратора и распаковщика в Qt этой цепочки таковы:

void WorkplaceTab::onReadyRead()
{
    if ( m_reader==NULL || m_xmlsource==NULL )
    {
        qDebug() << "WorkplaceTab::onReadyRead() - warning";
        return;
    }

    if (m_first)
    {
        // выполняется один раз - при приёме превой порции данных
        qDebug() << "m_reader->parse(m_xmlsource, true);";
        if (!m_reader->parse(m_xmlsource, true))
            to_idle_state(); // прекращение режима приёма
        m_first = false;
    }

    long watchdog = 100;
    while ( m_decrypter!=NULL &&
           (m_decompressor!=NULL?
                m_decompressor->bytesAvailable():
                m_decrypter->bytesAvailable())>0
         )
    {
        m_xmlsource->fetchData();
        if (!m_reader->parseContinue())
            to_idle_state();    // прекращение режима приёма

        if (watchdog--<0)
            break;
    }
}

void WorkplaceTab::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    logmessage( tr("%1 bytes and %2 rows loaded").arg(bytesReceived).arg(m_row) );

    if (bytesReceived==bytesTotal)
    {
        // выполняется один раз - после приёма последней порции данных
        qDebug() << "WorkplaceTab::onDownloadProgress LAST TIME";
        if (m_reader==NULL || m_xmlsource==NULL || m_reply==NULL)
        {
            qDebug() << "Warning: m_reader==NULL or m_xmlsource==NULL or m_reply==NULL!";
            return;
        }

        long watchdog=1000000;
        bool b=true;
        while ( b && m_xmlsource->data().length()>0 && --watchdog )
        {
            b = m_reader->parseContinue();
        }

        if (b)
        {
            m_xmlsource->next();        // и ещё раз - вхолостую -
            m_reader->parseContinue();  // что бы вызвалось QXmlInputSource::EndOfDocument
        }
    }
}

Согласитесь, это круто. Но, увы,
ложка дёгтя: если с шифрованием-расшифровкой всё идеально (уже прошло многомесячную интенсивную проверку обкатку), то с копрессия-декомпрессия очень редко, но, увы, всё же даёт сбой — в XML-е образуется мусорный участочек, парсер сигнализирует об ошибке... Происходит, правда, это редко, под нагрузкой и никогда — в начале (на маленьких данных не проявляется, обычно где то с 2000 строки), но всё же происходит... Была замечена ситуация, когда сбой был при работе клиента Windows, но не было при работе в Linux, откомпилированного из того-же исходника (правда библиотеки ZLib там были из разных исходников). В то же время сбой имеет тенденцию проявляться в одном и том же месте одного и того же длинного XML.
Так что я эту функцию сжатия в рабочих дистрибутивах сейчас выключаю, но в ряде простых задач с небольшими блоками данных вполне может прокатить. Или можно например в случае ошибок делать перезапрос с отключённым сжатием.
В чём причина не знаю, виноват скорее всего QtIOCompressor — точнее его не совершенство. Например он в принципе не умеет верно оценивать количество байт, которые он готов отдать в текущий момент.
Есть гипотеза, что ошибку можно обойти, например, усовершенствовав «массаж» или вставив между QtIOCompressor и QDecrypter-ом промежуточный буффер, скажем на 1 Kb.

Есть также открытые вопросы: какой стандартной утилитой можно распаковать данные, cгенерированные фильтром zlib.deflate в PHP?
Как этот поток распаковать на Java? (пока не дошёл до этого)?

В общем ещё есть тут над чем поразбираться!

Tags: , , , , , ,
Current Mood: [mood icon] satisfied
Previous day (Calendar) Next day