别让心跳把连接“越检测越死”
做 TCP 长连接时,有一个问题几乎躲不过:
TCP 已经连接了,怎么知道对方还活着?
很多人的第一反应就是:
QTimer timer; connect(&timer, &QTimer::timeout, this, [this]() { socket->write("ping"); });看起来没问题。
但真正上线以后,你会发现:
网线拔了,
connected()可能还是保持着对端进程崩了,本地不一定马上收到
disconnected()Wi-Fi 切换网络,连接可能进入“半死不活”状态
服务端还活着,但业务线程已经卡死,TCP 依然是
ESTABLISHED
所以:
TCP 连接成功 ≠ 对端业务正常。
这也是为什么生产环境里的 TCP 长连接,通常都会设计应用层心跳机制。
一、TCP 为什么需要心跳?
TCP 本身提供可靠传输,但它并不会主动告诉你:
“对方应用程序现在是不是还正常运行?”
例如:
Client Server │ │ │──── TCP Connect ───────>│ │<─── Connect OK ─────────│ │ │ │ │ │ 网络断开 X │ │ │ │ │ TCP 连接状态可能仍然存在 │尤其是网络异常时:
客户端 │ │ ESTABLISHED │ X────── 网络已经断了 │ │ │ 但应用层暂时不知道这时候如果你的程序只是:
if (socket->state() == QAbstractSocket::ConnectedState) { // 认为连接正常 }就容易踩坑。
二、一个靠谱的心跳应该怎么设计?
推荐使用:
Client Server │ │ │────── HEARTBEAT_REQ ─────────>│ │ │ │<───── HEARTBEAT_ACK ──────────│ │ │ │────── HEARTBEAT_REQ ─────────>│ │ │ │<───── HEARTBEAT_ACK ──────────│客户端维护三个状态:
lastSendTime lastRecvTime heartbeatTimer例如:
每 5 秒发送一次心跳 5s │ ├── ping │ ├── 等待 pong │ ├── 5s │ ├── ping │ └── ...如果连续一段时间没有收到 ACK:
超过 15 秒 ↓ 认为连接异常 ↓ 主动 disconnect ↓ 进入重连这里有一个非常重要的原则:
不要因为一次心跳超时就马上断开。
网络偶尔抖一下很正常。
通常可以采用:
心跳周期:5 秒 超时时间:15 秒也就是允许连续丢失几个心跳。
三、不要只发送 Ping,还要判断 Pong
最简单的协议可以设计成:
PING PONG例如:
socket->write("PING");服务器收到:
if (data == "PING") { socket->write("PONG"); }客户端收到:
if (data == "PONG") { lastRecvTime = QDateTime::currentMSecsSinceEpoch(); }这样才能形成完整闭环:
发送 Ping ↓ Server 收到 ↓ Server 回复 Pong ↓ Client 收到 ↓ 更新 lastRecvTime四、完整 Qt 客户端示例
下面写一个比较实用的版本。
HeartbeatClient.h
#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
#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); }连接成功
void HeartbeatClient::onConnected() { qDebug() << "TCP connected"; m_lastReceiveTime = QDateTime::currentMSecsSinceEpoch(); m_heartbeatTimer.start(); m_checkTimer.start(); }断开连接
void HeartbeatClient::onDisconnected() { qDebug() << "TCP disconnected"; m_heartbeatTimer.stop(); m_checkTimer.stop(); }发送心跳
void HeartbeatClient::sendHeartbeat() { if (m_socket->state() != QAbstractSocket::ConnectedState) return; m_socket->write("PING\n"); m_socket->flush(); qDebug() << "send PING"; }收到数据
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; } } }超时检测(最关键)
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即可:
void ClientConnection::onReadyRead() { while (m_socket->canReadLine()) { QByteArray data = m_socket->readLine().trimmed(); if (data == "PING") { m_socket->write("PONG\n"); } else { handleBusinessMessage(data); } } }整体流程:
六、这里有一个非常容易犯的错误
很多代码会这么写:
if (socket->bytesAvailable() == 0) { // 对方挂了 }这是错误的。
没有数据 ≠ TCP 断开。
TCP 是一个字节流协议。
对方可能只是:
5 秒没发送业务数据并不代表:
对方已经死亡所以必须使用:
PING → PONG这种应用层机制主动确认。
九、最后总结一下
Qt TCP 心跳真正要解决的不是:
“每隔几秒发送一个字符串。”
而是:
如何判断一个 TCP 长连接是否仍然具备业务可用性。
推荐的基本方案:
心跳周期:5s 超时时间:15s 协议:PING / PONG核心逻辑:
每 5 秒 │ ▼ 发送 PING │ ▼ 等待 PONG │ ▼ 更新 lastReceive │ ▼ 是否超过 15 秒? / \ 否 是 │ │ ▼ ▼ 正常 断开连接 │ ▼ 重连而在真正的工业软件、IM、设备通信、车载系统中,还应该继续考虑:
消息长度协议
心跳与业务消息复用
超时次数
网络抖动
自动重连
指数退避
连接状态机
线程安全
半连接检测
服务端多客户端心跳管理
一句话记住:
TCP 负责“可靠传输”,心跳负责“确认对方还活着”,重连负责“连接死了之后怎么办”。
这三件事情不要混在一起设计,TCP 长连接才会真正稳定。