使用QDataStream从QTcpSocket读取数据

9

我需要通过 QTcpSocket 发送二进制数据。我想使用 QDataStream,但是我遇到了一个问题 - 如果在读取数据的时候没有数据到达,它将不会发出任何声音。

比如说,如果我有这样的代码:

QString str;
stream >> str;

如果套接字中当前没有数据,它将会默默失败。有没有办法告诉它阻塞而不是失败?

4个回答

17

问题有点严重。Socket可以分块接收数据,所以即使您等待waitForReadyRead它也可能失败,因为没有足够的数据可以立即读取某个对象。
为解决此问题,您需要先发送数据大小,然后再发送实际数据。 发送代码:

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

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

tcpSocket->write(block);

在接收端,您需要等待可用数据的大小达到要求。接收器代码看起来差不多是这样的:

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


根据文档,可以在不添加额外大小信息的情况下读取QString,因为传递给QDataStream的QString包含大小信息。可以像这样验证大小:

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

谢谢,这似乎是正确的方法。我正在重新编写代码,创建一个名为“Block”的类,它可以在构造函数/析构函数中自动化块大小的写入,并很快会发布代码。 - sashoalm
好的,我已经完成了类的编写,我已将其发布为答案。 - sashoalm
while(true)循环是否必要? - Fareanor
此外,我认为clientReadStream >> str;可能会将超过块大小的内容放入str中(例如,如果套接字在此期间接收到另一个块)。最好是确切地读取块大小的字节数。 - Fareanor

12

我重新修改了来自@Marek的想法的代码,并创建了两个类 - BlockReader和BlockWriter:

使用样例:

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

我喜欢这个想法,不过我可能会重载类的流操作符,而不是将内部的_stream返回给用户,只是为了让它在使用时更加优雅。 - TheDarkKnight
这个能否用来传输大文件? - Nicholas Johnson
1
@Nicholas 我认为是这样的。只要将大文件分成块,就可以了。但是一次性全部上传可能不行。 - Maxwell175
为了使这段代码正常工作,您必须在“while (buffer.size() < n)”之前添加BlockReader:n = n + buffer.size(); - M. Di

3
你可以调用 QTCPSocket::waitForReadyRead 函数,该函数将阻塞直到有数据可用,或者连接 readyRead() 信号并在调用您的槽时,从流中读取。

1
是的,但我不知道需要多少字节。假设我已经序列化了一个QString,我怎么知道有多少可用字节?QString有多长?QDataStream会处理这些细节。这种方法首先就破坏了使用QDataStream的目的。顺便说一下,我在问题中有一个解释为什么这不是一个好的解决方案的段落,但决定将其删除以保持问题简洁 - 我不喜欢列举所有不好的想法。 - sashoalm
@MarekR 比我先完成了; 在数据包前加上大小并在接收套接字上检查大小绝对是正确的方法。 - TheDarkKnight

0

自Qt 5.7起,QDataStream引入了读取事务的概念:https://doc.qt.io/qt-5/qdatastream.html#using-read-transactions

因此,QDataStream知道何时发生截断(由于没有足够的字节可用)。

解决上述问题的方法可能是:

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

不过,这只是阐述了这个概念。如果确信必要的数据很快就会到达,那么才应该使用这种相当幼稚的实现,否则这可能会导致无休止的循环。更健壮的检查可能还应考虑 QDataStream 后面的底层 QIODevice 的状态等因素。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接