如何确保来自QTcpSocket的readyRead()信号不能错过?

Mis*_*sch 28 c++ qt qtcpsocket

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

问题描述

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

void readSocketData()
{
    datacounter += socket->readAll().length();
    qDebug() << datacounter;
}
Run Code Online (Sandbox Code Playgroud)

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

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

当然我们可以像这样修改插槽:

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

但是,我们还没有解决问题.数据仍然可能在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();
}
Run Code Online (Sandbox Code Playgroud)

然后我让另一个应用程序发送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);
}
Run Code Online (Sandbox Code Playgroud)

由于我没有直接调用插槽,而是使用QTimer::singleShot(),我认为QTcpSocket无法知道我再次调用插槽,因此readyRead()不会发生不发出的问题.

我加入参数的原因是不允许更快地退出bool selfCall被调用的插槽QTcpSocket,否则会再次出现同样的问题,数据准确地到达错误的时刻并且readyRead()不会被发出.

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

emk*_*y08 10

简短的回答

文档QIODevice::readyRead()状态:

如果您重新进入事件循环或readyRead()在连接到waitForReadyRead()信号的插槽内呼叫,则不会重新发送信号.

因此,请确保你

  • 不要readyRead()在你的插槽中实例化,
  • 不要QEventLoop在你的插槽内打电话,
  • 不要QApplication::processEvents()在你的插槽内打电话,
  • 不要QIODevice::waitForReadyRead()在不同的线程中使用相同的实例,

并且您应该始终接收另一方发送的所有数据.


背景

QTcpSocket信号由发射readyRead()如下:

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

一旦块超出范围(由完成),QAbstractSocketPrivate::emitReadyRead()变量就会回滚到该变量.因此,错过信号的唯一机会是控制流在最后一个信号的处理完成之前再次达到条件.emittedReadyReadfalseifQScopedValueRollbackreadyRead()if

这应该只能在上面列出的情况下实现.


evi*_*uff 6

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

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

Qt使用select()系统调用来轮询打开的文件描述符,以查找发生的任何更改或可用的操作.在每个循环上简单说一下Qt检查是否有任何打开的文件描述符有可用于读取/关闭等的数据.因此在单线程应用程序流上看起来就像那样(代码部分简化)

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

void slotReadyRead() {
     some code;
}
Run Code Online (Sandbox Code Playgroud)

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

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

您应该在中断处理程序上查看像readyRead这样的插槽,并将其逻辑仅保留在填充内部缓冲区的提取功能中,而处理应在单独的线程中完成,或者在空闲时应用程序等.原因是任何此类应用程序通常都是一个群发服务系统,如果它花费更多的时间来服务一个请求,那么它的队列之间的两个请求之间的时间间隔无论如何都会超支.

第二种情况:多线程应用程序

实际上,这种情况与1)预期你应该正确设计每个线程中发生的事情并没有多大区别.如果你保持主循环与轻度'伪中断处理程序'你将是绝对正常并在其他线程中保持处理逻辑,但这个逻辑应该与你自己的预取缓冲区,而不是QIODevice.