Question

I need to send binary data through a QTcpSocket. I was thinking about using QDataStream, but I've encountered a problem - it silently fails if no data has arrived at the time I try to read.

For example if I have this code:

QString str;
stream >> str;

It will fail silently if no data is currently there in the socket. Is there a way to tell it to block instead?

Was it helpful?

Solution 2

I reworked the code from @Marek's idea and created 2 classes - BlockReader and BlockWriter:

Sample usage:

// Write block to the socket.
BlockWriter(socket).stream() << QDir("C:/Windows").entryList() << QString("Hello World!");

....

// Now read the block from the socket.
QStringList infoList;
QString s;
BlockReader(socket).stream() >> infoList >> s;
qDebug() << infoList << s;

BlockReader:

class BlockReader
{
public:
    BlockReader(QIODevice *io)
    {
        buffer.open(QIODevice::ReadWrite);
        _stream.setVersion(QDataStream::Qt_4_8);
        _stream.setDevice(&buffer);

        quint64 blockSize;

        // Read the size.
        readMax(io, sizeof(blockSize));
        buffer.seek(0);
        _stream >> blockSize;

        // Read the rest of the data.
        readMax(io, blockSize);
        buffer.seek(sizeof(blockSize));
    }

    QDataStream& stream()
    {
        return _stream;
    }

private:
    // Blocking reads data from socket until buffer size becomes exactly n. No
    // additional data is read from the socket.
    void readMax(QIODevice *io, int n)
    {
        while (buffer.size() < n) {
            if (!io->bytesAvailable()) {
                io->waitForReadyRead(30000);
            }
            buffer.write(io->read(n - buffer.size()));
        }
    }
    QBuffer buffer;
    QDataStream _stream;
};

BlockWriter:

class BlockWriter
{
public:
    BlockWriter(QIODevice *io)
    {
        buffer.open(QIODevice::WriteOnly);
        this->io = io;
        _stream.setVersion(QDataStream::Qt_4_8);
        _stream.setDevice(&buffer);

        // Placeholder for the size. We will get the value
        // at the end.
        _stream << quint64(0);
    }

    ~BlockWriter()
    {
        // Write the real size.
        _stream.device()->seek(0);
        _stream << (quint64) buffer.size();

        // Flush to the device.
        io->write(buffer.buffer());
    }

    QDataStream &stream()
    {
        return _stream;
    }

private:
    QBuffer buffer;
    QDataStream _stream;
    QIODevice *io;
};

OTHER TIPS

The problem is a bit more serious. Socket can receive data in chunks, so even if you will wait for waitForReadyRead it can fail since there is not enough data to immediately read some object.
To solve this problem you have to send a size of data first then actual data. Send code:

QByteArray block;
QDataStream sendStream(&block, QIODevice::ReadWrite);
sendStream << quint16(0) << str;

sendStream.device()->seek(0);
sendStream << (quint16)(block.size() - sizeof(quint16));

tcpSocket->write(block);

On receiver you have to wait until size of available data is meets requirement. Receiver code looks more or less like that:

void SomeClass::slotReadClient() { // slot connected to readyRead signal of QTcpSocket
    QTcpSocket *tcpSocket = (QTcpSocket*)sender();
    QDataStream clientReadStream(tcpSocket);

    while(true) {
        if (!next_block_size) {
            if (tcpSocket->bytesAvailable() < sizeof(quint16)) { // are size data available
                break;
            }
            clientReadStream >> next_block_size;
        }

        if (tcpSocket->bytesAvailable() < next_block_size) {
            break;
        }
        QString str;
        clientReadStream >> str;

        next_block_size = 0;
    }
}


small update, based on documentation it is possible to read QString without adding extra size information, since QString passed to QDataStream contains size information. Size can be verified like that:

void SomeClass::slotReadClient() { // slot connected to readyRead signal of QTcpSocket
    QTcpSocket *tcpSocket = (QTcpSocket*)sender();
    while(true) {
        if (tcpSocket->bytesAvailable() < 4) {
           break;
        }
        char buffer[4]
        quint32 peekedSize;
        tcpSocket->peek(buffer, 4);
        peekedSize = qFromBigEndian<quint32>(buffer); // default endian in QDataStream
        if (peekedSize==0xffffffffu) // null string
           peekedSize = 0;
        peekedSize += 4;
        if (tcpSocket->bytesAvailable() < peekedSize) {
           break;
        }
        // here all required for QString  data are available
        QString str;
        QDataStream(tcpSocket) >> str;
        emit stringHasBeenRead(str);
     }
}

You can call the QTCPSocket::waitForReadyRead function, which will block until data is available, or connect to the readyRead() signal and when your slot is called, then read from the stream.

QDataStream has gained the concept of read transactions since 5.7: https://doc.qt.io/qt-5/qdatastream.html#using-read-transactions

Therefore, QDataStream knows when there was a truncation (due to not enough bytes available).

A way to solve the above problem might be:

QString str;
do
{
    stream.startTransaction();
    stream >> str;
} while(!stream.commitTransaction());

However, this is only illustrates the concept. Such rather naive implementation should only be used if one is absolutely sure the necessary data will arrive shortly, otherwise this can cause an endless loop. A more robust check should probably also consider the state of the underlying QIODevice behind the QDataStream, etc.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top