如何确保 QTcpSocket 的 readyRead() 信号不会被错过?

29

使用QTcpSocket接收数据时,请使用readyRead()信号,该信号表示新数据可用。 然而,在相应的槽函数实现中读取数据时,不会发出额外的readyRead()信号。 这可能是有意义的,因为您已经在函数中,读取了所有可用的数据。

问题描述

假设以下是该槽函数的实现:

void readSocketData()
{
    datacounter += socket->readAll().length();
    qDebug() << datacounter;
}
如果在调用readAll()之后但离开槽之前有一些数据到达怎么办?如果这是其他应用程序发送的最后一个数据包(或者至少在一段时间内的最后一个数据包)呢?不会发出任何额外的信号,因此您必须确保自己读取所有数据。 减少问题的一种方法(但不能完全避免)是修改槽:
void readSocketData()
{
    while(socket->bytesAvailable())
        datacounter += socket->readAll().length();
    qDebug() << datacounter;
}

然而,我们还没有解决这个问题。仍然有可能出现数据在socket->bytesAvailable()检查之后到达的情况(即使将检查放在函数的绝对末尾也无法解决这个问题)。

确保能够重现问题

由于这个问题很少出现,我会继续使用第一个槽函数实现,并添加人为超时,以确保问题发生:

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的设计错误还是我遗漏了什么?


非常好的问题,描述得非常清楚。 - leemes
1
我认为当启动事件循环时,您允许Qt处理事件,例如从网络接口读取。如果您不这样做(尝试使用sleep),则不应该发生这种情况。在一些较小的睡眠之间放置QCoreApplication :: processEvents()可能会再次等同于您的场景,因为Qt处理传入数据,看到您当前在与readyRead()连接的插槽中,因此它不会再次调用它,您的检查器再次被忽略。 - leemes
但是仍然有一个问题:这个问题可以转化为“当我可能允许Qt处理事件时,如何正确处理它?”(请记住,如果您说“好吧,那我就不允许Qt处理事件,问题就解决了”,您可能有一天会重写您的插槽并忘记您不应该这样做。砰!) - leemes
1
是的,你说得对。然而,我之前确实遇到过这个问题(没有任何人为延迟)。不幸的是,我现在不确定我当初是如何陷入这个问题的,但很可能是由于不同的线程。使用多线程,这个问题仍然会存在。 - Misch
1
希望有一位Qt超级程序员能够回答这个问题!我遇到了同样的问题,但是我无法解决它。 - TSG
7个回答

13

简短回答

QIODevice::readyRead()文档中说明:

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

因此,请确保您:

  • 不要在槽函数内实例化一个QEventLoop,
  • 不要在槽函数内调用QApplication::processEvents(),
  • 不要在槽函数内调用QIODevice::waitForReadyRead(),
  • 不要在不同的线程内使用同一个QTcpSocket实例。

现在您应该总是能够接收到对方发送的所有数据。


背景

通过QAbstractSocketPrivate::emitReadyRead()方法发出readyRead()信号,如下所示:

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

只有在最后一个readyRead()信号的处理完成之前,控制流再次到达if条件时(换句话说,存在递归时),emittedReadyRead变量才会在QScopedValueRollback执行之后回滚为false。因此,错过readyRead()信号的唯一可能性是当控制流再次到达if条件之前

而且,只有在上述情况下才可能发生递归。


这是一个很棒的答案。如果我在一个槽函数中调用了'write'函数,并将其连接到除'readReady()'信号以外的某个其他信号,我是否可以在此之后实例化QEventLoop? - Savner_Dig

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 sockets /文件等,它的工作方式完全相同。
我可以想象在slotReadyRead中存在真正长时间延迟和大量数据到来的情况下(例如串行端口),您可能会在操作系统FIFO缓冲区中遇到溢出,但这更多是与不良软件设计相关,而不是QT或操作系统问题。
您应该将诸如readyRead之类的插槽视为中断处理程序,并仅将它们的逻辑保留在填充内部缓冲区的获取功能中,而处理应该在单独的线程中进行,或者在应用程序空闲时进行等等。原因是任何此类应用程序通常都是一个大规模服务系统,如果它花费更多时间为一个请求提供服务,那么两个请求之间的时间间隔肯定会被超过。
第二种情况:多线程应用程序
实际上,这种情况与第1种情况并没有太大的区别,除非您应该正确地设计每个线程中发生的事情。如果您使用轻量级的“伪中断处理程序”保持主循环,那么您将完全没有问题,并且在其他线程中保留处理逻辑,但是该逻辑应该使用您自己的预取缓冲区而不是QIODevice。

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连接。

0
如果在从套接字接收数据时要显示一个 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信号都可以被忽略,因为processEvents调用后bytesAvailable()总是返回实际数量。只有在流暂停时,while循环才会结束。但是,下一个readReady不会被错过,并且会再次启动它。

在这种情况下,您应该使用单独的后台工作线程来读取传入的数据。在非GUI相关的插槽中处理GUI相关事件通常是一个不好的想法,尽管您的示例可能足够简单以实际工作。 - emkey08

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

0

我遇到了同样的问题,我更倾向于使用信号readyRead()和socket.readAll(),我正在尝试在连接后立即执行以下操作,但并不确定:

    QByteArray RBuff;
if(m_socket->waitForConnected(3000))
{
    while (m_socket->ConnectedState == QAbstractSocket::ConnectedState) {
        RBuff = m_socket->read(2048);
        SocketRead.append(RBuff);
        if (!SocketRead.isEmpty() && SocketRead.length() == 2048)
        {
            readData(SocketRead);
            SocketRead.remove(0,2048);
        }
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
    }

    //m_socket->close();*/
}
else
{

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

希望有所帮助。

好的,你提供的示例使用了 QNetworkAccessManagerQNetworkRequestQNetworkReply。据我所知,这些仅被认为是用于 HTTP 方法,而不是任何您想要使用的 TCP 协议。我从一个设备传输数据到另一个设备的示例只是一个示例,我想能够通过 TCP 发送任意数据,不一定只是从一个设备传输一个 blob 到另一个设备。 - Misch
对于您提供的另一个源代码:这并没有回答我的问题。在您获取此代码的论坛中,作者知道他正在等待256字节的数据,并且希望等待读取直到完全到达。我想做相反的事情:确保永远不会发生数据意外留在TCP队列中。 - Misch
在这个主题上再做一些搜索,有两个使用QTCPSocket的例子,一个是阻塞方式,另一个是异步方式。异步方式等待某个大小的数据包,然后像我上面提到的那样进行处理。 - phyatt

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