qabstractsocket example c++ qt qtcpsocket

c++ - example - ¿Cómo asegurarse de que las señales readyRead() de QTcpSocket no se pueden perder?



qtcpsocket server client example (6)

Cuando se utiliza QTcpSocket para recibir datos, la señal a utilizar es readyRead() , que indica que hay nuevos datos disponibles. Sin embargo, cuando se encuentre en la implementación de ranura correspondiente para leer los datos, no se readyRead() ningún readyRead() adicional. Esto puede tener sentido, ya que usted ya está en la función, donde está leyendo todos los datos que están disponibles.

Descripción del problema

Sin embargo, asuma la siguiente implementación de este espacio:

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

¿Qué pasa si llegan algunos datos después de llamar a readAll() pero antes de salir de la ranura? ¿Qué pasa si este fue el último paquete de datos enviado por la otra aplicación (o al menos el último por algún tiempo)? No se emitirá ninguna señal adicional, por lo que debe asegurarse de leer todos los datos usted mismo.

Una forma de minimizar el problema (pero no evitarlo por completo)

Por supuesto, podemos modificar la ranura de esta manera:

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

Sin embargo, no hemos resuelto el problema. Todavía es posible que los datos lleguen justo después de que socket->bytesAvailable() -check (e incluso colocar la / otra comprobación en el extremo absoluto de la función no resuelva esto).

Asegurándose de poder reproducir el problema

Como este problema, por supuesto, ocurre muy raramente, me atengo a la primera implementación de la ranura e incluso agregaré un tiempo de espera artificial para asegurarme de que el problema ocurra:

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

Luego, dejo que otra aplicación envíe 100.000 bytes de datos. Esto es lo que pasa:

¡Nueva conexión!
32768 (o 16K o 48K)

La primera parte del mensaje se lee, pero el final ya no se lee, ya que no se volverá a llamar a readyRead() .

Mi pregunta es: ¿cuál es la mejor manera de estar seguro de que este problema nunca ocurre?

Solución posible

Una solución que surgió es volver a llamar a la misma ranura al final y verificar al comienzo de la ranura, si hay más datos para leer:

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

Como no llamo directamente a la ranura, pero uso QTimer::singleShot() , supongo que la QTcpSocket no puede saber que estoy llamando a la ranura nuevamente, por lo que el problema que readyRead() no se emite puede '' ya suceda

La razón por la que he incluido el parámetro bool selfCall es que la ranura llamada por QTcpSocket no puede salir antes, de lo contrario puede volver a ocurrir el mismo problema, los datos llegan exactamente en el momento incorrecto y readyRead() '' t emitido.

¿Es esta la mejor solución para resolver mi problema? ¿La existencia de este problema es un error de diseño en Qt o me falta algo?


Solución

De la documentación QIODevice :: readyRead :

Si vuelve a ingresar el bucle de evento o llama a waitForReadyRead () dentro de una ranura conectada a la señal readyRead (), la señal no se volverá a emitir.

Por lo tanto, solo asegúrese de no ingresar ningún bucle de evento o llamar a waitForReadyRead dentro de su ranura, y el problema debería desaparecer. (Consulte a continuación para obtener más detalles).

Fondo

Dentro de la implementación relevante de QAbstractSocketPrivate::canReadNotification , la señal de readyRead se emite de la siguiente manera (vea la línea número 733):

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

Puede ver que la variable boolean emittedReadyRead se restablece tan pronto como el bloque if sale del ámbito (hecho por QScopedValueRollback ). Entonces, la única posibilidad de perder una señal de readyRead es cuando el flujo de control del programa llega al

if (!emittedReadyRead && hasData) {

compruebe nuevamente antes de que readyRead el procesamiento actual de la señal readyRead . Y esto solo debería ser posible si

  • llama a waitForReadyRead dentro de tu ranura ( así que no lo hagas ),
  • haga cualquier gestión de eventos dentro de su ranura como, por ejemplo, instanciando un QEventLoop o llamando a QApplication::processEvents ( así que no haga esto ),
  • utilice varios subprocesos y no sincronice el acceso al mismo objeto QTcpSocket correctamente ( así que, por supuesto, tampoco lo haga )

Creo que el escenario mencionado en este tema tiene dos casos principales que funcionan de manera diferente, pero en general QT no tiene este problema en absoluto e intentaré explicar a continuación por qué.

Primer caso: aplicación de una sola rosca.

Qt usa la llamada al sistema select () para sondear el descriptor de archivo abierto para cualquier cambio ocurrido u operaciones disponibles. Dicción simple en cada ciclo Qt comprueba si alguno de los descriptores de archivos abiertos tiene datos disponibles para leer / cerrar, etc. Por lo tanto, en el flujo de aplicaciones de un solo hilo se ve así (la parte del código se simplifica)

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

Entonces, ¿qué pasará si llegan nuevos datos mientras el programa todavía está dentro de slotReadyRead ... honestamente nada especial. OS almacenará los datos en el búfer, y tan pronto como el control regrese a la siguiente ejecución de select () OS notificará al software que hay datos disponibles para un manejador de archivos particular. Funciona de la misma manera para sockets / archivos TCP, etc.

Puedo imaginar situaciones en las que (en caso de retrasos realmente largos en slotReadyRead y una gran cantidad de datos) puede experimentar un desbordamiento dentro de búferes OS FIFO (por ejemplo para puertos serie) pero eso tiene más que ver con un mal diseño de software en lugar de QT u OS problemas.

Deberías buscar en slots como readyRead like en un manejador de interrupciones y mantener su lógica solo dentro de la función fetch que llena tus búfers internos mientras el procesamiento debe hacerse en hilos separados o mientras la aplicación está inactiva, etc. La razón es que cualquier aplicación en general es un sistema de servicio masivo y, si dedica más tiempo a atender una solicitud, entonces un intervalo de tiempo entre dos solicitudes su cola se saturará de todos modos.

Segundo escenario: aplicación multiproceso

En realidad, este escenario no difiere mucho de 1) espera que usted diseñe correctamente lo que sucede en cada uno de sus hilos. Si mantienes el bucle principal con ''manejadores de pseudointerruptores'' livianos, estarás absolutamente bien y seguirás procesando la lógica en otros hilos, pero esta lógica debería funcionar con tus propios búferes de captación previa en lugar de con QIODevice.


El problema es bastante interesante.

En mi programa, el uso de QTcpSocket es muy intenso. Así que escribí toda la biblioteca, que divide los datos salientes en paquetes con encabezado, identificador de datos, número de índice del paquete y tamaño máximo, y cuando llega el siguiente dato, sé exactamente a qué lugar pertenece. Incluso si extraño algo, cuando viene el próximo readyRead , el receptor lee todo y compone los datos recibidos correctamente. Si la comunicación entre sus programas no es tan intensa, podría hacer lo mismo, pero con el temporizador (que no es muy rápido, pero resuelve el problema).

Acerca de tu solución. No creo que sea mejor que esto:

void readSocketData() { while(socket->bytesAvailable()) { datacounter += socket->readAll().length(); qDebug() << datacounter; QEventLoop loop; QTimer::singleShot(1000, &loop, SLOT(quit())); loop.exec(); } }

El problema de ambos métodos es el código justo después de salir de la ranura, pero antes de regresar de emitir la señal.

También podría conectarse con Qt::QueuedConnection .


Estos son algunos ejemplos de formas de obtener todo el archivo, pero usando algunas otras partes de la API de QNetwork:

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

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

Estos ejemplos muestran una forma más sólida de manejar los datos TCP, y cuando los búferes están llenos, y un mejor manejo de errores con una API de nivel superior.

Si aún desea utilizar la API de nivel inferior, aquí hay una publicación con una excelente forma de manejar los búferes:

Dentro de su readSocketData() haga algo como esto:

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

http://www.qtcentre.org/threads/11494-QTcpSocket-readyRead-and-buffer-size

EDITAR: ejemplos adicionales de cómo interactuar con 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

Espero que ayude.


Si se muestra un QProgressDialog mientras se reciben datos de un socket, solo funciona si se QApplication::processEvents() (por ejemplo, QProgessDialog::setValue(int) method). Esto, por supuesto, conduce a la pérdida de readyRead señales de readyRead como se mencionó anteriormente.

Así que mi solución fue un ciclo while que incluye el comando processEvents como:

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

Si se llama a la ranura una vez, se pueden ignorar las señales de readyRead porque bytesAvailable() siempre devuelve el número real después de la llamada a processEvents . Solo al pausar la secuencia, el ciclo while finaliza. Pero luego, el siguiente readReady no se pierde y lo vuelve a iniciar.


Tuve el mismo problema de inmediato con la ranura ReadyRead. No estoy de acuerdo con la respuesta aceptada; no resuelve el problema Usar bytesDisponible como describió Amartel fue la única solución confiable que encontré. Qt :: QueuedConnection no tuvo ningún efecto. En el siguiente ejemplo, estoy deserializando un tipo personalizado, por lo que es fácil predecir un tamaño mínimo de bytes. Nunca pierde datos.

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