Как убедиться, что сигналы readyRead() из QTcpSocket нельзя пропустить?

при использовании QTcpSocket чтобы получить данные, сигнал для использования readyRead(), который сигнализирует, что новые данные доступны. Однако, когда вы находитесь в соответствующей реализации слота для чтения данных, никаких дополнительных readyRead() будет испускаться. Это может иметь смысл, поскольку вы уже находитесь в функции, где вы читаете все доступные данные.

описание

однако предположим, что следующая реализация этого слот:

void readSocketData()
{
    datacounter += socket->readAll().length();
    qDebug() << datacounter;
}

что делать, если некоторые данные приходят после вызова readAll() но прежде чем покинуть гнездо? Что если это был последний пакет данных, отправленный в другое приложение (или, по крайней мере последнее время)? Никакой дополнительный сигнал не будет испущен, поэтому вы должны убедиться, что прочитали все данные самостоятельно.

один из способов минимизировать проблему (но не избежать ее полностью)

конечно, мы можем изменить слот такой:

void readSocketData()
{
    while(socket->bytesAvailable())
        datacounter += socket->readAll().length();
    qDebug() << datacounter;
}

однако, мы не решили проблему. По-прежнему возможно, что данные поступают сразу после socket->bytesAvailable()-check (и даже размещение/другой проверки в абсолютном конце функции не решает этого).

убедитесь, что вы можете воспроизвести проблему

поскольку эта проблема, конечно, случается очень редко, я придерживаюсь первой реализации слота, и я даже добавлю искусственный тайм-аут, чтобы быть уверенным, что проблема возникает:

void readSocketData()
{
    datacounter += socket->readAll().length();
    qDebug() << datacounter;

    // wait, to make sure that some data arrived
    QEventLoop loop;
    QTimer::singleShot(1000, &loop, SLOT(quit()));
    loop.exec();
}

я тогда давай другое приложение отправляет 100 000 байт данных. Вот что происходит:--17-->

новое подключение!
32768 (или 16K или 48K)

первая часть сообщения читается, но конец больше не читается, как readyRead() больше не будет вызываться.

мой вопрос: каков наилучший способ убедиться, что эта проблема никогда не возникает?

возможное решение

одно решение, которое я придумал, снова вызывает тот же слот в конце снова, и проверить в начале слота, если есть еще какие-либо данные для чтения:

void readSocketData(bool selfCall) // default parameter selfCall=false in .h
{
    if (selfCall && !socket->bytesAvailable())
        return;

    datacounter += socket->readAll().length();
    qDebug() << datacounter;

    QEventLoop loop;
    QTimer::singleShot(1000, &loop, SLOT(quit()));
    loop.exec();

    QTimer::singleShot(0, this, SLOT(readSocketDataSelfCall()));
}

void readSocketDataSelfCall()
{
    readSocketData(true);
}

поскольку я не вызываю слот напрямую, но использую QTimer::singleShot(), Я предполагаю, что QTcpSocket не могу знать, что я снова вызываю слот, так что проблема в том, что readyRead() не испускается больше не может произойти.

причина, по которой я включил параметр bool selfCall это слот, который называется QTcpSocket не разрешено выходить раньше, иначе та же проблема может повторите, что данные поступают точно в неправильный момент и readyRead() не испущенная это.

это действительно лучшее решение для решения моей проблемы? Является ли существование этой проблемы ошибкой дизайна в Qt или я что-то упускаю?

6 ответов


решение

С QIODevice:: readyRead documentation:

если вы повторно вводите цикл событий или вызываете waitForReadyRead () внутри слота подключенный к сигналу readyRead (), сигнал не будет повторно установлен.

так, просто убедитесь, что вы не вводите цикл событий или позвоните waitForReadyRead в вашем слоте, и проблема должны не будет. (См. ниже для дальнейшего подробности.)


фон

в рамках соответствующих QAbstractSocketPrivate:: canReadNotification реализации readyRead сигнал посылается следующим образом (см. номер строки 733):

if (!emittedReadyRead && hasData) {
    QScopedValueRollback<bool> r(emittedReadyRead);
    emittedReadyRead = true;
    emit q->readyRead();
}

можно посмотреть здесь emittedReadyRead логическая переменная сбрасывается, как только if блок выходит за рамки (выполняется QScopedValueRollback). Так что единственный шанс пропустить readyRead сигнал, когда поток управления достигает the

if (!emittedReadyRead && hasData) {

проверьте еще раз до текущего readyRead обработка сигнала завершена. И это должно быть возможно только в том случае, если вы

  • вызов waitForReadyRead внутри слот (так что не делайте этого),
  • сделайте любую обработку событий внутри вашего слота, например, создание экземпляра QEventLoop или позвонив QApplication::processEvents (так что не делайте этого),
  • используйте несколько потоков и не синхронизируйте доступ к одному и тому же QTcpSocket правильно объект (так, конечно, также не делайте этого)

Я думаю, что сценарий, упомянутый в этой теме, имеет два основных случая, которые работают по-разному, но в целом QT вообще не имеет этой проблемы, и я попытаюсь объяснить ниже, почему.

первый случай: однопоточного приложения.

Qt использует системный вызов select () для опроса открытого файлового дескриптора для любых изменений или доступных операций. Простое высказывание на каждом цикле Qt проверяет, есть ли у любого из открытых файловых дескрипторов данные для чтения / закрытия и т. д. Так что наедине поток резьбовых приложений выглядит так (упрощенная часть кода)

int mainLoop(...) {
     select(...);
     foreach( descriptor which has new data available ) {
         find appropriate handler
         emit readyRead; 
     }
}

void slotReadyRead() {
     some code;
}

Итак, что произойдет, если новые данные поступят, пока программа все еще внутри slotReadyRead.. честно говоря, ничего особенного. ОС будет буферизировать данные, и как только управление вернется к следующему выполнению select (), ОС уведомит программное обеспечение о наличии данных для конкретного дескриптора файла. Он работает абсолютно одинаково для сокетов/файлов TCP и т. д.

Я могу визуализировать ситуации, когда (в случае действительно длинные задержки в slotReadyRead и много данных) вы можете испытать переполнение в буферах OS FIFO (например, для последовательных портов), но это больше связано с плохим дизайном программного обеспечения, а не с проблемами QT или OS.

вы должны смотреть на слоты, такие как readyRead, как на обработчиках прерываний и сохранять свою логику только в функциональности выборки, которая заполняет ваши внутренние буферы, в то время как обработка должна выполняться в отдельных потоках или во время приложения на холостом ходу и т. д.. Причина в том, что любое такое приложение в целом является системой массового обслуживания, и если оно тратит больше времени на обслуживание одного запроса, то временной интервал между двумя запросами все равно будет переполнен.

второй сценарий: многопоточное приложение

на самом деле этот сценарий не так сильно отличается от 1) ожидайте, что вы должны правильно спроектировать, что происходит в каждом из ваших потоков. Если вы держите основной цикл с легкими "псевдо-обработчиками прерываний", вы будете абсолютно в порядке и сохраните обработка логики в других потоках, но эта логика должна работать с вашими собственными буферами предварительной выборки, а не с QIODevice.


вот несколько примеров способов получить весь файл, но с использованием некоторых других частей API QNetwork:

http://qt-project.org/doc/qt-4.8/network-downloadmanager.html

http://qt-project.org/doc/qt-4.8/network-download.html

эти примеры показывают более сильный способ обработки данных TCP, и когда буферы заполнены, и лучшую обработку ошибок с api более высокого уровня.

Если вы все еще хотите использовать API нижнего уровня, вот сообщение с отличным способом обработки буферов:

внутри readSocketData() сделать что-то вроде этого:

if (bytesAvailable() < 256)
    return;
QByteArray data = read(256);

http://www.qtcentre.org/threads/11494-QTcpSocket-readyRead-and-buffer-size

EDIT: дополнительные примеры взаимодействия с QTCPSockets:

http://qt-project.org/doc/qt-4.8/network-fortuneserver.html

http://qt-project.org/doc/qt-4.8/network-fortuneclient.html

http://qt-project.org/doc/qt-4.8/network-blockingfortuneclient.html

надеюсь, что это поможет.


проблема довольно интересная.

в моей программе использование QTcpSocket очень интенсивно. Поэтому я написал всю библиотеку, которая разбивает исходящие данные на пакеты с заголовком, идентификатором данных, номером индекса пакета и максимальным размером, и когда приходит следующий фрагмент данных, я точно знаю, где он принадлежит. Даже если я что-то упущу, когда следующий readyRead приходит, приемник читает все и составляет полученные данные правильно. Если связь между вашими программами не так интенсивно, вы могли бы сделать то же самое, но с таймером (который не очень быстрый, но решает проблему.)

о своем решении. Я не думаю, что это лучше, чем это:

void readSocketData()
{
    while(socket->bytesAvailable())
    {
        datacounter += socket->readAll().length();
        qDebug() << datacounter;

        QEventLoop loop;
        QTimer::singleShot(1000, &loop, SLOT(quit()));
        loop.exec();
    }
}

проблема обоих методов-это код сразу после выхода из слота, но перед возвращением от испускания сигнала.

также вы можете связаться с Qt::QueuedConnection.


если QProgressDialog указываются при получении данных из сокета он работает только если QApplication::processEvents() отправлено (например,QProgessDialog::setValue(int) methode). Это, конечно, приводит к потере readyRead сигналы, как упоминалось выше.

таким образом, мой обходной путь был циклом while, включая команду processEvents, такую как:

void slot_readSocketData() {
    while (m_pSocket->bytesAvailable()) {
        m_sReceived.append(m_pSocket->readAll());
        m_pProgessDialog->setValue(++m_iCnt);
    }//while
}//slot_readSocketData

если слот вызывается один раз любой дополнительный readyRead сигналы можно игнорировать, потому что bytesAvailable() всегда возвращает фактическое число после processEvents звонок. Только при приостановке потока цикл while заканчивается. Но затем следующий readReady не пропущено и начинает его снова.


У меня была та же проблема сразу с слотом readyRead. Я не согласен с принятым ответом; это не решает проблему. Использование bytesAvailable, как описал Amartel, было единственным надежным решением, которое я нашел. Qt:: QueuedConnection не имел никакого эффекта. В следующем примере я десериализую пользовательский тип, поэтому легко предсказать минимальный размер байта. Он никогда не пропускает данные.

void MyFunExample::readyRead()
{
    bool done = false;

    while (!done)
    {

        in_.startTransaction();

        DataLinkListStruct st;

        in_ >> st;

        if (!in_.commitTransaction())
            qDebug() << "Failed to commit transaction.";

        switch (st.type)
        {
        case  DataLinkXmitType::Matrix:

            for ( int i=0;i<st.numLists;++i)
            {
                for ( auto it=st.data[i].begin();it!=st.data[i].end();++it )
                {
                    qDebug() << (*it).toString();
                }
            }
            break;

        case DataLinkXmitType::SingleValue:

            qDebug() << st.value.toString();
            break;

        case DataLinkXmitType::Map:

            for (auto it=st.mapData.begin();it!=st.mapData.end();++it)
            {
                qDebug() << it.key() << " == " << it.value().toString();
            }
            break;
        }

        if ( client_->QIODevice::bytesAvailable() < sizeof(DataLinkListStruct) )
            done = true;
    }
}