Read from QTcpSocket using QDataStream

18,290

Solution 1

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

Solution 2

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

Solution 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.

Share:
18,290
sashoalm
Author by

sashoalm

Updated on July 21, 2022

Comments

  • sashoalm
    sashoalm almost 2 years

    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
    sashoalm over 10 years
    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
    sashoalm over 10 years
    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.
  • TheDarkKnight
    TheDarkKnight over 10 years
    @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.
  • sashoalm
    sashoalm over 10 years
    OK, I finished writing the classes, I've posted it as an answer.
  • TheDarkKnight
    TheDarkKnight over 10 years
    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.
  • Nicholas Johnson
    Nicholas Johnson almost 7 years
    Can this by chance be used to transmit LARGE files?
  • Maxwell175
    Maxwell175 over 6 years
    @Nicholas I'd think so. As long as you split your large file(s) up into blocks. But all at once... probably not.