QTcpSocket 在 RemoteHostClosedError 后重新连接到服务器时传输数据失败

QTcpSocket fails to transmit data when reconnecting to server after RemoteHostClosedError

请参阅问题末尾的 EDIT1 以获得可能的解决方案 - 如果有人可以对我的解释发表评论,那就太好了,这样我就可以更好地理解正在发生的事情

我正在编写一个简单的 TCP 客户端,基于 QTcpSocket 并由 QStateMachine 管理(连接到服务器 -> 传输数据 -> 如果由于任何原因断开连接,重新连接到服务器)。

我注意到,如果连接在服务器端关闭(客户端收到 RemoteHostClosedError 通知),重新连接后 QTcpSocket write() 方法成功,但网络上未传输任何数据 - 服务器未收到任何数据, 并且客户端的 bytesWritten() 信号没有启动。

我在 error() 信号 (https://doc.qt.io/qt-5/qabstractsocket.html#error) 的文档中发现

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".

我想我已经同意了,因为重新连接发生在 QStateMachine 状态之一,并且根据 QT 文档,QStateMachine 应该有自己的事件循环。

下面是一些重现问题的简化代码(抱歉,不是那么简单,但我找不到更简单的方法来显示问题):

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

要进行测试,您可以只启动 netcat (nc -l -p 11000) ,然后在收到 TEST MESSAGE 后关闭 nc 进程,最后重新启动它。第二次,没有收到 TEST MESSAGE,我们也没有 onSocketBytesWritten() 打印输出,见下文:

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!

EDIT1: 我发现如果我在连接时创建 QTcpSocket 并在断开连接时销毁它,问题就不会发生。这是使用套接字的expected/proper方式吗?

是否可以只创建一次套接字 connect/disconnect?也许这只是以特定方式冲洗或清理的问题,但到目前为止我找不到它。

以下是使上述代码在服务器端断开连接时工作的修改:

将套接字创建从 class 构造函数移至 onfsmConnectEntered() - "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
}

在断开连接时删除套接字,以便它被释放并在重新连接时重新创建:

void TestClient::onSocketDisconnected()
{
    qDebug() << "disconnected...";
    m_socket->deleteLater();
    m_socket = nullptr;

    emit fsmEvtError();
}

不要使用 waitForX 方法,因为它们会阻塞事件循环并阻止它们使用该资源,因为信号无法正确完成它们的工作或 QStateMachine。

综合以上,解决方案是:

void TestClient::onfsmConnectEntered()
{
    m_socket->connectToHost("localhost", 11000);
}

但即便如此,您的代码仍有错误,因为它没有考虑其他情况,例如:

  • 如果当你启动客户端时服务器不是运行,你的应用程序将尝试连接错误将被启动并且没有别的。

  • 如果服务器失败的时间超过设置为 waitForConnected() 的 10000 毫秒超时时间,则会发生与前一种情况相同的情况。

然后我们的想法是尝试连接,直到您确定连接为止,这可以通过具有适当周期的 QTimer 来完成。

testclient.h

#ifndef TESTCLIENT_H
#define TESTCLIENT_H

#include <QObject>

class QTcpSocket;
class QStateMachine;
class QTimer;

#include <QAbstractSocket>

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();
private:
    // Member variables
    QTcpSocket*         m_socket;       // TCP socket used for communications to server
    QStateMachine*      m_clientFsm;      // FSM defining general client behaviour
    QTimer*             m_timer;
private:
    void createClientFsm();             // Create client FSM
    void tryConnect();
};

#endif // TESTCLIENT_H

testclient.cpp

#include "testclient.h"
#include <QState>
#include <QStateMachine>
#include <QTcpSocket>
#include <QThread>      // Sleep
#include <QTimer>

//-----------------------------------------------------------------------------
// PUBLIC METHODS
//-----------------------------------------------------------------------------
TestClient::TestClient(QObject *parent) : QObject(parent)
{
    m_socket = new QTcpSocket(this);
    m_timer = new QTimer(this);
    m_timer->setInterval(100);
    connect(m_timer, &QTimer::timeout, this, &TestClient::tryConnect);
    connect(m_socket, &QAbstractSocket::connected,this, &TestClient::onSocketConnected);
    connect(m_socket, &QAbstractSocket::disconnected,this, &TestClient::onSocketDisconnected);
    connect(m_socket, &QIODevice::bytesWritten,this, &TestClient::onSocketBytesWritten);
    connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), this, &TestClient::onSocketError);
}
void TestClient::start()
{
    createClientFsm();
    m_clientFsm->start();
}
//-----------------------------------------------------------------------------
// TCP CONNECTION MANAGEMENT SLOTS
//-----------------------------------------------------------------------------
void TestClient::onSocketConnected()
{
    m_timer->stop();
    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, &QAbstractState::entered, this, &TestClient::onfsmConnectEntered);
    connect(sTransmit, &QAbstractState::entered, this, &TestClient::onfsmTransmitEntered);
    // Create state machine
    m_clientFsm->addState(sConnect);
    m_clientFsm->addState(sTransmit);
    m_clientFsm->setInitialState(sConnect);
}
void TestClient::tryConnect(){
    m_socket->connectToHost("localhost", 11000);
}
void TestClient::onfsmConnectEntered()
{
    m_timer->start();
}
void TestClient::onfsmTransmitEntered()
{
    qDebug() << "sending data...";
    m_socket->write("TEST MESSAGE");
}