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