Solved QTcpSocket fails to transmit data when reconnecting to server after RemoteHostClosedError
-
I'm writing a simple TCP client, based on QTcpSocket and managed by a QStateMachine (connect to server -> transmit data -> if disconnected for any reason, reconnect to server).
I noticed that if the connection is shut down on the server side (client is notified with RemoteHostClosedError), after reconnection the QTcpSocket write() method succeeds but no data is transmitted on the wire - nothing is received by the server, and the bytesWritten() signal on the client side does not fire up.
I found in the documentation for error() signal (https://doc.qt.io/qt-5/qabstractsocket.html#error) that "When this signal is emitted, the socket may not be ready for a reconnect attempt. In that case, attempts to reconnect should be done from the event loop". I think I'm already ok with that, as the reconnection happens in one of the QStateMachine states, and QStateMachine has its own event loop.
Below some simplified code to reproduce the issue:
testclient.h
#ifndef TESTCLIENT_H #define TESTCLIENT_H #include <QObject> #include <QTcpSocket> #include <QDebug> #include <QStateMachine> class TestClient : public QObject { Q_OBJECT public: explicit TestClient(QObject *parent = nullptr); public slots: void start(); signals: // FSM events void fsmEvtConnected(); void fsmEvtError(); private slots: void onSocketConnected(); // Notify connection to TCP server void onSocketDisconnected(); // Notify disconnection from TCP server void onSocketBytesWritten(qint64 bytes); // Notify number of bytes written to TCP server void onSocketError(QAbstractSocket::SocketError err); // FSM state enter/exit actions void onfsmConnectEntered(); void onfsmTransmitEntered(); void onfsmTransmitExited(); private: // Member variables QTcpSocket* m_socket; // TCP socket used for communications to server QStateMachine* m_clientFsm; // FSM defining general client behaviour private: void createClientFsm(); // Create client FSM }; #endif // TESTCLIENT_H
testclient.cpp
#include "testclient.h" #include <QState> #include <QThread> // Sleep //----------------------------------------------------------------------------- // PUBLIC METHODS //----------------------------------------------------------------------------- TestClient::TestClient(QObject *parent) : QObject(parent) { m_socket = new QTcpSocket(this); connect(m_socket, SIGNAL(connected()),this, SLOT(onSocketConnected())); connect(m_socket, SIGNAL(disconnected()),this, SLOT(onSocketDisconnected())); connect(m_socket, SIGNAL(bytesWritten(qint64)),this, SLOT(onSocketBytesWritten(qint64))); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError))); } void TestClient::start() { createClientFsm(); m_clientFsm->start(); } //----------------------------------------------------------------------------- // TCP CONNECTION MANAGEMENT SLOTS //----------------------------------------------------------------------------- void TestClient::onSocketConnected() { qDebug() << "connected..."; emit fsmEvtConnected(); } void TestClient::onSocketDisconnected() { qDebug() << "disconnected..."; emit fsmEvtError(); } void TestClient::onSocketBytesWritten(qint64 bytes) { qDebug() << bytes << " bytes written..."; } void TestClient::onSocketError(QAbstractSocket::SocketError err) { qDebug() << "socket error " << err; } //----------------------------------------------------------------------------- // FSM MANAGEMENT //----------------------------------------------------------------------------- void TestClient::createClientFsm() { m_clientFsm = new QStateMachine(this); // Create states QState* sConnect = new QState(); QState* sTransmit = new QState(); // Add transitions between states sConnect->addTransition(this, SIGNAL(fsmEvtConnected()), sTransmit); sTransmit->addTransition(this, SIGNAL(fsmEvtError()), sConnect); // Add entry actions to states connect(sConnect, SIGNAL(entered()), this, SLOT(onfsmConnectEntered())); connect(sTransmit, SIGNAL(entered()), this, SLOT(onfsmTransmitEntered())); // Add exit actions to states connect(sTransmit, SIGNAL(exited()), this, SLOT(onfsmTransmitExited())); // Create state machine m_clientFsm->addState(sConnect); m_clientFsm->addState(sTransmit); m_clientFsm->setInitialState(sConnect); } void TestClient::onfsmConnectEntered() { qDebug() << "connecting..."; m_socket->connectToHost("localhost", 11000); // Wait for connection result if(!m_socket->waitForConnected(10000)) { qDebug() << "Error: " << m_socket->errorString(); emit fsmEvtError(); } } void TestClient::onfsmTransmitEntered() { qDebug() << "sending data..."; m_socket->write("TEST MESSAGE"); } void TestClient::onfsmTransmitExited() { qDebug() << "waiting before reconnection attempt..."; QThread::sleep(2); }
main.cpp
#include <QCoreApplication> #include "testclient.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); TestClient client(&a); client.start(); return a.exec(); }
To test, you can just launch netcat (nc -l -p 11000) , then close the nc process after receiving TEST MESSAGE and finally relaunch it again. The second time, TEST MESSAGE is not received, and we don't have the onSocketBytesWritten() printout, see below:
connecting... connected... sending data... 12 bytes written... <<<<<<<<<< Correct transmission, event fires up socket error QAbstractSocket::RemoteHostClosedError disconnected... waiting before reconnection attempt... connecting... connected... sending data... <<<<<<<<<< No transmission, event does not fire up, no socket errors!
Regards
-
To sum up and close the topic: the proper solution would be Solution 1 from a couple of posts above:
@Andrea-Narciso said in QTcpSocket fails to transmit data when reconnecting to server after RemoteHostClosedError:
Not using waitForConnected() function and completely relying on the connected() event plus some QTimer to manage the timeout for a connection attempt. Apparently, the use of such blocking function messes up something in the event loop, even if it is not clear to me how - the connected() event and all the state machine events are still fired up! Maybe this is a QT bug...
Anyway, to test this solution simply remove the following lines from onfsmConnectEntered():
// Wait for connection result if(!m_socket->waitForConnected(10000)) { qDebug() << "Error: " << m_socket->errorString(); emit fsmEvtError(); }
By adding a QTimer, a connection timeout can be handled so that waitForConnected() can be entirely replaced.
-
I found out that if I create the QTcpSocket on connection and destroy it on disconnection, the problem does not happen. Is this the expected/proper way to use sockets?
Wouldn't it be possible instead to create the socket just once and just connect/disconnect? Maybe it is just a matter of flushing or cleaning up in a specific manner, but I could not find it so far.
It would be great if somebody could comment on my interpretation, so that I can understand better what's happening.
BTW, here are the modifications that make the code above work on server-side disconnection:
Move socket creation from class constructor to onfsmConnectEntered() - handler for entry in the "Connect" QState:
void TestClient::onfsmConnectEntered() { m_socket = new QTcpSocket(this); connect(m_socket, SIGNAL(connected()),this, SLOT(onSocketConnected())); connect(m_socket, SIGNAL(disconnected()),this, SLOT(onSocketDisconnected())); connect(m_socket, SIGNAL(bytesWritten(qint64)),this, SLOT(onSocketBytesWritten(qint64))); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError))); qDebug() << "connecting..."; m_socket->connectToHost("localhost", 11000); // The rest of the method is the same }
Delete the socket on disconnection, so that it is deallocated and will be created again on reconnection:
void TestClient::onSocketDisconnected() { qDebug() << "disconnected..."; m_socket->deleteLater(); m_socket = nullptr; emit fsmEvtError(); }
-
Hey, I'm relatively sure you've hit a bug. Your code looks fine to me, so I suggest posting it on the bugtracker.
-
Hi kshegunov, in the meantime by experimenting and by some crossposting (https://stackoverflow.com/questions/59593665/qtcpsocket-fails-to-transmit-data-when-reconnecting-to-server-after-remotehostcl) a couple of solutions came out, reported below.
However, such solutions tackle the problem with different approach than the one shown in my original code, so if there is a bug in the QT codebase, they could actually circumvent it.
Asking you a favor: please have a look at the solutions below too, if you still think my original code was legit and that the solutions work around a possible bug in QT, I'll go on posting the issue in the bugtracker
Solution 1
Not using waitForConnected() function and completely relying on the connected() event plus some QTimer to manage the timeout for a connection attempt. Apparently, the use of such blocking function messes up something in the event loop, even if it is not clear to me how - the connected() event and all the state machine events are still fired up! Maybe this is a QT bug...Anyway, to test this solution simply remove the following lines from onfsmConnectEntered():
// Wait for connection result if(!m_socket->waitForConnected(10000)) { qDebug() << "Error: " << m_socket->errorString(); emit fsmEvtError(); }
By adding a QTimer, a connection timeout can be handled so that waitForConnected() can be entirely replaced.
Solution 2
Creating the QTcpSocket on connection and destroying it on disconnection, so that the socket context is completely discarded and freshly created at every reconnection. Modifications to my original code:Move socket creation from class constructor to onfsmConnectEntered() - handler for entry in the "Connect" QState:
void TestClient::onfsmConnectEntered() { m_socket = new QTcpSocket(this); connect(m_socket, SIGNAL(connected()),this, SLOT(onSocketConnected())); connect(m_socket, SIGNAL(disconnected()),this, SLOT(onSocketDisconnected())); connect(m_socket, SIGNAL(bytesWritten(qint64)),this, SLOT(onSocketBytesWritten(qint64))); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError))); qDebug() << "connecting..."; m_socket->connectToHost("localhost", 11000); // The rest of the method is the same }
Delete the socket on disconnection, so that it is deallocated and will be created again on reconnection:
void TestClient::onSocketDisconnected() { qDebug() << "disconnected..."; m_socket->deleteLater(); m_socket = nullptr; emit fsmEvtError(); }
-
@Andrea-Narciso said in QTcpSocket fails to transmit data when reconnecting to server after RemoteHostClosedError:
Not using waitForConnected() function and completely relying on the connected() event plus some QTimer to manage the timeout for a connection attempt.
This is a good advice. The
waitFor***
have been finicky forever and even the docs acknowledge in reality they're not to be used. If that works for you, then this is the way to go.Creating the QTcpSocket on connection and destroying it on disconnection, so that the socket context is completely discarded and freshly created at every reconnection.
I don't consider this normal behavior, no. After the
disconnect
runs the socket should be available for reuse. Recreating the object should not be necessary. -
To sum up and close the topic: the proper solution would be Solution 1 from a couple of posts above:
@Andrea-Narciso said in QTcpSocket fails to transmit data when reconnecting to server after RemoteHostClosedError:
Not using waitForConnected() function and completely relying on the connected() event plus some QTimer to manage the timeout for a connection attempt. Apparently, the use of such blocking function messes up something in the event loop, even if it is not clear to me how - the connected() event and all the state machine events are still fired up! Maybe this is a QT bug...
Anyway, to test this solution simply remove the following lines from onfsmConnectEntered():
// Wait for connection result if(!m_socket->waitForConnected(10000)) { qDebug() << "Error: " << m_socket->errorString(); emit fsmEvtError(); }
By adding a QTimer, a connection timeout can be handled so that waitForConnected() can be entirely replaced.