断网时数据必须缓存到本地:检测send()失败后切换缓存模式,用deque+定期刷盘存储带seq_id和ts_ms的序列化数据;网络恢复需连续3次心跳成功;补发须保序防重放,每条封装固定帧头并等待ACK;新数据插入队列尾部;用epoll驱动非阻塞发送。

断网时数据必须缓存到本地,不能丢
网络中断期间,send() 或 write() 会立即失败(返回 -1,errno 通常是 ECONNRESET、EPIPE 或 ENOTCONN),此时若直接丢弃待发数据,补发就无从谈起。关键动作是:检测失败 → 切换为缓存模式 → 把原始数据(含时间戳、序号、业务类型)序列化后写入本地临时存储。
推荐用轻量级方案:std::deque<:vector>></:vector> 做内存队列 + 定期刷盘到文件(如每 100 条或 500KB 写一次 fstream)。避免用 SQLite 等重型方案——补发逻辑本身要快,数据库 I/O 可能成瓶颈。
- 每条缓存数据必须带唯一递增序号(
seq_id)和生成时间(ts_ms),用于后续校验与去重 - 缓存文件建议按日期分片(如
cache_20240520.bin),便于清理过期数据 - 程序启动时需扫描缓存目录,按文件名时间排序加载,再按
seq_id排序重入队列
如何可靠检测“网络已恢复”
靠 ping 或 connect() 成功一次就发包,极易误判——TCP 连接可能建连成功但中间链路仍抖动,导致后续 send() 又失败。真正可用的判断逻辑是:连续三次心跳包(小数据)成功发出且收到 ACK,并在 1s 内完成。
具体做法:维护一个独立的心跳 socket(非主业务 socket),每 2s 发一个 4 字节 "PING",超时设为 500ms;仅当连续 3 次 recv() 返回 4 且内容为 "PONG",才标记网络就绪。这个状态要全局共享,避免多个线程重复试探。
立即学习“C++免费学习笔记(深入)”;
- 不要复用业务 socket 做心跳——业务连接可能处于
TIME_WAIT或半开状态,干扰判断 - 心跳服务端必须有明确
PONG回复,不能只靠 TCP ACK,否则无法区分“链路通”和“对端进程挂了” - 检测通过后,先清空发送缓冲区(
setsockopt(..., SO_SNDBUF, ...)不起作用,得用shutdown(SHUT_WR)+ 重连)
补发必须严格保序且防重放
补发不是把缓存一股脑 send() 出去——TCP 层不保证应用层消息边界,接收端若没做帧解析,会粘包错序。正确做法是:每条历史数据封装为固定头 + 变长体格式(头含 seq_id、len、crc32),每次只发一条,等对端确认(ACK 帧含已收最大 seq_id)后再发下一条。
ACK 机制可简化:接收端收到完整帧后,立刻回一个 8 字节 ACK(前 4 字节为 seq_id,后 4 字节为 0x00000000 表示成功)。发送端收到 ACK 后,从缓存队列 pop 前 N 条(N=已确认最大序号 - 当前最小序号 + 1),再推进补发指针。
- 若某条发出去 3 秒没回 ACK,重发该条(不是重发整个队列),最多重试 3 次后丢弃并告警
- 接收端必须检查
seq_id是否 > 已处理最大序号,否则直接丢弃(防止网络延迟导致旧包晚到) - 补发过程中新产生的实时数据,插入到缓存队列尾部,不插队——保证历史数据优先发完
Linux 下用 epoll 实现非阻塞补发调度
补发是 I/O 密集型任务,若用 sleep(1) 轮询发包,既浪费 CPU 又不准时。更合理的是让 epoll 管理 socket 可写事件,只在 socket 真正 ready 时才取一条缓存发出去。
核心逻辑:初始化时 epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) 注册 EPOLLOUT;每次发包前检查缓存是否为空;发完或部分发送(send() 返回 bytes_sent )后,保持监听;若 <code>send() 返回 -1 且 errno == EAGAIN 或 EWOULDBLOCK,继续等待下一次 epoll_wait() 通知。
- 务必把 socket 设为非阻塞(
fcntl(sockfd, F_SETFL, O_NONBLOCK)),否则send()可能卡死 - 不要在
epoll_wait()返回后一次性发多条——TCP 发送窗口有限,强行多发反而触发EAGAIN更频繁 - 补发线程和业务线程间共享缓存队列,需用
std::mutex+std::condition_variable同步,但锁粒度要细(只锁 push/pop,不锁 send)
最易被忽略的是缓存持久化与程序崩溃的兼容性:如果进程在写缓存文件中途崩溃,会导致文件损坏、后续加载失败。必须用“写临时文件 + rename() 原子提交”模式,且每次写入前先 fseek() 到末尾,避免覆盖已有数据。


















