输入关键词开始搜索

心跳机制 — 原理、实现与面试考点

心跳机制 — 原理、实现与面试考点

基于 PulseQt T018 实战,覆盖协议设计、代码实现、跨场景应用、面试八股。


一、为什么需要心跳

TCP 连接的”假活”问题:

客户端 ──── TCP 连接 ──── 服务端

    双方都认为连接还在,但中间网络已经断了

原因:TCP 没有应用层数据时,keepalive 默认 2 小时才探测一次
结果:断线后 2 小时内双方都感知不到 → 数据丢失、资源泄漏

心跳 = 应用层定期发空包,“我还活着,你还活着吗?“


二、PulseQt 心跳实现

协议帧定义

心跳请求帧(上位机→下位机):
  A5 5A 00 02 [CRC16]
  头   长=0 类型

心跳应答帧(下位机→上位机):
  A5 5A 00 03 [CRC16]
  头   长=0 类型

核心代码

定时检查(ParseWorker::onHeartbeatCheck)

void ParseWorker::onHeartbeatCheck()
{
    qint64 elapsed = QDateTime::currentMSecsSinceEpoch() - m_lastDataTime;

    // 5s 无数据 → 发心跳
    if (elapsed >= 5000) {
        emit writeData(buildFrame(Frame::TYPE_HEARTBEAT));
        m_heartbeatMissed++;
    }

    // 连续 6 次无应答 (30s) → 超时
    if (m_heartbeatMissed >= 6) {
        m_heartbeatTimer->stop();
        qWarning() << "Heartbeat timeout (30s)";
    }
}

收到数据时重置

void ParseWorker::onRawDataReceived(const QByteArray &data)
{
    m_lastDataTime = QDateTime::currentMSecsSinceEpoch();  // 任何数据都算"活动"
    m_heartbeatMissed = 0;
    m_decoder.feed(data);
}

收到心跳应答时重置计数器

// frameDecoded lambda 中
if (frame.type == Frame::TYPE_ACK) {
    m_heartbeatMissed = 0;
    return;
}

收到对方心跳时回应

if (frame.type == Frame::TYPE_HEARTBEAT) {
    emit writeData(buildFrame(Frame::TYPE_ACK));
    return;
}

状态机

        收到任意数据
  ┌─────────────────────────────┐
  │                             │
  ▼                             │
 [正常] ──5s无数据──→ [发心跳] ──→ missed++
  │                    │
  │        收到ACK      │ missed < 6
  └──────────────────────┘

  │ missed >= 6 (30s)

 [超时] → 停止心跳 → 上层断线重连

三、三种常见心跳模式

模式 1:Ping-Pong(PulseQt 采用)

A ── Ping ──→ B
A ←─ Pong ── B
优点缺点
双向确认,A 知道自己发出的探测 B 收到了需要 B 配合实现应答

适用:自定义协议、物联网设备、长连接服务器。

模式 2:单向心跳

A ── Ping ──→ B  (B 不回应)
A 只管发,B 只管收。B 端有超时检测。
优点缺点
B 不需要实现应答逻辑A 不知道 B 是否还活着

适用:单向数据上报(传感器→网关)、卫星遥测。

模式 3:TCP Keepalive(操作系统级)

int keepalive = 1;
int idle = 60;      // 60s 无数据开始探测
int interval = 10;  // 每 10s 探测一次
int count = 3;      // 3 次失败判定断开
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));
优点缺点
零应用层代码全局生效,不能按连接定制
操作系统处理默认 2 小时,改配置影响所有连接

四、设计心跳的三个关键参数

参数PulseQt 值选择理由
心跳间隔5s100Hz 数据流下 5s 已经是”空闲”了
超时阈值30s(6×5s)网络抖动 2-3 个包正常,6 个确认断线
重置条件收到任何数据都重置心跳目的是检测”对方还活着”,有数据就是活着

常见参数经验值

场景心跳间隔超时说明
物联网设备30s-60s3-5 次省电,不频繁唤醒
WebSocket30s3 次Nginx 默认 60s 超时
游戏长连接5s-10s2-3 次实时性要求高
金融行情1s2 次毫秒级感知断线
工业采集5s5-6 次平衡可靠性和带宽

五、面试常考问题

Q1:TCP 有 keepalive,为什么还要应用层心跳?

TCP keepalive:
  - 默认 2 小时才探测(Linux)
  - 改配置影响全局所有连接
  - 只能检测网络层通断,不检测应用层假死

应用层心跳:
  - 间隔可控(5s vs 2h)
  - 可按连接定制策略
  - 能检测"服务进程卡死但 TCP 还连着"的情况

Q2:心跳间隔设多大合适?

没有固定值,看业务:带宽敏感的物联网设备(NB-IoT)可能 30 分钟一次;金融行情 1 秒一次。核心公式:

心跳间隔 = 业务可容忍的最大断线感知时间 / 超时次数

PulseQt:业务要求 30s 内感知断线 → 30s / 6 次 = 5s 间隔。

Q3:心跳包应该带数据吗?

// 方案 A:纯心跳(PulseQt 采用)
// 帧 = Header + Length=0 + Type=0x02 + CRC

// 方案 B:带时间戳的心跳
// 可以计算网络延迟:收到应答的时间 - 请求中携带的发送时间 = RTT

一般用纯心跳。带宽充裕时可以带时间戳,方便监控链路质量。

Q4:心跳处理和业务逻辑应该在同一线程吗?

PulseQt 的做法(✅ 正确):
  解析线程:处理心跳(在 frameDecoded lambda 中判断 type=0x02/0x03)
  → 心跳是协议层的事,和数据帧同层处理

反例(❌ 不好):
  单独线程处理心跳 → 和业务数据线程抢锁 → 复杂度翻倍

Q5:如何防止心跳风暴?

场景:1000 台设备同时连一个服务器
如果所有设备同一时刻发心跳 → 服务器瞬间 1000 个包

解法:心跳间隔加随机抖动
  interval = 5000 + rand() % 1000;  // 5s ± 0.5s 随机

Q6:收到心跳应答后要不要重置数据超时?

。心跳应答也是”对方还活着的证据”。PulseQt 的 m_lastDataTimeonRawDataReceived 中更新——但心跳应答走的是 frameDecoded 而不是 rawDataReceived

PulseQt 的处理:心跳应答在 frameDecoded 中只重置 m_heartbeatMissed,不重置 m_lastDataTime。这意味着心跳应答只能证明对方活着,但不能阻止继续发心跳——因为”没有业务数据”这个事实没变。

这是一个合理的设计选择,面试时可以讲出这个权衡。


六、PulseQt 实现中的反向通道

心跳需要上位机主动发数据,但 T013 的数据流是单向的(设备→上位机)。解决方案:加一条反向信号线。

ParseWorker                  TcpWorker                 TCP
    │                           │                      │
    │ emit writeData(frame)     │                      │
    │──────────────────────────→│ write(frame)         │
    │   (QueuedConnection)      │──────────────────────→│ 设备

ParseWorker::writeData 信号 → TcpWorker::write 槽 → m_socket->write()。不需要修改线程模型,信号槽自动跨线程。