news 2026/10/26 5:02:02

VC6.0下Win32串口GPS数据采集与NMEA解析实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC6.0下Win32串口GPS数据采集与NMEA解析实践

简介:《VC6.0下编写的GPS数据采集程序》面向使用Visual C++ 6.0开发串口采集与定位应用的学习者,也可供GIS、车辆追踪、户外导航等课程设计或原型开发参考。内容围绕GPS接收器与PC的串口交互展开,涉及波特率等通信参数配置、NMEA报文中GPGGA/GPGLL语句的拆分与数值转换、异常数据处理,并覆盖MFC界面搭建、实时数据显示、SQLite或Access记录保存及VC6.0调试排错思路。压缩包约2.99MB,文件总数与类型明细未标注,下载后可按工程源码、界面与数据模块自行梳理;已有126人学习。对希望把串口通信、协议解析和桌面端展示串联起来的读者,这份工程可作为从通信配置到数据落库的参照,便于对照理解采集流程、定位常见问题并积累调试经验。

1. VC6.0 串口收 GPS 数据:一套旧工具为什么还值得做

一台工控机、一块多串口卡、一个装了几个文件夹的老采集软件,旁边挂着 GPS 接收模块——这是很多现场的真实样子。VC6.0 下写 GPS 数据采集程序,核心就三件事:把 COM 口按正确参数打开、把 NMEA 0183 语句一行行读进来、把解析后的时间、经纬度、速度、定位状态落成能用的数据。它不难,难在这套环境没有现代语法糖,也没有现成的串口库可以省心,任何一处资源泄漏或超时设置不当,都会在连续跑几天之后暴露出来。适合手上有旧工控机、旧采集盒、必须交付一个双击就能运行的 exe 的从业者,也适合想把串口采集这条链路彻底摸清的人。

2. 串口打开与收数:VC6.0 下最稳的一套 API 组合

2.1 为什么绕开 ActiveX 串口控件,直接用 Win32 串口 API

在 VC6.0 时代,很多人第一反应是把串口控件拖到对话框上,配几个属性,双击事件里读数据。小 Demo 这样做没问题,一旦放到现场连续跑,麻烦就来了:控件依赖注册,换机器或重装系统后经常提示类未注册;事件驱动模式下缓冲区由控件内部管理,收得快了丢字节、收得慢了界面卡;一台机器挂四个串口时,控件实例之间的消息分发更难控制。更现实的一点是,控件把超时、清错、缓冲区深度都藏起来了,出问题只能靠猜,成了彻头彻尾的黑匣子。

Win32 的串口 API 是文件句柄模型:CreateFile打开COMx,SetCommState配参数,SetCommTimeouts定超时,ReadFile/WriteFile收发,ClearCommError取错误和缓冲状态。这套接口的好处是把每个环节都摆在明面上,你可以自己决定读线程怎么阻塞、一次读多少、出错后怎么重连。代价是要自己写状态管理,代码量比拖控件多,但换来的是可排查性,这一点在连续运行场景里值回票价。

我一般把串口封装成一个极简的类,只暴露Open、Close、Read、Write、IsOpen五个方法,内部保存句柄和最后一次错误码。这样上层业务代码不用关心 COM1 还是 COM7,换口只改一个字符串。

2.2 CreateFile 到 SetCommState:一次配对的参数表

打开串口最容易翻车的地方不是语法,而是参数和模块对不上。GPS 接收模块出厂默认常见 9600 8N1,也有 38400 或 115200 的版本,数据位、校验位、停止位、流控必须和模块一致,否则读到的就是一堆乱码或者干脆零字节。先把下面这张表对着模块手册核一遍,比事后猜要省时间。

参数常见取值说明
波特率9600 / 38400 / 115200必须与模块一致,不一致表现为乱码或全零
数据位8绝大多数 NMEA 输出都是 8 位
校验位无少数模块用偶校验,需查手册
停止位1基本不用改
流控无硬件流控打开后若不接线,会一直等 CTS 导致读不到数据
读间隔超时30~100 ms决定一次ReadFile多久返回
读总超时300~800 ms防止线程永久卡死

对应的初始化代码大致如下,注意每一项都要显式赋值,DCB结构体里残留的默认值经常是坑的来源。

// 打开并配置一个 GPS 串口,失败返回 INVALID_HANDLE_VALUE HANDLE OpenGpsPort(const char* portName) { HANDLE h = CreateFile(portName, // 例如 "COM3" GENERIC_READ | GENERIC_WRITE, 0, // 串口必须独占 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, // 不要用 FILE_FLAG_OVERLAPPED NULL); if (h == INVALID_HANDLE_VALUE) return h; SetupComm(h, 4096, 2048); // 收发缓冲区,GPS 数据量不大,4K 足够 DCB dcb; memset(&dcb, 0, sizeof(dcb)); dcb.DCBlength = sizeof(dcb); if (!GetCommState(h, &dcb)) { CloseHandle(h); return INVALID_HANDLE_VALUE; } dcb.BaudRate = CBR_9600; dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT; dcb.fBinary = TRUE; dcb.fParity = FALSE; dcb.fOutxCtsFlow = FALSE; // 关掉硬件流控 dcb.fRtsControl = RTS_CONTROL_ENABLE; // 有些模块要求 RTS 保持高 dcb.fDtrControl = DTR_CONTROL_ENABLE; if (!SetCommState(h, &dcb)) { CloseHandle(h); return INVALID_HANDLE_VALUE; } COMMTIMEOUTS to; to.ReadIntervalTimeout = 50; // 两字节之间最多等 50ms to.ReadTotalTimeoutMultiplier = 0; to.ReadTotalTimeoutConstant = 500; // 单次读最长 500ms 必返回 to.WriteTotalTimeoutMultiplier = 0; to.WriteTotalTimeoutConstant = 500; SetCommTimeouts(h, &to); PurgeComm(h, PURGE_RXCLEAR | PURGE_TXCLEAR); return h; }

ReadIntervalTimeout和ReadTotalTimeoutConstant的组合决定了读线程的响应节奏。前者设成 50ms,意味着一行 NMEA 语句(约 70 字节)在 9600 波特率下大约 70ms 传完,读调用会在数据间隙或超时后返回,不会一直吊着。后者是兜底,防止模块掉电后再也不返回。这两个值不要设成 0,全 0 在某些驱动上会退化成永久阻塞,线程就再也醒不过来了。

2.3 读线程收包:ReadFile 的阻塞、超时与粘包处理

串口数据是流式的,一次ReadFile拿到的可能是半条语句,也可能是三条半。所以读线程只负责“搬字节”,不负责“认语句”:它把读到的内容原样追加到一个环形缓冲区,解析逻辑在另一个地方按行切分。这样职责清晰,串口阻塞不会影响解析,解析慢也不会反压串口。

// 读线程:只搬字节,不做协议解析 DWORD WINAPI GpsReadThread(LPVOID) { char buf[512]; DWORD got = 0; while (g_bRunning) { if (g_hCom == INVALID_HANDLE_VALUE) { Sleep(200); continue; } if (!ReadFile(g_hCom, buf, sizeof(buf), &got, NULL)) { // 拔线、模块掉电都会走到这里,交给重连逻辑 g_nFailCount++; Sleep(500); continue; } if (got == 0) continue; // 超时返回,正常现象 g_nFailCount = 0; RingAppend(buf, got); // 加锁写入环形缓冲区 } return 0; }

got == 0是超时返回,不是错误,这一点很多人第一次写会误判成掉线,结果日志里全是假的断连记录。真正的错误是ReadFile返回 FALSE,此时用GetLastError()看具体码,再配合ClearCommError把驱动的错误标志清掉,否则下一次读可能继续失败。环形缓冲区的大小按一秒的数据量估算即可:9600 波特率满打满算一秒 960 字节,NMEA 输出通常每秒一条到五条,2KB 到 8KB 的环形区足够吸收解析侧的抖动。

缓冲区写入要加锁,因为解析可能放在主线程的定时器里,也可能放在另一个线程。VC6.0 没有std::mutex,用CRITICAL_SECTION就行,开销小,够用。

3. NMEA 0183 解析:把 $GPRMC 变成能用的经纬度

3.1 一条 $GPRMC 里每个字段到底代表什么

NMEA 0183 是一套 ASCII 语句协议,一行以$开头,以*加两位十六进制校验和结尾,字段之间用逗号分隔。GPS 模块常见的输出有$GPGGA、$GPRMC、$GPGSV、$GPVTG几种,如果只想要一个能落库的定位记录,$GPRMC是信息最全的一条:它同时带 UTC 时间、定位状态、经纬度、地面速度、航向和日期。$GPGGA有定位质量指示和卫星数,但没有日期和速度,实际项目里经常两条都收,RMC 做主记录,GGA 补充质量字段。

一条典型的 RMC 长这样,数值取自标准示例,仅用于说明字段位置:

$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
序号字段含义注意点
1123519UTC 时间 hhmmss是 UTC,落库前要换算本地时区
2A定位状态A 有效,V 无效,V 时经纬度不可信
34807.038纬度 ddmm.mmmm度分格式,不是十进制度
4N纬度半球N 正 S 负
501131.000经度 dddmm.mmmm经度是三位度
6E经度半球E 正 W 负
7022.4地面速度,节需要乘 1.852 换 km/h
8084.4航向,度真北方向
9230394日期 ddmmyy两位年份,需要补世纪
10003.1,W磁偏角多数场景可以不存

字段 7 的单位是节,这一点最容易在后期对账时出问题:地图上量出来 40 km/h,日志里写着 21.6,不是采集错了,是单位没换。字段 9 的两位年份也要处理,23是 2023 还是 1923,按程序运行年份就近补,别直接加 2000,否则跨世纪的老设备会给你惊喜。

3.2 校验和与字段切分的 C++ 写法

校验和是$和*之间所有字符逐字节异或,得到的值用两位大写十六进制表示。校验失败的行必须丢弃,因为半条语句拼接、电磁干扰、波特率不匹配都会产生看似完整的假语句。用strtok切分要小心,连续逗号会被合并,导致空字段丢失、字段序号整体错位;NMEA 里空字段很常见,比如无效定位时速度字段就是空的,所以必须用能保留空字段的切法。

// 校验和:p 指向 '$' 之后的第一个字符 bool NmeaCheckSumOk(const char* p) { const char* star = strchr(p, '*'); if (!star || strlen(star) < 3) return false; unsigned char sum = 0; for (const char* q = p; q < star; ++q) sum ^= (unsigned char)(*q); char hex[3]; sprintf(hex, "%02X", sum); return _stricmp(hex, star + 1) == 0; } // 按逗号切分并保留空字段,返回字段个数 int NmeaSplit(char* line, char* fields[], int maxFields) { int n = 0; char* p = line; fields[n++] = p; while (*p && n < maxFields) { if (*p == ',') { *p = '\0'; fields[n++] = p + 1; } p++; } return n; }

切分之后先判断字段数够不够,RMC 至少要 10 个字段才谈得上解析,少字段说明这行本身就残缺。再判断语句头是不是$GPRMC,因为同一个串口上可能还挂着别的设备,或者模块同时输出 GSV、VTG,不筛头就会把卫星信噪比当成纬度。

3.3 度分转十进制度、定位有效性判定

4807.038这种格式是“度 + 分”,转换规则是前两位(经度前三位)为度,剩下的为分,分除以 60 加到度上。这个转换写错一位,坐标就偏几十公里,而且看起来还挺像真的,属于典型的不报错但错得离谱。

// ddmm.mmmm / dddmm.mmmm 转十进制度,南纬西经取负 double NmeaToDegree(const char* value, const char* hemi) { if (!value || !*value) return 0.0; double raw = atof(value); int deg = (int)(raw / 100.0); double minute = raw - deg * 100.0; double dd = deg + minute / 60.0; if (hemi && (*hemi == 'S' || *hemi == 'W')) dd = -dd; return dd; }

光转换还不够,落库前要过三道筛。第一道看定位状态,V直接标记为无效,此时经纬度可能是上一次的缓存值或者全零,写进去就是脏数据。第二道看数值范围,纬度绝对值大于 90、经度绝对值大于 180 一律丢弃,这类值通常来自半条语句或解析错位。第三道看跳变,相邻两条记录如果位置差出几公里而时间只差一秒,基本可以判定为漂移,尤其在刚上电搜星阶段,前几十秒的数据质量普遍很差。我一般的做法是上电后丢弃前 30 秒的定位记录,只保留状态字段,等状态稳定为A且连续三条位置连续再开始正式落库。

4. 采集数据落盘:文本日志、二进制记录和断电补写怎么选

4.1 文本和二进制各自的适用面

现场最常见的争议是要不要用 CSV。CSV 的好处是人能直接打开看,出了问题现场人员用记事本就能确认程序有没有在收数,排查成本极低;坏处是体积大、解析回放慢、浮点精度受格式化影响。二进制结构体的好处是紧凑、回读快、字段精度完整;坏处是不能肉眼确认,必须配一个回放工具,而回放工具往往没人维护。

维度CSV 文本二进制定长记录
可读性直接打开可看需要专用工具
体积约为二进制的 3~5 倍紧凑
追加写天然按行追加需保证结构体对齐一致
断电风险最多丢最后一行可能写出半条记录
后期分析Excel、脚本直接读需要解析代码

我的选择是双写:主记录走二进制保证完整性和体积,同时输出一份精简 CSV 只含时间、经纬度、状态、速度四个字段,专门给人看。两份文件用同一个时间戳命名,出问题时对照着看,比任何日志框架都直观。

4.2 用 CRITICAL_SECTION 保护采集缓冲

解析出来的记录要跨线程交给写盘逻辑,VC6.0 下最轻的方案就是临界区加一个固定长度的数组队列。队列满时丢最旧的记录并计数,绝不能阻塞采集线程,否则串口缓冲区溢出,丢的就不是一条两条了。

CRITICAL_SECTION g_cs; GpsFix g_queue[256]; int g_qHead = 0, g_qTail = 0, g_qCount = 0; long g_nDropped = 0; void PushFix(const GpsFix& f) { EnterCriticalSection(&g_cs); if (g_qCount >= 256) { g_qHead = (g_qHead + 1) % 256; // 丢最旧 g_qCount--; g_nDropped++; } g_queue[g_qTail] = f; g_qTail = (g_qTail + 1) % 256; g_qCount++; LeaveCriticalSection(&g_cs); } bool PopFix(GpsFix& f) { bool ok = false; EnterCriticalSection(&g_cs); if (g_qCount > 0) { f = g_queue[g_qHead]; g_qHead = (g_qHead + 1) % 256; g_qCount--; ok = true; } LeaveCriticalSection(&g_cs); return ok; }

临界区里只做内存拷贝,不做fprintf、不做Sleep、不做任何可能阻塞的调用,这是硬规矩。写盘线程从队列里批量取,一次取 20 条再统一写,能把 IO 次数压下来一个量级。g_nDropped一定要暴露到界面或者日志里,它是判断“程序是不是跟不上”的唯一指标。

4.3 掉线、断电和跨天文件的处理

GPS 模块掉线是常态,电源波动、天线被挡、线缆松动都会触发。程序要能区分“暂时没数据”和“端口已经废了”:连续 N 次ReadFile失败就关闭句柄、等几秒、重新CreateFile,重连成功后注意把环形缓冲区清空,否则上一次残留的半条语句会和新的数据拼在一起,产生一条校验和恰好能过的假记录,这种错误最难查。

断电风险靠fflush加周期性FlushFileBuffers兜。VC6.0 的FILE*默认有缓冲,不刷新的话断电会丢一整块,而不是一行。折中做法是每写 20 条fflush一次,每 5 分钟对文件句柄调一次FlushFileBuffers,兼顾性能和可靠性。

跨天切文件按 UTC 日期判断,不要按本地时间,否则时区一换文件名就乱。文件命名带上设备标识和日期,例如gps_<端口号>_<yyyymmdd>.bin,重连和重启都追加同一个文件,避免一天产生几百个小文件。切文件时先关旧文件再开新文件,中间允许丢几十毫秒的数据,比写出跨文件的半条记录安全。

5. 避坑排查:VC6.0 串口 GPS 采集最常翻车的 5 个现场

5.1 打不开、收不到、收一半:连接层的三个现场

现象:CreateFile一直返回无效句柄,换台机器就能打开。原因通常有两个:一是这个口被别的程序占着,串口是独占资源,上一份程序没退干净或者调试器还挂着句柄;二是COM10以上的端口名直接传"COM10"会失败,必须写成"\\\\.\\COM10"。解决办法是先检查任务管理器里有没有残留进程,再把端口名统一加上\\.\前缀,这套写法对COM1也兼容,不用分支判断。

现象:程序能打开口,但读到的全是零字节,日志里一条语句都没有。原因多半在硬件流控,fOutxCtsFlow打开而 CTS 线没接,驱动会认为对方一直忙,永远不发数据;也可能是波特率和模块不一致,数据其实进来了但全是乱码,被校验和直接过滤掉,表现出来同样是零。解决时先把流控全部关掉、RTS和DTR拉高,再用一个不做校验的原始日志把收到的字节十六进制打印出来,看有没有$开头,有就是波特率问题,没有就是接线或流控问题。

现象:平时正常,连续跑两三天后突然收不到数据,重启程序又好了。这是典型的句柄或缓冲区泄漏,常见于错误分支里直接continue而没有ClearCommError,驱动的错误标志越积越多,最后彻底不投递数据。解决办法是在每次读失败后都调用ClearCommError取走错误码和残留字节数,并且给重连加一个计数器,连续失败超过阈值就彻底关掉句柄重开,别指望它自己恢复。

5.2 解析出错与数据看着对但不可用:数据层的两个现场

现象:日志里偶尔出现一条纬度是 480.7 或者经度是 11.31 的记录。原因是字段切分时把小数点的位置算错了,或者把速度字段当成了经度。用strtok切分是最常见的诱因,连续逗号被合并,字段整体前移一位,纬度读到了半球标志,转换函数却照样能算出一个数。解决办法是换成保留空字段的切分函数,并且在转换前先检查字符串里小数点的位置:纬度的度部分固定两位,经度固定三位,长度不对直接判为无效。

现象:坐标看着正常,但和地图对不上,整体偏移几百米到几公里。先排除度分转换写错,再看模块输出的是不是原始坐标而是经过了差分或自定义偏移的坐标。有些模块出厂带偏移配置,输出的是加了固定量的位置,这种情况只能拿一个已知点做标定,算出偏移量再减掉。还有一种可能是把$GPGGA和$GPRMC的记录混在了一张表里,两条语句的时间字段格式不同,对齐之后就错位了。解决办法是每条记录都带上语句类型标识,落库时分开存,回放时按类型分别解析。

现象:程序退出时界面卡住十几秒,任务管理器显示进程还在。读线程还在ReadFile里阻塞,主线程等它退出,超时设得太长就一直等。解决办法是退出时先PurgeComm再CloseHandle,句柄一关阻塞的读调用会立刻返回错误,线程自然退出;同时给线程一个g_bRunning标志,循环里检查,别用TerminateThread硬杀,那个 API 不会释放线程持有的锁。

提示:这五条里有四条都能靠“原始字节日志”定位。程序里常驻一个只记录十六进制收发的开关,出问题时打开跑十分钟,比对着代码猜半天有用得多。

6. 长期运行:让这套采集程序少出玄学问题的几个习惯

现场设备讲究的是“装上去就别管”,所以程序里要有几个不起眼但救命的习惯。第一个是重连状态机,不要写成“失败就重开”,而是分成关闭、等待、重开、探测四步,每步都有超时,避免模块反复插拔时把 CPU 打满。

// 简化的重连状态机,放在读线程循环里 switch (g_nLinkState) { case LINK_OK: if (g_nFailCount > 3) { CloseHandle(g_hCom); g_hCom = INVALID_HANDLE_VALUE; g_dwNextTry = GetTickCount() + 3000; g_nLinkState = LINK_WAIT; } break; case LINK_WAIT: if (GetTickCount() >= g_dwNextTry) { g_nLinkState = LINK_OPEN; } break; case LINK_OPEN: g_hCom = OpenGpsPort(g_szPort); RingReset(); // 关键:清掉残留半包 g_nFailCount = 0; g_nLinkState = (g_hCom == INVALID_HANDLE_VALUE) ? LINK_WAIT : LINK_OK; g_dwNextTry = GetTickCount() + 3000; break; }

第二个是自检输出,每分钟往日志里写一行心跳,内容包括收字节数、有效语句数、校验失败数、队列丢弃数、当前重连次数。这五个数字放在一起,几乎能覆盖九成的现场问题:字节数在涨但有效语句不涨,是波特率或校验问题;有效语句在涨但丢弃数也在涨,是写盘跟不上;重连次数频繁,是供电或线缆问题。

第三个是配置外置。端口号、波特率、落盘目录、丢包阈值这些不要硬编码在代码里,用同目录下的 ini 文件读,现场换个口不用重新编译。VC6.0 下GetPrivateProfileString就够用,别为了这点配置引入额外的库。

自检指标正常表现异常时先查
每分钟收字节数与波特率匹配,波动小接线、流控、模块供电
有效语句占比95% 以上波特率、校验和、语句头过滤
队列丢弃计数长期为 0写盘频率、磁盘占用
重连次数一天不超过个位数线缆、电源、端口被抢占

我自己吃过一次亏:图省事把日志文件用追加模式一直开着,跑了大半年,文件涨到几个 G,某天磁盘满了,程序还在正常收数、正常解析,就是写不进去,而界面上一切正常。从那以后我定了个规矩,任何落盘动作都要检查返回值,写失败就切备用目录并点亮一个明显的状态灯,宁可界面上难看,也不能让数据静默丢失。这类问题没有后悔药,只能靠提前埋检查点。

这套东西不新,但把串口的超时、缓冲、重连、校验、落盘这几件事按顺序做扎实,一台工控机跑几年不出事并不难。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 5:02:01

WiFi 6与边缘AI驱动的智能工业安全锁止系统

1. 项目概述&#xff1a;WiFi与AI赋能的智能安全锁止系统在工业安全领域&#xff0c;锁止/挂牌&#xff08;LOTO&#xff09;系统是防止设备意外启动的关键防线。传统机械式LOTO装置存在三个痛点&#xff1a;操作依赖人员自觉性、状态无法远程监控、历史记录难以追溯。我们团队…

作者头像 李华
网站建设 2026/10/26 5:00:40

基于Grok 4.1情商模型的AI对话系统设计与实践

1. 项目概述&#xff1a;当AI成为你的"情感军师"去年在开发一款社交产品时&#xff0c;我注意到一个有趣现象&#xff1a;超过68%的用户在初次聊天时会出现"不知道说什么"的尴尬。这促使我开始探索如何用Grok 4.1的情商模型&#xff08;EQ Model&#xff0…

作者头像 李华
网站建设 2026/10/26 5:00:11

无知识库也能高效改写:信息解构×场景约束×三秒验证

1. 这不是知识库问题&#xff0c;是内容改写底层逻辑的错位“没有知识库&#xff0c;怎么改写出满意的内容&#xff1f;”——这句话一出来&#xff0c;我就知道提问的人刚被市面上那些“AI写作神器”带偏了。最近刷到太多教程&#xff0c;动不动就教人“搭建专属知识库”“喂养…

作者头像 李华
网站建设 2026/10/26 4:59:46

神经网络智能涌现现象与工程实践解析

1. 高维空间中的智能涌现现象解析当神经网络参数规模突破某个临界阈值时&#xff0c;我们会观察到一种神奇的现象&#xff1a;模型突然展现出训练数据中从未明确出现过的能力。这种"相变"般的智能跃迁&#xff0c;本质上源于高维表示空间的几何特性变化。在参数较少的…

作者头像 李华
网站建设 2026/10/26 4:59:19

互联网医院解决方案:在线问诊系统与AI智慧分诊开发

1. 互联网医院解决方案概述互联网医院正在重塑传统医疗服务的交付模式。这套在线问诊系统源码AI智慧分诊APP开发方案&#xff0c;为医疗机构提供了从患者接入到诊疗服务的全流程数字化解决方案。核心价值在于通过技术手段突破时空限制&#xff0c;将三甲医院的优质医疗服务延伸…

作者头像 李华
网站建设 2026/10/26 4:58:10

实时流计算状态管理:拆分流数据状态与流信息状态

实时流计算这条线上待久了&#xff0c;会摸出一个规律&#xff1a;凡是作业跑着跑着开始抖、重启之后数字对不上、延迟曲线无缘无故抬头&#xff0c;八成最后都会指到同一个地方——状态管理。前面算子写得再漂亮&#xff0c;状态这一层没设计好&#xff0c;整条链路就是建在沙…

作者头像 李华