【问题标题】:Read from QTcpSocket using QDataStream使用 QDataStream 从 QTcpSocket 读取
【发布时间】:2013-11-09 23:39:44
【问题描述】:

我需要通过QTcpSocket 发送二进制数据。我正在考虑使用QDataStream,但我遇到了一个问题 - 如果在我尝试读取时没有数据到达,它会静默失败。

例如,如果我有这个代码:

QString str;
stream >> str;

如果当前套接字中没有数据,它将静默失败。有没有办法让它阻止?

【问题讨论】:

    标签: c++ qt qtcpsocket


    【解决方案1】:

    问题有点严重。套接字可以分块接收数据,因此即使您等待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;
        }
    }
    


    小更新,基于documentation,可以在不添加额外大小信息的情况下读取 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,它会自动在其构造函数/析构函数中写入块的大小,并将很快发布代码。
    • 好的,我写完了课程,我已经把它作为答案发布了。
    【解决方案2】:

    我根据@Marek 的想法重新编写了代码并创建了 2 个类 - 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;
    

    块读取器:

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

    块写入器:

    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 返回给用户,只是为了让它在使用中更加优雅。
    • 这个可以用来传输大文件吗?
    • @Nicholas 我想是的。只要您将大文件拆分为块。但一下子……可能不会。
    【解决方案3】:

    您可以调用 QTCPSocket::waitForReadyRead 函数,该函数将阻塞直到数据可用,或者连接到 readyRead() 信号并且当您的插槽被调用时,然后从流中读取。

    【讨论】:

    • 是的,但我不知道我需要多少字节。假设我已经序列化了一个 QString,我怎么知道我需要多少字节可用? QString 有多长? QDataStream 负责处理这些细节。这种方法首先破坏了使用 QDataStream 的目的。顺便说一句,我有一段解释为什么在我的问题中它不是一个好的解决方案,但决定删除它以保持我的问题简短 - 我不喜欢列举所有不好的想法。
    • @MarekR 打败了我;预先设置数据包的大小并检查接收套接字的大小绝对是正确的方法。