Подтвердить что ты не робот

Как убедиться, что сигналы 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 байт данных. Вот что происходит:

новое соединение!
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 или я что-то не хватает?

4b9b3361

Ответ 1

Короткий ответ

Документация QIODevice::readyRead гласит:

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

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


Фон

Сигнал readyRead передается QAbstractSocketPrivate::emitReadyRead следующим образом:

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

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

Чтобы этого не случилось, убедитесь, что вы

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

Ответ 2

Я думаю, что сценарий, упомянутый в этом разделе, имеет два основных случая, которые работают по-разному, но в целом 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 или ОС.

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

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

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

Ответ 3

Вот несколько примеров способов получения всего файла, но с использованием некоторых других частей 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

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

Ответ 4

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

В моей программе использование 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.

Ответ 5

Если a QProgressDialog должен отображаться при приеме данных из сокета, он работает только при отправке любого QApplication::processEvents() (например, методом QProgessDialog::setValue(int)). Это, конечно, приводит к потере сигналов 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 не пропущен и снова запустится.

Ответ 6

У меня была такая же проблема сразу с слотом 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;
    }
}