别让心跳把连接"越检测越死"
做 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 长连接才会真正稳定。