【问题标题】:How to make sure that readyRead() signals from QTcpSocket can't be missed?如何确保不会错过来自 QTcpSocket 的 readyRead() 信号?
【发布时间】:2013-04-08 01:27:06
【问题描述】:

当使用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-&gt;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是因为QTcpSocket调用的槽不能早点退出,否则同样的问题可能再次出现,数据到达的时间正好是错误的并且readyRead() 不会发出。

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

【问题讨论】:

  • 很好的问题,描述得也很好。
  • 我认为当启动一个事件循环时,您允许 Qt 处理事件,例如从网络接口读取。如果你不这样做(试试 sleep 代替),这不应该发生。将QCoreApplication::processEvents() 放在一些较小的睡眠之间可能再次等于您的场景,因为 Qt 处理传入的数据,看到您当前位于连接到 readyRead() 的插槽中,因此它不会再次调用它,并且您的检查器再次错过。
  • 但是:问题可能会变成“当我可能允许 Qt 处理事件时如何正确处理这个问题?” (请记住,如果您说“好的,那么我只是不允许 Qt 处理事件,并且它已修复”,您可能有一天会重写您的插槽并忘记您不应该这样做。Boom。)
  • 是的,你是对的。但是我前段时间确实遇到了这个问题(没有任何人为延迟)。不幸的是,我不确定我最初是如何解决这个问题的,但这可能是由于不同的线程。使用多线程,这个问题还是会存在
  • 我希望一些 Qt 超级程序员能回答这个问题!我遇到了同样的问题,我不知道如何解决这个问题

标签: c++ qt qtcpsocket


【解决方案1】:

简答

QIODevice::readyRead()documentation 声明:

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

因此,请确保您

  • 不要在您的插槽内实例化 QEventLoop
  • 不要在你的槽内调用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();
}

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

只有在上面列出的情况下才能进行递归。

【讨论】:

    【解决方案2】:

    我认为本主题中提到的场景有两个不同的主要情况,但总的来说 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 一起使用。

    【讨论】:

      【解决方案3】:

      这个问题很有趣。

      在我的程序中,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

      【讨论】:

        【解决方案4】:

        以下是获取整个文件的一些示例,但使用 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

        希望对您有所帮助。

        【讨论】:

        • 嗯,您提供的示例使用QNetworkAccessManagerQNetworkRequestQNetworkReply。然而,Afaik 这些仅被认为与 HTTP 方法一起使用,而不是您想要的任何 TCP 协议。我将数据从一台设备传输到另一台设备的示例只是一个示例,我希望能够通过 TCP 发送任意数据,而不必只是将一个 blob 从一台设备传输到另一台设备。
        • 对于您提供的其他来源:这不能回答我的问题。在论坛中,您从作者那里得到了这段代码,出于某种原因,他知道他正在等待 256 字节的数据,并且他想等待读取,直到它完全到达。我想做相反的事情:确保数据永远不会(无意)留在 TCP 队列中。
        • 对该主题进行更多搜索,有两个示例说明如何以阻塞方式或异步方式使用QTCPSocket。异步方式,等待某个大小的数据包,然后处理它,就像我上面提到的那样。
        【解决方案5】:

        如果在从套接字接收数据时应显示QProgressDialog,则它仅在发送任何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并没有错过,又重新开始了。

        【讨论】:

        • 在这种情况下,您应该使用单独的后台工作线程来读取传入的数据。在与 GUI 无关的插槽中处理与 GUI 相关的事件通常是一个坏主意,尽管您的示例可能足够简单以实际工作。
        【解决方案6】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-03-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-06-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多