Qt TCP 心跳到底应该怎么设计?

别让心跳把连接"越检测越死"

做 TCP 长连接时,有一个问题几乎躲不过:

TCP 已经连接了,怎么知道对方还活着?

很多人的第一反应就是:

cpp 复制代码
QTimer timer;
connect(&timer, &QTimer::timeout, this, [this]() {
    socket->write("ping");
});

看起来没问题。

但真正上线以后,你会发现:

  • 网线拔了,connected() 可能还是保持着

  • 对端进程崩了,本地不一定马上收到 disconnected()

  • Wi-Fi 切换网络,连接可能进入"半死不活"状态

  • 服务端还活着,但业务线程已经卡死,TCP 依然是 ESTABLISHED

所以:

TCP 连接成功 ≠ 对端业务正常。

这也是为什么生产环境里的 TCP 长连接,通常都会设计应用层心跳机制


一、TCP 为什么需要心跳?

TCP 本身提供可靠传输,但它并不会主动告诉你:

"对方应用程序现在是不是还正常运行?"

例如:

cpp 复制代码
Client                    Server
  │                         │
  │──── TCP Connect ───────>│
  │<─── Connect OK ─────────│
  │                         │
  │                         │
  │       网络断开          X
  │                         │
  │                         │
  │    TCP 连接状态可能仍然存在
  │

尤其是网络异常时:

cpp 复制代码
客户端
  │
  │ ESTABLISHED
  │
  X────── 网络已经断了
  │
  │
  │ 但应用层暂时不知道

这时候如果你的程序只是:

cpp 复制代码
if (socket->state() == QAbstractSocket::ConnectedState)
{
    // 认为连接正常
}

就容易踩坑。


二、一个靠谱的心跳应该怎么设计?

推荐使用:

cpp 复制代码
Client                          Server
  │                               │
  │────── HEARTBEAT_REQ ─────────>│
  │                               │
  │<───── HEARTBEAT_ACK ──────────│
  │                               │
  │────── HEARTBEAT_REQ ─────────>│
  │                               │
  │<───── HEARTBEAT_ACK ──────────│

客户端维护三个状态:

bash 复制代码
lastSendTime
lastRecvTime
heartbeatTimer

例如:

bash 复制代码
每 5 秒发送一次心跳

5s
 │
 ├── ping
 │
 ├── 等待 pong
 │
 ├── 5s
 │
 ├── ping
 │
 └── ...

如果连续一段时间没有收到 ACK:

bash 复制代码
超过 15 秒
     ↓
认为连接异常
     ↓
主动 disconnect
     ↓
进入重连

这里有一个非常重要的原则:

不要因为一次心跳超时就马上断开。

网络偶尔抖一下很正常。

通常可以采用:

bash 复制代码
心跳周期:5 秒
超时时间:15 秒

也就是允许连续丢失几个心跳。


三、不要只发送 Ping,还要判断 Pong

最简单的协议可以设计成:

bash 复制代码
PING
PONG

例如:

复制代码
socket->write("PING");

服务器收到:

bash 复制代码
if (data == "PING")
{
    socket->write("PONG");
}

客户端收到:

bash 复制代码
if (data == "PONG")
{
    lastRecvTime = QDateTime::currentMSecsSinceEpoch();
}

这样才能形成完整闭环:

cpp 复制代码
发送 Ping
   ↓
Server 收到
   ↓
Server 回复 Pong
   ↓
Client 收到
   ↓
更新 lastRecvTime

四、完整 Qt 客户端示例

下面写一个比较实用的版本。

HeartbeatClient.h

cpp 复制代码
#pragma once

#include <QObject>
#include <QTcpSocket>
#include <QTimer>
#include <QElapsedTimer>

class HeartbeatClient : public QObject
{
    Q_OBJECT

public:
    explicit HeartbeatClient(QObject *parent = nullptr);

    void connectToServer(const QString &host, quint16 port);

private slots:
    void onConnected();
    void onDisconnected();
    void onReadyRead();

    void sendHeartbeat();
    void checkHeartbeat();

private:
    QTcpSocket *m_socket = nullptr;

    QTimer m_heartbeatTimer;
    QTimer m_checkTimer;

    qint64 m_lastReceiveTime = 0;

    QString m_host;
    quint16 m_port = 0;
};

HeartbeatClient.cpp

cpp 复制代码
#include "HeartbeatClient.h"

#include <QDateTime>
#include <QDebug>

HeartbeatClient::HeartbeatClient(QObject *parent)
    : QObject(parent)
{
    m_socket = new QTcpSocket(this);

    connect(m_socket,
            &QTcpSocket::connected,
            this,
            &HeartbeatClient::onConnected);

    connect(m_socket,
            &QTcpSocket::disconnected,
            this,
            &HeartbeatClient::onDisconnected);

    connect(m_socket,
            &QTcpSocket::readyRead,
            this,
            &HeartbeatClient::onReadyRead);

    // 每 5 秒发送一次心跳
    m_heartbeatTimer.setInterval(5000);

    connect(&m_heartbeatTimer,
            &QTimer::timeout,
            this,
            &HeartbeatClient::sendHeartbeat);

    // 每 1 秒检查一次心跳状态
    m_checkTimer.setInterval(1000);

    connect(&m_checkTimer,
            &QTimer::timeout,
            this,
            &HeartbeatClient::checkHeartbeat);
}

void HeartbeatClient::connectToServer(const QString &host,
                                      quint16 port)
{
    m_host = host;
    m_port = port;

    m_socket->connectToHost(host, port);
}
连接成功
cpp 复制代码
void HeartbeatClient::onConnected()
{
    qDebug() << "TCP connected";

    m_lastReceiveTime =
        QDateTime::currentMSecsSinceEpoch();

    m_heartbeatTimer.start();
    m_checkTimer.start();
}
断开连接
cpp 复制代码
void HeartbeatClient::onDisconnected()
{
    qDebug() << "TCP disconnected";

    m_heartbeatTimer.stop();
    m_checkTimer.stop();
}
发送心跳
cpp 复制代码
void HeartbeatClient::sendHeartbeat()
{
    if (m_socket->state() != QAbstractSocket::ConnectedState)
        return;

    m_socket->write("PING\n");
    m_socket->flush();

    qDebug() << "send PING";
}
收到数据
cpp 复制代码
void HeartbeatClient::onReadyRead()
{
    while (m_socket->canReadLine())
    {
        QByteArray data = m_socket->readLine().trimmed();

        if (data == "PONG")
        {
            m_lastReceiveTime =
                QDateTime::currentMSecsSinceEpoch();

            qDebug() << "receive PONG";
        }
        else
        {
            // 处理正常业务数据
            qDebug() << "business data:" << data;
        }
    }
}
超时检测(最关键)
cpp 复制代码
void HeartbeatClient::checkHeartbeat()
{
    if (m_socket->state() != QAbstractSocket::ConnectedState)
        return;

    const qint64 now =
        QDateTime::currentMSecsSinceEpoch();

    const qint64 elapsed =
        now - m_lastReceiveTime;

    // 15 秒没有收到任何数据
    if (elapsed > 15000)
    {
        qDebug() << "heartbeat timeout";

        m_socket->abort();

        // 后续可以进入重连流程
    }
}

这样一个最基本的 TCP 心跳机制就完成了。


五、服务器端其实更简单

服务端收到 PING 回复 PONG 即可:

cpp 复制代码
void ClientConnection::onReadyRead()
{
    while (m_socket->canReadLine())
    {
        QByteArray data =
            m_socket->readLine().trimmed();

        if (data == "PING")
        {
            m_socket->write("PONG\n");
        }
        else
        {
            handleBusinessMessage(data);
        }
    }
}

整体流程:

六、这里有一个非常容易犯的错误

很多代码会这么写:

cpp 复制代码
if (socket->bytesAvailable() == 0)
{
    // 对方挂了
}

这是错误的。

没有数据 ≠ TCP 断开。

TCP 是一个字节流协议。

对方可能只是:

复制代码
5 秒没发送业务数据

并不代表:

复制代码
对方已经死亡

所以必须使用:

复制代码
PING → PONG

这种应用层机制主动确认。

九、最后总结一下

Qt TCP 心跳真正要解决的不是:

"每隔几秒发送一个字符串。"

而是:

如何判断一个 TCP 长连接是否仍然具备业务可用性。

推荐的基本方案:

cpp 复制代码
心跳周期:5s
超时时间:15s
协议:PING / PONG

核心逻辑:

cpp 复制代码
           每 5 秒
              │
              ▼
           发送 PING
              │
              ▼
           等待 PONG
              │
              ▼
        更新 lastReceive
              │
              ▼
       是否超过 15 秒?
          /          \
        否            是
        │             │
        ▼             ▼
      正常          断开连接
                      │
                      ▼
                     重连

而在真正的工业软件、IM、设备通信、车载系统中,还应该继续考虑:

  • 消息长度协议

  • 心跳与业务消息复用

  • 超时次数

  • 网络抖动

  • 自动重连

  • 指数退避

  • 连接状态机

  • 线程安全

  • 半连接检测

  • 服务端多客户端心跳管理


一句话记住:

TCP 负责"可靠传输",心跳负责"确认对方还活着",重连负责"连接死了之后怎么办"。

这三件事情不要混在一起设计,TCP 长连接才会真正稳定。

相关推荐
上海云盾-小余22 分钟前
分层防护思路:WAF 应用防护与 TCP 底层防护如何协同
网络·网络协议·tcp/ip
H_oRIZoN_1 小时前
Linux入门DAY35(TCP粘包问题与HTTP)
linux·tcp/ip·http
好评1242 小时前
【Linux】传输层协议TCP
linux·网络·tcp/ip
秋田君2 小时前
Qt_webSocket协议编程实战
开发语言·qt·websocket
Fcy6482 小时前
Linux下 Socket编程 —— TCP网络编程
linux·网络·tcp/ip
西西弗Sisyphus4 小时前
Qt 在无边框窗口上做一套换肤系统
开发语言·数据库·qt
西西弗Sisyphus4 小时前
Qt 启动 UsageStatistic 插件报错
开发语言·qt
神仙别闹4 小时前
基于 QT(C++)实现操作系统
数据库·c++·qt
Discipline~Hai4 小时前
Linux网络编程02-TCP协议
linux·网络·tcp/ip·嵌入式·linux网络编程