28

QTcpSocket用于接收数据时,使用的信号是readyRead(),表示有新数据可用。但是,当您在相应的插槽实现中读取数据时,不会readyRead()发出额外的数据。这可能是有道理的,因为您已经在函数中,您正在读取所有可用数据。

问题描述

但是,假设此插槽的以下实现:

void readSocketData()
{
    datacounter += socket->readAll().length();
    qDebug() << datacounter;
}

如果一些数据在调用之后readAll()但在离开插槽之前到达怎么办?如果这是其他应用程序发送的最后一个数据包(或者至少是一段时间内的最后一个)怎么办?不会发出额外的信号,因此您必须确保自己读取所有数据。

最小化问题的一种方法(但不能完全避免)

当然我们可以这样修改slot:

void readSocketData()
{
    while(socket->bytesAvailable())
        datacounter += socket->readAll().length();
    qDebug() << datacounter;
}

但是,我们还没有解决问题。数据仍然有可能在socket->bytesAvailable()-check 之后到达(甚至将/另一个检查放在函数的绝对末尾也不能解决这个问题)。

确保能够重现问题

由于这个问题当然很少发生,所以我坚持第一个实现插槽,我什至会添加一个人为的超时,以确保问题发生:

void readSocketData()
{
    datacounter += socket->readAll().length();
    qDebug() << datacounter;

    // wait, to make sure that some data arrived
    QEventLoop loop;
    QTimer::singleShot(1000, &loop, SLOT(quit()));
    loop.exec();
}

然后我让另一个应用程序发送 100,000 字节的数据。这就是发生的事情:

新连接!
32768(或 16K 或 48K)

消息的第一部分被读取,但结尾不再被读取,因为readyRead()不会再被调用。

我的问题是:确定这个问题永远不会发生的最好方法是什么?

可能的解决方案

我想出的一个解决方案是在最后再次调用同一个插槽,并在插槽的开头检查是否还有更多数据要读取:

void readSocketData(bool selfCall) // default parameter selfCall=false in .h
{
    if (selfCall && !socket->bytesAvailable())
        return;

    datacounter += socket->readAll().length();
    qDebug() << datacounter;

    QEventLoop loop;
    QTimer::singleShot(1000, &loop, SLOT(quit()));
    loop.exec();

    QTimer::singleShot(0, this, SLOT(readSocketDataSelfCall()));
}

void readSocketDataSelfCall()
{
    readSocketData(true);
}

由于我不直接调用插槽,而是使用QTimer::singleShot(),因此我假设QTcpSocket无法知道我正在再次调用插槽,因此readyRead()不会再发生未发出的问题。

我包含该参数的原因是不允许bool selfCall被调用的插槽更快退出,否则可能会再次出现相同的问题,即数据恰好在错误的时刻到达并且没有发出。QTcpSocketreadyRead()

这真的是解决我的问题的最佳解决方案吗?这个问题的存在是 Qt 中的设计错误还是我遗漏了什么?

4

6 回答 6

12

简短的回答

国家的文件QIODevice::readyRead()

readyRead()不是递归发出的;如果您重新进入事件循环或waitForReadyRead()在连接到readyRead()信号的插槽内调用,则不会重新发送信号。

因此,请确保您

  • 不要QEventLoop在你的插槽内实例化 a ,
  • 不要QApplication::processEvents()在你的插槽内调用,
  • 不要QIODevice::waitForReadyRead()在你的插槽内调用,
  • 不要QTcpSocket在不同的线程中使用相同的实例。

现在您应该始终收到对方发送的所有数据。


背景

readyRead()信号由以下方式发出QAbstractSocketPrivate::emitReadyRead()

// Only emit readyRead() when not recursing.
if (!emittedReadyRead && channel == currentReadChannel) {
    QScopedValueRollback<bool> r(emittedReadyRead);
    emittedReadyRead = true;
    emit q->readyRead();
}

一旦块超出范围(由 完成) ,emittedReadyRead变量就会回滚。所以错过信号的唯一机会是当控制流在最后一个信号的处理完成之前再次达到条件时(换句话说,当有递归时)。falseifQScopedValueRollbackreadyRead()ifreadyRead()

并且递归应该只在上面列出的情况下才有可能。

于 2016-03-23T14:08:53.630 回答
6

我认为本主题中提到的场景有两个不同的主要情况,但总的来说 QT 根本没有这个问题,我将在下面尝试解释原因。

第一种情况:单线程应用程序。

Qt 使用 select() 系统调用来轮询打开的文件描述符以了解发生的任何更改或可用的操作。在每个循环上简单地说 Qt 检查是否有任何打开的文件描述符具有可读取/关闭的数据等。所以在单线程应用程序流看起来像这样(代码部分简化)

int mainLoop(...) {
     select(...);
     foreach( descriptor which has new data available ) {
         find appropriate handler
         emit readyRead; 
     }
}

void slotReadyRead() {
     some code;
}

那么,如果新数据到达而程序仍在 slotReadyRead 中会发生什么......老实说没什么特别的。操作系统将缓冲数据,一旦控制权返回到 select() 的下一次执行,操作系统就会通知软件有可用于特定文件句柄的数据。它对 TCP 套接字/文件等的工作方式完全相同。

我可以想象以下情况(如果 slotReadyRead 延迟非常长并且有大量数据传入),您可能会在 OS FIFO 缓冲区(例如串行端口)中遇到溢出,但这更多地与糟糕的软件设计有关,而不是QT 或操作系统问题。

您应该在中断处理程序上查看像 readyRead 这样的插槽,并将它们的逻辑仅保留在填充您的内部缓冲区的获取功能中,而处理应该在单独的线程中或应用程序处于空闲状态等时完成。原因是任何这样的应用程序通常是一个大规模服务系统,如果它在服务一个请求上花费更多时间,那么两个请求之间的时间间隔无论如何它的队列都会溢出。

第二种场景:多线程应用

实际上,这种情况与 1) 并没有太大区别,期望您应该正确设计每个线程中发生的事情。如果您使用轻量级的“伪中断处理程序”来保持主循环,那么您将完全可以并在其他线程中保持处理逻辑,但是此逻辑应该与您自己的预取缓冲区一起使用,而不是与 QIODevice 一起使用。

于 2013-04-16T09:50:22.687 回答
1

这个问题很有趣。

在我的程序中,QTcpSocket 的使用非常密集。所以我编写了整个库,它将传出的数据分解成带有标题、数据标识符、包索引号和最大大小的包,当下一条数据到来时,我确切地知道它属于哪里。即使我错过了什么,当下一个readyRead到来时,接收器会读取所有内容并正确组合接收到的数据。如果您的程序之间的通信不是那么激烈,您可以这样做,但使用计时器(这不是很快,但可以解决问题。)

关于您的解决方案。我不认为这样更好:

void readSocketData()
{
    while(socket->bytesAvailable())
    {
        datacounter += socket->readAll().length();
        qDebug() << datacounter;

        QEventLoop loop;
        QTimer::singleShot(1000, &loop, SLOT(quit()));
        loop.exec();
    }
}

这两种方法的问题是代码在离开插槽之后,但在从发出信号返回之前。

您也可以与Qt::QueuedConnection.

于 2013-04-16T02:28:56.670 回答
0

以下是获取整个文件的一些示例,但使用了 QNetwork API 的其他部分:

http://qt-project.org/doc/qt-4.8/network-downloadmanager.html

http://qt-project.org/doc/qt-4.8/network-download.html

这些示例展示了一种处理 TCP 数据的更强大的方法,以及当缓冲区已满时,以及使用更高级别的 api 更好地处理错误。

如果您仍想使用较低级别的 api,这里有一个处理缓冲区的好方法的帖子:

在你里面readSocketData()做这样的事情:

if (bytesAvailable() < 256)
    return;
QByteArray data = read(256);

http://www.qtcentre.org/threads/11494-QTcpSocket-readyRead-and-buffer-size

编辑:如何与 QTCPSockets 交互的其他示例:

http://qt-project.org/doc/qt-4.8/network-fortuneserver.html

http://qt-project.org/doc/qt-4.8/network-fortuneclient.html

http://qt-project.org/doc/qt-4.8/network-blockingfortuneclient.html

希望有帮助。

于 2013-04-15T22:03:08.360 回答
0

如果在QProgressDialog从套接字接收数据时显示 a ,则它仅QApplication::processEvents()在发送任何数据时才有效(例如通过QProgessDialog::setValue(int)方法)。这当然会导致readyRead如上所述的信号丢失。

所以我的解决方法是一个包含 processEvents 命令的 while 循环,例如:

void slot_readSocketData() {
    while (m_pSocket->bytesAvailable()) {
        m_sReceived.append(m_pSocket->readAll());
        m_pProgessDialog->setValue(++m_iCnt);
    }//while
}//slot_readSocketData

如果插槽被调用一次,readyRead则可以忽略任何其他信号,因为调用bytesAvailable()后总是返回实际数字processEvents。只有在流暂停时,while 循环才会结束。但是接下来readReady不会错过并再次开始。

于 2017-11-07T11:19:04.650 回答
0

readyRead 插槽我马上遇到了同样的问题。我不同意接受的答案;它不能解决问题。使用 Amartel 描述的 bytesAvailable 是我找到的唯一可靠的解决方案。Qt::QueuedConnection 没有效果。在下面的示例中,我正在反序列化一个自定义类型,因此很容易预测最小字节大小。它永远不会丢失数据。

void MyFunExample::readyRead()
{
    bool done = false;

    while (!done)
    {

        in_.startTransaction();

        DataLinkListStruct st;

        in_ >> st;

        if (!in_.commitTransaction())
            qDebug() << "Failed to commit transaction.";

        switch (st.type)
        {
        case  DataLinkXmitType::Matrix:

            for ( int i=0;i<st.numLists;++i)
            {
                for ( auto it=st.data[i].begin();it!=st.data[i].end();++it )
                {
                    qDebug() << (*it).toString();
                }
            }
            break;

        case DataLinkXmitType::SingleValue:

            qDebug() << st.value.toString();
            break;

        case DataLinkXmitType::Map:

            for (auto it=st.mapData.begin();it!=st.mapData.end();++it)
            {
                qDebug() << it.key() << " == " << it.value().toString();
            }
            break;
        }

        if ( client_->QIODevice::bytesAvailable() < sizeof(DataLinkListStruct) )
            done = true;
    }
}   
于 2017-11-07T19:32:17.160 回答