c++ 如何确保QTcpSocket发出的readyRead()信号不会丢失?

yqkkidmi  于 2022-12-30  发布在  其他
关注(0)|答案(7)|浏览(343)

当使用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()-check之后到达(即使将/another 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);
}

由于我没有直接调用slot,而是使用QTimer::singleShot(),我假设QTcpSocket不知道我再次调用slot,所以readyRead()不发出的问题不会再发生。
我之所以包含参数bool selfCall,是因为QTcpSocket调用的插槽不允许很快退出,否则同样的问题可能再次发生,数据恰好在错误的时间到达,readyRead()不会发出。
这真的是解决我的问题的最好解决方案吗?这个问题的存在是Qt中的设计错误还是我遗漏了什么?

11dmarpk

11dmarpk1#

简短的回答

QIODevice::readyRead()的文档说明:
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块超出范围,emittedReadyRead变量就会回滚到false(由QScopedValueRollback完成)。因此,错过readyRead()信号的唯一机会是当控制流再次达到if条件时在最后的readyRead()信号的处理已经完成之前(换句话说,什么时候会有递归)。
并且递归应该只在上面列出的情况下才是可能的。

n1bvdmb6

n1bvdmb62#

我认为这个主题中提到的场景有两种主要的情况,它们的工作原理不同,但通常QT根本没有这个问题,我将在下面解释原因。
第一种情况:单线程应用程序。
Qt使用select()系统调用来轮询打开的文件描述符,以查看是否发生了任何更改或可用操作。简单地说,Qt在每个循环中都会检查打开的文件描述符是否有数据可供读取/关闭等。因此,单线程应用程序流程看起来像这样(代码部分简化)

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

void slotReadyRead() {
     some code;
}

那么,如果新的数据到达,而程序仍然在slotReadyRead ..老实说,没有什么特别的。操作系统将缓冲数据,并尽快控制将返回到下一次执行选择()操作系统将通知软件,有数据可用于特定的文件句柄。它的工作方式完全相同的TCP套接字/文件等。
我可以想象这样的情况(在slotReadyRead延迟很长且有大量数据到来的情况下),您可能会在操作系统FIFO缓冲区(例如串行端口)内遇到溢出,但这更多地与糟糕的软件设计有关,而不是QT或操作系统问题。
您应该像在中断处理程序上一样查看readyRead等插槽,并将它们的逻辑仅保留在获取功能中,该功能会填充内部缓冲区,而处理应该在单独的线程中完成,或者应用程序处于空闲状态等。原因是,任何此类应用程序通常都是一个大众服务系统,如果它在服务一个请求上花费更多时间,那么它的队列中两个请求之间的时间间隔无论如何都会溢出。
第二种情况:多线程应用
实际上,这种情况与1)期望你应该正确设计每个线程中发生的事情没有太大的不同。如果你用轻量级的"伪中断处理程序"来保持主循环,你绝对会很好,并在其他线程中保持处理逻辑,但这个逻辑应该与你自己的预取缓冲区而不是QIODevice一起工作。

cidc1ykv

cidc1ykv3#

这个问题很有趣。
在我的程序中,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连接。

ar7v8xwq

ar7v8xwq4#

以下是获取整个文件的一些方法示例,但使用了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
希望能有所帮助。

wkyowqbh

wkyowqbh5#

如果QProgressDialog在从套接字接收数据时显示,则只有在发送了任何QApplication::processEvents()(例如,通过QProgessDialog::setValue(int)方法)时才会工作。这当然会导致readyRead信号丢失,如上所述。
因此,我的解决方法是使用while循环,其中包含processEvents命令,例如:

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

如果slot被调用一次,任何额外的readyRead信号都可以被忽略,因为bytesAvailable()总是在processEvents调用后返回实际的数字。只有在流暂停时while循环才结束。但是下一个readReady不会丢失,并会再次启动它。

mxg2im7a

mxg2im7a6#

我在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;
    }
}
1aaf6o9v

1aaf6o9v7#

我遇到了同样的问题,宁愿使用信号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
{

相关问题