心跳包应采用固定长度纯文本(如“ping”)或二进制头(如1字节类型+3字节保留),服务端直接返回固定“pong”,避免json等复杂序列化,确保轻量、快速识别、无状态响应,防止与业务数据耦合或粘包误判。

心跳包该用什么数据格式发
TCP本身不提供应用层心跳机制,所以得自己定义心跳消息。别用复杂序列化(比如Protobuf),长连接心跳的核心诉求是轻量、可快速识别、服务端能无状态响应。最稳妥的是固定长度纯文本或二进制头:"PING"(4字节)或 "\x01\x00\x00\x00"(1字节类型+3字节保留)。服务端收到直接回"PONG",不解析业务字段,避免心跳逻辑和业务耦合。
常见错误是把JSON当心跳体——单次心跳带200字节JSON,每30秒发一次,连接数一多,光心跳就吃掉几MB/s内存和CPU;更糟的是服务端反序列化失败导致心跳判定超时,误杀健康连接。
客户端怎么检测连接是否断开
TCP连接断开后,send() 通常不会立刻报错,要等下一次写操作或启用SO_KEEPALIVE。但系统级keepalive默认2小时才探测,完全不能满足业务心跳需求(一般要求30秒内发现异常)。必须自己实现读超时+写保活:
- 用
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))设接收超时(比如5秒),每次recv()返回-1且errno == EAGAIN || errno == EWOULDBLOCK不算错;但若超时后仍收不到"PONG",就认为连接失效 - 心跳发送不能依赖定时器唤醒再
send()——如果上一个心跳还没发完(比如网络卡顿),定时器又触发,容易堆积write调用,甚至触发EWOULDBLOCK;正确做法是把心跳请求放入发送缓冲区队列,由主IO循环统一处理 - 别只靠
send()返回值判断:成功返回仅表示数据进了内核发送队列,不代表对方收到;真正可靠的是“发了PING → 必须在超时窗口内收到PONG”,否则主动close()
如何避免心跳干扰业务数据收发
业务协议和心跳协议必须分离,否则解析器可能把"PING"当成非法业务包丢弃,或把业务数据误判为心跳。典型方案是分层设计:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
在应用层协议头部加1字节type字段:0x01表示业务数据,0x02表示心跳。收包时先读首字节,再决定走业务解包流程还是直接回"PONG"。这样即使心跳包夹在业务包中间(TCP流式传输导致),也能准确识别。
另一个坑是使用阻塞socket做心跳:一旦业务recv()卡住,心跳定时器到了也发不出去,结果服务端因收不到心跳主动断连。必须用非阻塞socket + select()/poll()/epoll()统一调度读写事件。
服务端怎么高效响应大量心跳
服务端心跳响应必须零拷贝、无锁、不分配堆内存。不要为每个心跳new一个string再send(),高频连接下GC压力或malloc争用会拖垮性能。推荐做法:
- 预分配一个全局常量
const char PONG_MSG[] = "PONG",响应时直接send(fd, PONG_MSG, 4, MSG_NOSIGNAL) - 禁用
SIGPIPE信号(加MSG_NOSIGNAL标志),否则对端突然断连时,send()触发SIGPIPE会导致进程退出 - 心跳请求到达时不更新任何连接状态时间戳——那是客户端的事;服务端只做最简反射动作。连接活跃性由客户端上报的最后心跳时间决定,服务端定期扫描过期连接即可
真正难的是心跳与业务共用同一个socket时的粘包处理:如果业务协议没定长/分隔符,而心跳又是不定长字符串,就可能出现"PIN" + "Gxxx"被拆成两包,导致首包无法识别。这时候必须强制心跳用固定长度(如4字节)或加校验字节,不能图省事用裸字符串。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










