9

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?

sashoalm
  • 75,001
  • 122
  • 434
  • 781

4 Answers4

17

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);
     }
}
Marek R
  • 32,568
  • 6
  • 55
  • 140
  • Thanks, this seems to be the right way. I'm reworking the code to make a class `Block`, that automates the writing of the size of the block in its constructor/destructor, and will post the code shortly. – sashoalm Oct 30 '13 at 13:03
  • OK, I finished writing the classes, I've posted it as an answer. – sashoalm Oct 30 '13 at 14:54
  • Is the `while(true)` loop necessary ? – Fareanor Jul 07 '23 at 06:12
  • Moreover I think `clientReadStream >> str;` can put into `str` more that the block size (for example if the socket received another block in the meanwhile). It may be useful to read exactly the block size number of bytes instead. – Fareanor Jul 07 '23 at 06:45
12

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;
};
sashoalm
  • 75,001
  • 122
  • 434
  • 781
  • I like that, though I'd probably overload the stream operators of the classes, rather than return the internal _stream to the user, just to make it a little more elegant in its use. – TheDarkKnight Nov 29 '13 at 15:36
  • Can this by chance be used to transmit LARGE files? – Nicholas Johnson Jul 04 '17 at 19:18
  • 1
    @Nicholas I'd think so. As long as you split your large file(s) up into blocks. But all at once... probably not. – Maxwell175 Jan 27 '18 at 06:21
  • For this code to work properly you have to add in BlockReader just before "while (buffer.size() < n)": n = n+ buffer.size(); – M. Di Oct 01 '22 at 11:03
3

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.

TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85
  • 1
    Yes, but I can't know how many bytes I need. Suppose I have serialized a QString, how do I know how many bytes I need available? How long is the QString? QDataStream takes care of those details. This approach defeats the purpose of using QDataStream in the first place. Btw I had a paragraph explaining why it's not a good solution in my question, but decided to remove it to keep my question brief - I don't like to enumerate all the ideas that are not good. – sashoalm Oct 30 '13 at 12:13
  • @MarekR beat me to it; prepending the size of the packet and checking for the size on the receiving socket is definitely the right way to go. – TheDarkKnight Oct 30 '13 at 13:16
0

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.

crtxcr
  • 1