ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32以太网传感器固件重构:三态状态机实现数据可靠传输与断网补报

STM32以太网传感器固件重构:三态状态机实现数据可靠传输与断网补报 1. 一次被网络故障逼出来的重构从串行轮到三态状态机大概两年前我做了一款用于机房监控的以太网温湿度传感器。方案本身没什么新鲜的STM32F103通过I2C读SHT30数据用lwIP走TCP上报到局域网里的监控平台。当时我的想法很简单这就是个读传感器加发网络包的活难点最多在协议栈配置上。直到设备在客户现场稳定跑了一周之后各种各样的网络幺蛾子开始冒出来我才意识到固件里最考验人的根本不是传感器也不是以太网链路而是网络不稳的时候数据到底怎么保住、保多少、什么时候能补回来。服务器偶尔重启、网线被人误拔、交换机端口闪断任何一个故障都会直接打击我上一版那个采集完立即上报的固件。那版固件逻辑非常朴素while大循环里顺序执行读传感器、组包、TCP发送三步看起来干净利落。可网络一旦抖动整个循环就卡在TCP发送环节温湿度采样周期从设定的5秒漂移成几十秒服务端恢复之后固件只发最新一条数据中间那几分钟甚至十几分钟的采样点全部消失。客户不关心你的网络拓扑里谁出了故障他只看到监控平台曲线里出现了一段断崖然后一句你的设备不行就把你的整个方案打上问号。重构这套固件时我给自己定的核心目标就一句话把采集、存储、上报三个动作彻底解耦让网络故障不再拖累采集让采样数据在极端条件下也不丢。最终落地的是一个三态状态机架构设备已经连续稳定运行超过一年。这篇文章把这个过程中踩过的坑、验证过的细节、最后沉淀下来的代码骨架完整记录下来给正在用STM32、GD32这类MCU做网络传感器的同行一个可直接参考的样本。1.1 旧固件为什么撑不住网络故障先复盘一下旧固件失败的具体机制。它几乎是新手做联网传感器最典型的写法一个while(1)大循环循环里依次完成阻塞读I2C、拼接数据帧、TCP发送。从代码结构看逻辑很顺三步一个周期但它有三个非常致命的问题。问题一I2C读取不是零耗时。SHT30这类数字温湿度传感器的单次读取通常在几毫秒到十几毫秒前提是总线上一切正常。可一旦线序接触不良或者总线上有其他设备抢答驱动里的超时重试机制会把一次读取拉长到几百毫秒采样周期立刻失真。问题二TCP发送的耗时完全不可控。lwIP虽然是事件驱动的协议栈但用socket API编写时connect和send都可能因为对端无响应而长时间阻塞。客户那台监控平台偶尔会重启升级重启过程持续几分钟甚至十几分钟期间TCP连接根本建立不起来。而旧固件的处理方式是不断重试整个while循环全部卡死在网络部分新一轮I2C采样完全停止数据连续性从偶尔断一条恶化成整段空白。问题三也是最要命的数据没有任何落地保护。旧固件不保存历史数据每次采样完成直接组包发送发送失败就丢弃。即使网络恢复也只能上报最新的一条中间积压的采样点全部丢失。平台端看到的是一条断开的曲线而设备端没有留下任何可以追溯的记录。客户来投诉的时候我连这段时间设备其实采到了数据、只是没发出来的辩解都拿不出实据。我当时总结下来这套方案的病根就是三个字串行化。采集、存储、上报三个环节彼此强依赖前一个环节的故障会被直接放大到后一个环节。要治本只能把三者的执行节奏彻底拆开。1.2 三态状态机的基本设计逻辑重构后的架构不再用一个串行的大循环包办所有事情而是把固件的工作划分成三个独立状态来管理采集态、存储态、上报态。采集态负责按固定周期读取传感器、做数据滤波和合理性判断、为每条数据打上时间戳生成一条完整的数据记录。存储态作为采集和上报之间的数据中转站。新采集到记录先写入内存环形缓冲区同时按策略写入外部Flash上报成功的记录再从缓冲区中清除。上报态只在有数据待发且网络可用时运行负责从存储区批量取数据、按协议发给服务器、等待服务器返回确认再决定是否重发或清除。严格来说采集状态机和上报状态机是两个并行运行的有限状态机存储态更像是一个由两者共享的数据服务。没有把它们合并成一个采集—存储—上报独热码大状态机是因为这三个动作的时间诉求完全不同采集要求准时存储要求安全上报要求可靠。把三者放进同一套状态流转里等于把网络故障的低概率问题放大成采集故障的确定性问题这是我绝对不想再看到的。有了这个总体框架下面每个状态的具体实现、边界处理和踩坑细节我按实际开发顺序展开讲。2. 采集态让每个采样点都稳定、可信且带着时间戳2.1 传感器驱动把读一次拆成两个非阻塞状态采集状态机的运行节奏用一个周期标志驱动。定时器每5秒置一次采集心跳标志状态机轮询到这个标志才从IDLE进入读取流程。这里最关键的决策是整个读取过程里没有任何delay阻塞而是把一次I2C读取拆成TRIGGER和READ_POLL两个子状态。TRIGGER状态做的事是向传感器发送测量启动命令这个操作本身只需要几微秒完成后立刻返回主循环READ_POLL状态则每隔几个毫秒检查一次I2C是否完成、DMA搬运是否结束。SHT30从触发到数据就绪通常只要几毫秒但把等待拆成多次短轮询之后整个采集流程对主循环的占用时间被压缩到几十微秒量级。网络协议栈的报文处理、按键扫描、LED心跳再也不会因为传感器读取而被长时间卡住。选择SHT30没有太多纠结I2C接口接线简单驱动代码成熟精度湿度±2%RH、温度±0.3℃对机房、仓库、温室这类场景完全够用。类似可选的还有AHT20、HDC1080逻辑都差不多。唯一要提醒的是拿到任何一颗传感器第一天就应当用逻辑分析仪或者示波器确认时序波形确认测量完成标志位在真实总线上可靠不要只对着数据手册上的最大转换时间写一个想当然的超时等待。时序验证不到位后面所有状态机的稳定都无从谈起。2.2 滤波与异常值识别别让偶发坏数污染整段曲线SHT30的原始读数直接用会遇到两类问题。第一类是噪声毛刺。机房空调压缩机启停、开关门造成的气流扰动让湿度数据频繁出现小幅跳变。我在采集状态机里做了一阶滞后滤波filtered filtered 0.2f * (raw - filtered);这个滤波器实现极简、不占用额外内存但能有效压掉高频抖动。系数0.2在5秒采样周期下对应的时间常数大约25秒不会让温度变化看起来太迟钝又能把短促的毛刺削平。实测下来湿度曲线比原始读数平滑得多监控平台上的图表观感提升非常明显。第二类是偶发异常值。I2C在MCU中断频繁时时序偶尔会被破坏SHT30可能返回温度85℃、湿度-5%这种明显非法的组合。我在写入存储之前加了一轮合理性判断温度范围限定在-40到85℃湿度范围限定在0到100%同时相邻两次读数的跳变幅度不能超过物理可能的上限。比如5秒内温度跳变超过20℃直接判定异常。超限的记录丢弃本次采集进入ERROR_WAIT等下一个采样周期再继续。连续3次采集失败则置一个告警标志随数据帧的alarm_flag字段上报让平台端知道现场传感器可能出了问题。这套逻辑把偶发坏数污染整段曲线的概率降到了几乎为零。2.3 时间戳最容易忽略却又决定补报价值的设计一条温湿度记录如果只有温度和湿度两个字段那它只适合实时上传、服务器收到时打戳这种最理想的应用。但在我这个需要断网补报的方案里记录必须自带数据产生时刻补报才有意义。如果断网期间缓存了20分钟数据恢复后一股脑发给服务器服务器却不知道这些数据各自是什么时候采集的只能按收到时刻打戳整段曲线就全部错位。设备没有外部RTC芯片局域网里也没有NTP服务器时间戳的来源需要自己设计。我的方案是TCP连接建立后服务器在握手阶段下发一次Unix时间戳固件记录本地时钟与服务器时间之间的初始偏差之后靠系统tick维持走时。单次校时的误差主要来自晶振漂移24小时累计大约1到2秒对温湿度监控场景完全够用。如果以后部署的设备需要跨地域严格同步可以把校时改成周期性的每24小时校一次误差能进一步压缩。特别注意一点时间戳必须在采集态写入记录时固化而不是在发送时才填充。因为一条记录可能在缓冲区里待几十分钟如果发送时才打时间戳所有缓存记录的时间会全部变成发送时刻失去了历史数据的意义。3. 存储态内存环形缓冲加Flash兜底的双层结构3.1 为什么不能采到就立刻上报把存储作为独立的一态来设计是因为网络故障是常态而不是异常。如果采集到的数据不落地那么上报失败等同于数据失踪。存储态在这个架构里承担两个职责短期缓冲给上报模块提供待发数据长期保存防止设备掉电后历史数据全部蒸发。第一层存储是内存环形缓冲区。结构如下#define SENSOR_BUF_CAPACITY 96 typedef struct { sensor_record_t records[SENSOR_BUF_CAPACITY]; uint16_t head; uint16_t tail; uint16_t count; } ring_buffer_t;head是写入指针由采集状态机使用tail是读取和删除指针由上报状态机使用。单MCU轮询架构下两个状态机不会在同一时刻运行所以不需要加锁。缓冲区容量定96条对应5秒采样周期下的8分钟能覆盖绝大多数短暂网络故障。缓冲区写满时新数据覆盖最旧数据因为对实时监控来说最近的数据价值更高而较旧的数据如果还没报出去Flash里通常还留有备份。3.2 Flash存储轮流扇区策略与页写入时机第二层存储是外部SPI Flash我选的是W25Q16容量16Mbit用来保存未上报记录的深后备。选择外部Flash而不是MCU内部Flash是因为内部Flash频繁擦写会影响程序存储区可靠性而且容量和寿命都不适合做数据日志区。W25Q16有512个4KB扇区我划出8个扇区共32KB作为温湿度历史区。数据量方面5秒一条记录、一天就是17280条全存不现实所以策略是尽量保留未上报记录上报成功后及时清除。Flash写入按页进行每页256字节能放8条记录一条记录32字节固定大小页内不跨页这样即便写到一半掉电最多丢当前页的最后几条之前的数据完好。这里有一个关键的工程细节W25Q16的页写入时间大概在1ms以内但扇区擦除可能需要几百毫秒绝对不能放在中断或者采集状态机的主路径上执行。我的做法是采集状态机攒够8条记录后只把这一页需要落Flash的事件挂到任务标志位由主循环在空闲时段调Flash写入模块执行。这样即使Flash正在擦除采集也不会被阻塞三态之间的独立性能保持住。磨损均衡方面8个扇区轮流写写满一个就切到下一个回卷时覆盖最旧的扇区。每个扇区头部写一个带CRC的扇区头标记记录当前写入位置和扇区序号。考虑到温湿度数据在正常场景下补报迅速Flash擦写次数不会增长太快这套简化版轮流策略已经足够可靠。3.3 记录帧格式固定32字节字段布局必须一次想清楚所有存入Flash和上报到服务器的记录使用同一个固定32字节结构#pragma pack(1) typedef struct { uint8_t frame_header; // 0xAA uint8_t device_id; // 设备编号 uint32_t timestamp; // Unix时间戳秒 int16_t temperature; // 温度乘10存储 uint16_t humidity; // 湿度乘10存储 uint8_t alarm_flag; // 告警标志 uint8_t reserve[3]; // 预留字节写入前必须清零 uint16_t crc16; // CRC16-CCITT } sensor_record_t; #pragma pack()这个字段布局有几个值得说道的地方。温度用int16乘10存储湿度用uint16乘10存储是为了避开浮点数在Flash和网络传输里的字节序、对齐填充问题。0.1℃精度对温湿度监控足够了整数运算在MCU上也更快。CRC16覆盖从frame_header到reserve的整段数据用查表法计算CPU开销可以忽略。服务器收到数据后先校验CRC再解析坏包在源头就被拦截。另一个必须强调的点结构体要加#pragma pack(1)强制单字节对齐。如果不加在默认对齐规则下这个结构体实际占40字节而代码里按32字节写入Flash、按32字节组网络包服务端按32字节解析结果就是帧头对得上、后面的字段全部错位。这个坑我真实踩过会让你在联调时怀疑人生。另一个字段设计上的细节是reserve预留字节字段在写入前必须用memset清零整个结构体否则Flash擦除态0xFF会残留给后续版本兼容性埋雷。这个问题的具体后果后面踩坑部分细说。3.4 掉电重启后如何恢复待上报数据Flash层设计的最终目的是掉电重启后数据不丢。固件上电初始化时会扫描这8个扇区的扇区头定位当前有效扇区然后从扇区的最后一条有效帧向前追溯把未上报记录全部搬回内存待上报队列。完成之后上报状态机才被允许启动。实际操作中有一个必须处理的意外情况掉电瞬间如果正好处于擦除或写入动作中间扇区头可能不完整。解决办法是扇区头本身带CRC启动扫描时如果发现CRC校验失败就放弃当前扇区、回退到上一个扇区继续寻找有效数据。温湿度数据连续性强少个几页不至于伤筋动骨但保住绝大多数数据对客户来说非常重要。这个恢复流程在上电时要保证时序正确不能在待上报队列还没有完全恢复时就允许上报状态机运行否则会出现先发了一部分、另一部分还没挂上队列的并发错乱。4. 上报态真正和网络搏斗的战场4.1 确认机制服务器说收到了才算收到写设备上报时很多人引用TCP是可靠传输这句话然后把send函数返回值当作数据到达服务器的依据。这是误解。TCP的send成功只代表数据进入了协议栈的发送队列服务器应用层是否真的收到了、写库了TCP是不负责的。尤其是设备到服务器之间还隔着交换机、路由器这些中间节点应用层ACK必不可少。这里的协议设计其实很简单客户端发一批记录服务器在同一TCP连接里回一个确认帧typedef struct { uint8_t ack_header; // 0x55 uint8_t device_id; uint16_t ack_count; // 被确认的记录条数 uint32_t last_timestamp; // 最后一条确认记录的时间戳 uint16_t crc16; } ack_frame_t;固件只有在收到ACK之后才会删除内存缓冲区和Flash中已被确认的记录。ACK里带last_timestamp而不是单纯靠ack_count计数是为了精确对齐服务器确认到哪一条。如果上一批已经确认过的记录因为网络原因又重发了一次靠计数就会出现偏差靠时间戳能准确定位边界。4.2 批量上报和指数退避重传上报状态机的完整流转逻辑如下状态在IDLE、CONNECTING、SEND_BATCH、WAIT_ACK、ACK_CLEAN之间切换。IDLE状态下检查待上报队列空则保持空闲有数据时进入CONNECTING非阻塞等待TCP连接建立连接成功后在SEND_BATCH一次发送最多30条记录然后进入WAIT_ACK等待服务器确认同时启动超时计时收到ACK后在ACK_CLEAN阶段按last_timestamp清理缓冲区回到IDLE。如果WAIT_ACK超时状态回到CONNECTING重连并进入指数退避。重传退避的参数是第一次失败等2秒第二次4秒第三次8秒最高封顶30秒。退避的目的不是拖延而是防止在网络不稳定或者服务器高负荷时设备像无头苍蝇一样高频重试反而进一步加重链路拥塞。退避期间采集和存储照常运行所以最坏情况下只是补报延迟数据一条都不会丢。批量上报本身也值得展开。一次TCP连接处理30条记录和30次连接各处理1条相比省掉的是连接建立和拆除的开销更关键的是减少了ACK的往返次数整体吞吐量完全不在一个量级。批次数定为30是因为单包大小控制在1KB以内不容易触发lwIP的分片和滑动窗口的边界问题。4.3 幂等设计ACK丢了也不能产生重复数据网络环境里最阴险的场景是固件发出去的一批记录服务器其实已经收到并且写库了但服务器回给固件的ACK在链路中途丢失。固件超时后会重发同一批记录如果服务器不做幂等设计库里就会出现一整批重复数据后面画曲线、算均值全乱套。所以协议从第一天设计时就要假设两件事ACK会丢数据会被重发。对应到服务器端落库逻辑必须以(device_id timestamp 随机序号)作为唯一键写入前先查唯一索引已存在则跳过但依然回复ACK。这样固件无论重发多少次最终库里只有一份数据。随机序号的加入是吸取了教训因为纯timestamp可能在同一秒内有两条记录仅靠设备ID加时间戳解析去重会误杀真数据这个坑后面第四节详细展开。这个幂等设计的代价是服务器端多一条唯一索引查询但对一周几十万条级别的监控数据来说完全不构成瓶颈。反过来讲如果固件端坚持收到ACK才删、没收到就重发那么服务器端无论如何都必须做到同样数据的重复写入不产生重复记录。这是TCP不可靠链路上构建应用层可靠传输的基本功。5. 表驱动状态机的具体实现骨架5.1 采集状态机查表执行而不是switch-case满天飞状态机的实现我选了表驱动而不是switch-case。理由不是追求炫技而是表的结构天然把当前状态、处理函数、超时后的下一状态绑定在一起。新增一个状态只需要往表里加一行不需要修改N个case分支表是静态只读数据放在Flash里不占RAM调试时打印状态编号也比打印调用栈容易得多。采集状态机的状态表和驱动函数如下typedef enum { COLLECT_IDLE, COLLECT_TRIGGER, COLLECT_READ_POLL, COLLECT_VALIDATE, COLLECT_WRITE_BUF, COLLECT_ERROR_WAIT, COLLECT_STATE_MAX } collect_state_t; typedef struct { collect_state_t state; void (*process)(void); collect_state_t timeout_state; } collect_state_table_t; static const collect_state_table_t collect_table[COLLECT_STATE_MAX] { { COLLECT_IDLE, collect_idle_process, COLLECT_IDLE }, { COLLECT_TRIGGER, collect_trigger_process, COLLECT_ERROR_WAIT }, { COLLECT_READ_POLL, collect_read_poll_process, COLLECT_ERROR_WAIT }, { COLLECT_VALIDATE, collect_validate_process, COLLECT_IDLE }, { COLLECT_WRITE_BUF, collect_write_buf_process, COLLECT_IDLE }, { COLLECT_ERROR_WAIT, collect_error_wait_process, COLLECT_IDLE }, }; void collect_run(void) { collect_state_table_t *entry collect_table[g_collect_state]; entry-process(); if (collect_has_timeout()) { g_collect_state entry-timeout_state; } }每个process函数都遵循一句原则能干多少干多少干不完就返回。COLLECT_TRIGGER的process只是发起I2C读取请求设置超时计数立刻返回COLLECT_READ_POLL的process检查读取完成标志没完成就直接返回由主循环下一轮再次进入同一个状态。这种每轮只做一小步的写法是整个三态架构能在单个循环里并行运转的前提。如果任何一个process函数里出现忙等三态并行立刻崩成串行前功尽弃。5.2 上报状态机的驱动来源主循环加网络回调上报状态机同样是表驱动但它的驱动来源有两个一个是主循环的周期调用处理超时计时和退避逻辑另一个是lwIP的网络回调比如连接成功回调、可写回调、数据接收回调。我尽量让网络回调里的工作越少越好只设置状态机需要的标志位或保存数据指针真正的数据解析和状态转移放到主循环的上报状态机里做。lwIP在单MCU场景下没有多线程所有回调都在同一个上下文里执行天然没有锁竞争但回调函数里绝对禁止做耗时操作比如Flash擦除、长字符串处理。这是嵌入式网络开发的基本纪律。另一个容易踩坑的点是tcp_write的返回值处理这个细节在踩坑记录里专门讲。5.3 状态机边界条件必须显式处理的四个场景状态机的正常路径写完只算完成了一半真正体现设计水平的是边界条件。我在这个项目里反复被边界问题教训整理出四个必须在编码阶段就处理的点第一采集超时。I2C读取发起后50ms内没有完成强制超时并将状态置回COLLECT_IDLE。宁可少采一条也不能让整个采集状态机卡死在等待上因为卡住一次后面所有采样点都会跟着偏移。第二上报队列为空时不能发起TCP连接。有些新写的代码在SEND_BATCH之后立刻进入WAIT_ACK完全不检查缓冲区的count网络一恢复就发出大量空连接白消耗资源不说还会让服务器日志里堆满无意义的连接记录。第三Flash待上报区回卷时的覆盖策略。要保证上报成功的记录能及时从Flash中清除腾出空间否则长时间断网后新数据会覆盖掉尚未上报的旧数据等于丢了历史。第四上电恢复顺序。Flash扫描和待上报队列初始化必须在进入上报状态机之前完成。如果上报先启动可能覆盖尚未恢复的数据或者把队列状态搞成半个满的尴尬局面。6. 一年运行数据与三个必须写下来的坑6.1 实测参数99.7%上报成功率和100%数据连续率设备在客户机房已经连续运行超过一年我有几个统计数字可以分享。采样周期5秒长期运行无漂移。上报成功率按批次统计约99.7%剩余0.3%主要是服务器主动维护窗口期间的失败重试。数据连续率按采样点统计100%未出现采集缺口。单批次端到端上报延迟正常情况50ms以内收到ACK。服务器最长的维护停机时间8分钟期间设备把96条采样点压在内存缓冲恢复后2秒内一次性补报完成。掉电模拟场景也验证过Flash兜底。拔掉网线70分钟再插回期间记录全部落入Flash网络恢复后分3批补报每批间隔约5秒退避服务端最终落库数与设备端本地记录完全一致按唯一键去重后无重复、无缺失。这些数据让客户很满意也让团队在后面的项目中把采集、存储、上报三态解耦定为了标准架构。6.2 坑一lwIP内核发送缓冲区满导致发送卡死早期版本的上报模块我在SEND_BATCH状态里直接调用tcp_write后接tcp_output认为只要协议栈初始化正确数据总能发出去。直到某个客户现场出现设备间歇性死机排查了很久才发现lwIP内核的发送缓冲区是有限的当发送窗口变小且缓冲区满时tcp_write返回ERR_MEM。我的第一版代码对这个返回值的处理是立即重试结果在高负载下陷入死循环整个设备主循环被卡死在重试里看起来和死机一模一样。正确做法是tcp_write返回ERR_MEM时把要发送的payload挂到应用层发送队列注册tcp_sent回调。内核腾出空间后tcp_sent触发再从队列里取出payload继续发送。发送队列和上报状态机之间的同步也要处理好避免出现底层已经发出去、应用层队列还在等待重传的重复发送问题。这部分代码虽然多花了一点调试时间但之后一年多再也没有出现过发送卡死。6.3 坑二Flash擦除态0xFF与新老结构体不一致有一段时间设备升级固件后补报的历史数据时间戳大面积异常。排查方向从CRC一路转到时间戳字节序最后发现根因让人哭笑不得Flash擦除后的默认状态是0xFF而新版record结构体里预留字段reserve在老固件中没有被显式初始化残留的0xFF导致新旧固件对同一块Flash区域的解析不一致。只要一升级固件老固件写入的未上报记录中reserve字段全为0xFF新版固件收到这些记录后某些逻辑会产生误判。修复方式有两个方向。一是在record结构体里增加结构版本号字段升级时检测到版本变化就强制清空待上报区重新开始采集稳妥但是会丢掉升级前未上报的历史数据。二是统一在写入前用memset把整个struct清零再填充字段我后来两条都做了双保险。这个坑表面看是字段初始化疏漏本质上是Flash擦除态和应用层默认值的隐性冲突写Flash存储的固件尤其容易中招。6.4 坑三ACK在交换机里排队引发的重复数据这个坑是在客户现场暴露的。某客户的交换机开启了队列缓冲业务包和ACK包在某个方向上的队列里互相挤压固件发出一批数据后迟迟等不到ACK直到超时重发。服务器端当时做去重用的唯一键是device_id加timestamp理论上没什么问题但实现时timestamp精度是秒而同一个批次里恰好有两条记录落在同一秒内于是这两条被误判为重复其中一条真数据被丢弃。教训有两条。去重主键必须保证即使在极端情况下也不会碰撞单纯device_id加秒级时间戳在批量上报场景下不够安全加入随机序号后问题彻底消失。服务器端去重逻辑一定要写测试用例覆盖同一批次内多条记录时间戳相同的边界场景别想当然认为这不会发生。这个坑让我重新审视了所有跨设备的唯一性设计——只要存在重发机制去重主键就必须假设会碰撞。做这个项目一年多最深的体会是所谓采集、存储、上报三态本质上是在为一句话服务——把网络不可靠这个外部风险用固件内部的结构化解掉。三态边界划得越清晰网络该闹的故障让它闹设备该采的数据照样采互相不拖累。这个思路不只适用于以太网温湿度传感器任何带网络通信的记录类设备哪怕以后换Wi-Fi、换4G模块核心框架都可以复用。如果你也在做类似的东西建议动手写代码之前先把存储态的恢复流程和上报态的确认协议想透这两个点是数据连续性的根基。状态机和协议栈的坑踩过一次就能长记性但数据连续性的口碑丢失一次要很长时间才能补回来。
返回列表