c++qtqtcpsocket

How to make sure that readyRead() signals from QTcpSocket can't be missed?


When using QTcpSocket to receive data, the signal to use is readyRead(), which signals that new data is available. However, when you are in the corresponding slot implementation to read the data, no additional readyRead() will be emitted. This may make sense, as you are already in the function, where you are reading all the data that is available.

Problem description

However assume the following implementation of this slot:

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

What if some data arrives after calling readAll() but before leaving the slot? What if this was the last data packet sent by the other application (or at least the last one for some time)? No additional signal will be emitted, so you have to make sure to read all the data yourself.

One way to minimize the problem (but not avoid it totally)

Of course we can modify the slot like this:

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

However, we haven't solved the problem. It is still possible that data arrives just after the socket->bytesAvailable()-check (and even placing the/another check at the absolute end of the function doesn't solve this).

Making sure to be able to reproduce the problem

As this problem of course happens very rarely, I stick to the first implementation of the slot, and I'll even add a an artificial timeout, to be sure that the problem occurs:

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();
}

I then let another application send 100,000 bytes of data. This is what happens:

new connection!
32768 (or 16K or 48K)

The first part of the message is read, but the end isn't read anymore, as readyRead() won't be called again.

My question is: what is the best way to be sure, this problem never occurs?

Possible solution

One solution I came up with is calling the same slot again at the end again, and to check at the beginning of the slot, if there is any more data to read:

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);
}

As I don't call the slot directly, but use QTimer::singleShot(), I assume that the QTcpSocket can't know that I'm calling the slot again, so the problem that readyRead() isn't emitted can't happen anymore.

The reason why I have included the parameter bool selfCall is that the slot which is called by the QTcpSocket isn't allowed to exit sooner, else the same problem can occur again, that data arrives exactly at the wrong moment and readyRead() isn't emitted.

Is this really the best solution to solve my problem? Is the existence of this problem a design error in Qt or am I missing something?


Solution

  • Short answer

    The documentation of QIODevice::readyRead() states:

    readyRead() is not emitted recursively; if you reenter the event loop or call waitForReadyRead() inside a slot connected to the readyRead() signal, the signal will not be reemitted.

    Thus, make sure that you

    Now you should always receive all data sent by the other side.


    Background

    The readyRead() signal is emitted by QAbstractSocketPrivate::emitReadyRead() as follows:

    // Only emit readyRead() when not recursing.
    if (!emittedReadyRead && channel == currentReadChannel) {
        QScopedValueRollback<bool> r(emittedReadyRead);
        emittedReadyRead = true;
        emit q->readyRead();
    }
    

    The emittedReadyRead variable is rolled back to false as soon as the if block goes out of scope (done by the QScopedValueRollback). So the only chance to miss a readyRead() signal is when the control flow reaches the if condition again before the processing of the last readyRead() signal has finished (in other words, when there would be a recursion).

    And a recursion should only be possible in the situations listed above.