web-dev-qa-db-ja.com

QTcpSocketからのreadyRead()シグナルを見逃さないようにする方法は?

QTcpSocketを使用してデータを受信する場合、使用するシグナルはreadyRead()で、新しいデータが利用可能であることを示します。ただし、対応するスロット実装でデータを読み取る場合、追加のreadyRead()は発行されません。これは、すでに使用可能なすべてのデータを読み取っている関数内にいるため、理にかなっています。

問題の説明

ただし、このスロットの次の実装を想定します。

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

readAll()を呼び出した後、スロットを出る前にデータが到着した場合はどうなりますか?これが他のアプリケーションによって送信された最後のデータパケット(または少なくともしばらくの間最後のデータパケット)であった場合はどうなりますか?追加の信号は送信されないため、すべてのデータを自分で読み取る必要があります。

問題を最小限に抑える1つの方法(ただし、完全に回避するわけではありません)

もちろん、次のようにスロットを変更できます。

_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()が再度呼び出されないため、最後は読み取られません。

私の質問は、次のとおりです。この問題が発生しないことを確認する最善の方法は何ですか?

可能な解決策

私が思いついた1つの解決策は、最後にもう一度同じスロットを呼び出して、読み取るデータがまだあるかどうか、スロットの先頭で確認することです。

_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の設計エラーですか、それとも何か不足していますか?

28
Misch

簡潔な答え

documentation of QIODevice::readyRead()の状態:

readyRead()は再帰的に発行されません。イベントループに再び入るか、waitForReadyRead()信号に接続されたスロット内でreadyRead()を呼び出した場合、信号は再送信されません。

したがって、あなたが

  • しないでくださいスロット内でQEventLoopをインスタンス化し、
  • しないでくださいスロット内でQApplication::processEvents()を呼び出します。
  • しないでくださいスロット内でQIODevice::waitForReadyRead()を呼び出します。
  • 異なるスレッド内で同じQTcpSocketインスタンスを使用しないでください

これで、相手側から送信されたallデータを常に受信するはずです。


バックグラウンド

readyRead()シグナルは QAbstractSocketPrivate::emitReadyRead() によって次のように発行されます。

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

emittedReadyRead変数は、falseブロックがスコープから外れるとすぐにifにロールバックされます(QScopedValueRollbackによって実行されます)。したがって、readyRead()信号を見逃す唯一のチャンスは、制御フローが再びif条件に達したときbeforeです。最後のreadyRead()シグナルの処理が終了しました(つまり、再帰が発生した場合)。

そして、再帰は上記の状況でのみ可能です。

9
emkey08

このトピックで言及されているシナリオには、動作が異なる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内にある間に新しいデータが到着した場合、どうなるでしょうか。正直なところ、特別なことは何もありません。 OSはデータをバッファし、select()の次の実行に制御が戻るとすぐに、OSは特定のファイルハンドルに使用できるデータがあることをソフトウェアに通知します。 TCPソケット/ファイルなど)とまったく同じように機能します。

(slotReadyReadで非常に長い遅延が発生し、大量のデータが受信される場合)OS FIFO=バッファー(シリアルポートなど))内でオーバーランが発生する状況をイメージングできますが、 QTやOSの問題ではなく、不良なソフトウェア設計を行うこと。

割り込みハンドラーのreadyReadのようなスロットを調べて、処理が別のスレッドで実行されている間、またはアプリケーションがアイドル状態である間などに内部バッファーを埋めるフェッチ機能内にのみロジックを保持する必要があります。理由は、このようなアプリケーションは一般に大量サービスシステム。1つの要求の処理により多くの時間を費やした場合、2つの要求の間の時間間隔はとにかく超過します。

2番目のシナリオ:マルチスレッドアプリケーション

実際、このシナリオは1)とそれほど大きな違いはありません。それぞれのスレッドで発生することを正しく設計する必要があることを期待してください。メインループを軽度の「疑似割り込みハンドラー」で維持すると、問題なく他のスレッドで処理ロジックを維持できますが、このロジックは、QIODeviceではなく、独自のプリフェッチバッファーで動作するはずです。

6
evilruff

問題は非常に興味深いものです。

私のプログラムでは、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

1
Amartel

ファイル全体を取得する方法の例をいくつか示しますが、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

お役に立てば幸いです。

0
phyatt

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
user761576

ソケットからデータを受信して​​いるときに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
_

スロットが一度呼び出された場合、bytesAvailable()は常にreadyReadの呼び出し後に実際の数を返すため、追加のprocessEvents信号は無視できます。ストリームの一時停止時にのみ、whileループが終了します。しかし、次のreadReadyを見逃すことなく、再び開始します。

0
landydoc