web-dev-qa-db-fra.com

Comment s'assurer que les signaux readyRead () de QTcpSocket ne peuvent pas être manqués?

Lorsque vous utilisez QTcpSocket pour recevoir des données, le signal à utiliser est readyRead(), ce qui indique que de nouvelles données sont disponibles. Cependant, lorsque vous êtes dans l'implémentation de créneau correspondant pour lire les données, aucun readyRead() supplémentaire n'est émis. Cela peut avoir un sens, puisque vous êtes déjà dans la fonction, où vous lisez toutes les données disponibles. 

Description du problème

Cependant, supposons que l'implémentation suivante de cet emplacement:

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

Que faire si des données arrivent après avoir appelé readAll() mais avant de quitter la fente? Et si c’était le dernier paquet de données envoyé par l’autre application (ou au moins le dernier depuis un certain temps)? Aucun signal supplémentaire ne sera émis, vous devez donc vous assurer de lire toutes les données vous-même. . 

Une façon de minimiser le problème (mais pas de l'éviter totalement)

Bien sûr, nous pouvons modifier la fente comme ceci:

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

Cependant, nous n'avons pas résolu le problème. Il est toujours possible que les données arrivent juste après la vérification socket->bytesAvailable()- (et même placer la vérification/une autre à la fin absolue de la fonction ne résout pas le problème). 

S'assurer de pouvoir reproduire le problème

Comme ce problème ne se produit bien sûr que très rarement, je m'en tiens à la première mise en œuvre de la fente, et j'ajouterai même un délai d'attente artificiel, pour être sûr que le problème se produise:

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

J'ai ensuite laissé une autre application envoyer 100 000 octets de données. Voici ce qui se passe:

nouvelle connexion!
32768 (ou 16K ou 48K)

La première partie du message est lue, mais la fin n'est plus lue, car readyRead() ne sera plus appelé. 

Ma question est la suivante: quel est le meilleur moyen de s’assurer que ce problème ne se produit jamais?

Solution possible

Une solution que j'ai proposée consiste à rappeler le même emplacement à la fin et à vérifier au début de l'emplacement s'il y a plus de données à lire:

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

Comme je n'appelle pas directement l'emplacement, mais que j'utilise QTimer::singleShot(), je suppose que le QTcpSocket ne peut pas savoir que j'appelle à nouveau, alors le problème qui se pose est que readyRead() n'est pas émis. n'arrive plus. 

La raison pour laquelle j'ai inclus le paramètre bool selfCall est que l'emplacement appelé par la variable QTcpSocket n'est pas autorisé à quitter plus tôt, sinon le même problème peut se reproduire, les données arrivent exactement au mauvais moment et readyRead() isn pas émis. 

Est-ce vraiment la meilleure solution pour résoudre mon problème? L’existence de ce problème est-elle une erreur de conception dans Qt ou manque-t-il quelque chose?

27
Misch

Réponse courte

La documentation of QIODevice::readyRead() indique:

Si vous entrez à nouveau dans la boucle d'événement ou appelez waitForReadyRead() dans un emplacement connecté au signal readyRead(), le signal ne sera pas réémis.

Ainsi, assurez-vous que vous

  • ne pas instancier une QEventLoop dans votre emplacement,
  • ne pas appeler QApplication::processEvents() dans votre logement,
  • ne pas appeler QIODevice::waitForReadyRead() dans votre logement,
  • don't utilise la même instance QTcpSocket dans différents threads,

et vous devriez toujours recevoir toutes les données envoyées par l'autre partie.


Contexte

Le signal readyRead() est émis par QAbstractSocketPrivate::emitReadyRead() comme suit:

if (!emittedReadyRead && channel == currentReadChannel) {
    QScopedValueRollback<bool> r(emittedReadyRead);
    emittedReadyRead = true;
    emit q->readyRead();
}

La variable emittedReadyRead est restaurée à false dès que le bloc if sort de la portée (effectué par QScopedValueRollback). Ainsi, la seule chance de manquer un signal readyRead() est lorsque le flux de contrôle atteint à nouveau la condition ifavant le traitement du dernier signal readyRead() est terminé.

Et cela ne devrait être possible que dans les situations énumérées ci-dessus.

9
emkey08

Je pense que le scénario mentionné dans cette rubrique comporte deux cas majeurs qui fonctionnent différemment, mais en général, QT n’a pas ce problème du tout et j’essaierai d’expliquer pourquoi.

Premier cas: application à un seul thread.

Qt utilise l’appel système select () pour interroger le descripteur de fichier ouvert en cas de modification ou d’opération disponible. Dire simplement à chaque boucle que Qt vérifie si l'un des descripteurs de fichier ouverts dispose de données à lire/fermer, etc. Ainsi, un flux d'application à thread unique ressemble à cela (partie de code simplifiée)

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

void slotReadyRead() {
     some code;
}

Alors que se passera-t-il si de nouvelles données arrivent alors que le programme est toujours dans slotReadyRead .. honnêtement, rien de spécial. Le système d'exploitation mettra les données en mémoire tampon et dès que le contrôle reviendra à la prochaine exécution de select (), le système d'exploitation informera le logiciel de la disponibilité des données pour le descripteur de fichier particulier. Cela fonctionne absolument de la même manière pour les sockets/fichiers TCP, etc.

Je peux imaginer des situations où (en cas de très longs retards dans slotReadyRead et beaucoup de données à venir), vous pouvez rencontrer un débordement dans les tampons du système d'exploitation FIFO (par exemple pour les ports série) la conception de logiciels plutôt que des problèmes QT ou OS. 

Vous devriez regarder sur les emplacements comme readyRead comme sur des gestionnaires d'interruption et conserver leur logique uniquement dans la fonctionnalité d'extraction qui remplit les mémoires tampons internes lors du traitement, elle doit être effectuée dans des threads séparés ou lors d'une application inactive, etc. La raison en est un système de service de masse et s’il passe plus de temps à répondre à une demande, un intervalle de temps entre deux demandes dépassera de toute façon sa file d’attente. 

Deuxième scénario: application multithread

En réalité, ce scénario ne diffère pas beaucoup de 1) vous attendez à ce que vous deviez concevoir correctement ce qui se passe dans chacun de vos threads. Si vous conservez la boucle principale avec des «gestionnaires de pseudo-interruptions» très légers, vous serez parfaitement en mesure de conserver la logique de traitement dans les autres threads, mais cette logique devrait fonctionner avec vos propres tampons de prélecture plutôt qu'avec QIODevice. 

6
evilruff

Voici quelques exemples de moyens pour obtenir le fichier entier, mais en utilisant d'autres parties de l'API QNetwork:

http://qt-project.org/doc/qt-4.8/network-downloadmanager.html

http://qt-project.org/doc/qt-4.8/network-download.html

Ces exemples montrent un moyen plus efficace de gérer les données TCP et lorsque les mémoires tampons sont pleines, ainsi qu'une meilleure gestion des erreurs avec une API de niveau supérieur.

Si vous souhaitez toujours utiliser l'API de niveau inférieur, voici un article avec un excellent moyen de gérer les tampons:

Dans votre readSocketData(), faites quelque chose comme ceci:

if (bytesAvailable() < 256)
    return;
QByteArray data = read(256);

http://www.qtcentre.org/threads/11494-QTcpSocket-readyLire-et-buffer-size

EDIT: Autres exemples d'interaction avec 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

J'espère que cela pourra aider.

0
phyatt

J'ai eu le même problème tout de suite avec l'emplacement ReadyRead. Je ne suis pas d'accord avec la réponse acceptée. cela ne résout pas le problème. Utiliser bytesAvailable comme décrit par Amartel est la seule solution fiable que j'ai trouvée. Qt :: QueuedConnection n'a eu aucun effet. Dans l'exemple suivant, je désérialise un type personnalisé. Il est donc facile de prévoir une taille minimale en octets. Il ne manque jamais de données.

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

Si un QProgressDialog doit être affiché lors de la réception de données d'un socket, il ne fonctionne que si une QApplication::processEvents() est envoyée (par exemple, à l'aide de la méthode QProgessDialog::setValue(int)). Ceci conduit bien sûr à la perte des signaux readyRead comme mentionné ci-dessus.

Donc, ma solution de contournement était une boucle while comprenant la commande processEvents, telle que:

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

Si l'emplacement est appelé une fois, tous les signaux readyRead supplémentaires peuvent être ignorés car la fonction bytesAvailable() renvoie toujours le numéro actuel après l'appel processEvents. Ce n'est que lorsque le flux est mis en pause que la boucle while se termine. Mais alors la prochaine readReady n'est pas manquée et la redémarre.

0
landydoc

Le problème est assez intéressant.

Dans mon programme, l'utilisation de QTcpSocket est très intensive. J'ai donc écrit toute la bibliothèque, qui décompose les données sortantes en packages avec un en-tête, un identificateur de données, le numéro d'index du package et sa taille maximale, et lorsque la prochaine donnée arrive, je sais exactement à quoi elle appartient. Même si quelque chose me manque, quand la prochaine readyRead arrive, le récepteur lit tout et compose correctement les données reçues. Si la communication entre vos programmes n’est pas aussi intense, vous pouvez en faire autant, mais avec une minuterie (ce qui n’est pas très rapide, mais résout le problème.)

A propos de votre solution. Je ne pense pas que c'est mieux alors:

void readSocketData()
{
    while(socket->bytesAvailable())
    {
        datacounter += socket->readAll().length();
        qDebug() << datacounter;

        QEventLoop loop;
        QTimer::singleShot(1000, &loop, SLOT(quit()));
        loop.exec();
    }
}

Le problème des deux méthodes est le code juste après avoir quitté la fente, mais avant de revenir d'émettre le signal.

Aussi, vous pouvez vous connecter avec Qt::QueuedConnection.

0
Amartel