简介:NTRIP 2.0(RTCM 10410.1)官方标准文档,2021年1月12日由RTCM第104专门委员会发布,面向GNSS高精度定位开发者、CORS网络运维人员及RTK算法工程师,用于规范互联网环境下差分改正数据流的传输方式。正文系统阐述Ntrip的系统概念、服务器与客户端交互流程、挂载点数据订阅与身份验证机制、速率控制策略,以及RTCM 2.x/3.x消息的承载与封装方式,并整合RTCM Paper 111-2009、139-2011两项修正与2021年SC104-1216最新修订,涉及错误修复、性能提升与兼容性增强。资源包共1个PDF文件,约863KB,保留英文原版封面、目录与修订页结构,便于按章节定位条款细节。目前已有1340人学习下载,适合在搭建Ntrip Caster、调试挂载点接入或排查数据流中断时作为权威依据对照查阅,也可作为协议实现与兼容性测试的参考底本。
1. 一份 2021 年才定稿的 NTRIP 2.0 标准,老客户端为什么一接就卡
对接网络 RTK 服务时,最常被问的不是坐标怎么算,而是「你们这个 caster 到底认哪个版本的 NTRIP」。我第一次接手一套 GNSS 差分分发链路,客户端是某款老接收机的固件,握手阶段直接死在拉源列表那一步,日志里只有一行非标准的伪状态行——这就是 NTRIP 1.0 时代留下的尾巴。RTCM 10410.1 是 2021 年 1 月定稿的 NTRIP Version 2.0,在 2009 年原始版本上叠加了两次修正(Amendment 1、Amendment 2),把协议重新拽回 HTTP/1.1 与 RTSP 的标准轨道:废弃伪状态行、引入分块传输编码、规范化头部记录、补齐源列表过滤和 RTSP 通路。它不教你写代码,但它决定了你自研的 NtripClient 能不能和市面上的 caster 互通,也决定了你自建的 NtripServer 会不会在别人客户端上「看着连上了、其实没数据」。正在做 GNSS 数据分发、网络 RTK 接入或者 RINEX 后处理链路的人,值得逐节对着排查。
2. 系统模型先立住:Caster、Server、Client 三个角色与 mountpoint 的边界
2.1 无状态流而不是订阅通道,这条决定了后面所有坑
NTRIP 的定位很明确:它是 HTTP/1.1 和 RTSP 的一个裁剪子集,只保留实时数据流所需的那部分。这句话决定了后面所有实现取舍——它没有自定义的会话状态机,没有消息确认和重传,没有断点续传。一次 NtripClient 请求,本质上就是发一个 HTTP GET、拿到 200、然后服务端把字节按顺序灌进这条 TCP 连接,客户端读到多少算多少。中间丢了几个字节,协议层不会替你补,只能靠上层自己重新对齐 RTCM 电文头(前导字节0xD3加 6 位保留位和 10 位长度,尾部 CRC-24Q 校验)。
这种设计带来的好处是横向扩展极其简单:caster 前面挂四层负载均衡,任意一个客户端连接落到哪台机器都行,因为连接本身不携带需要跨请求保持的上下文。代价也同样明显——每建一条新连接,认证要重做一遍、挂载点订阅要重做一遍、VRS 场景下的 NMEA 位置还得重新注入一遍。所以客户端侧的重连逻辑不能写成「断了就退出」,而必须是一个带退避的状态机:断开 → 等待 1s → 重新 GET → 校验首包是不是合法电文 → 注入 GGA → 进入正常读取。这段逻辑协议里不给你,得自己写。
还有一点容易被忽略:无状态不等于无连接语义。HTTP/1.1 的长连接(Connection: keep-alive)解决的是同一条 TCP 上复用多次请求的问题,而 NTRIP 的典型用法是一条连接只发一次 GET、然后在这条连接上一直读流。所以看到Connection: close不要慌,NTRIP 的流式响应本来就不会正常结束,它要么被客户端主动断开,要么被 caster 因为超时、认证过期、源下线踢掉。
2.2 mountpoint 是唯一的路由键,也是唯一的权限粒度
整套系统里,NtripSource 产生数据流,NtripServer 把流推到 caster,NtripClient 从 caster 按需拉流,三方之间唯一的对齐物就是 mountpoint 这个字符串。对客户端来说它就是 URL 的 path 段,GET /MOUNTPOINT HTTP/1.1;对 caster 管理员来说它是权限配置的最小单位,谁可以往这个挂载点推、谁可以从这个挂载点拉,都挂在它下面。
实际项目里 mountpoint 的命名有几个血泪经验。别把版本号和星座写死在名字里又指望它不变,今天叫RTCM3_GPS,明天加了 Galileo 就得改名,所有客户端配置全部要跟着改;更稳的做法是把「区域 + 用途」放前面,格式信息交给源列表里的 FORMAT 字段去表达。另外注意大小写,规范层面挂载点是区分大小写的字符串,但不同 caster 实现的处理并不统一,我一般强制全大写,避免跨实现踩坑。还有唯一性——同一个 caster 上两个 NtripServer 用同一个 mountpoint 推流,大多数实现是后来的把先来的顶掉,而且不一定报错,表现为「刚才还好好的流突然换了一个基准站坐标」,非常难查。
2.3 Server 端先想清楚三件事:电文、速率、坐标
自建 NtripServer 的人最容易犯的错,是先写传输、后想内容。传输层其实很薄——NTRIP 2.0 里服务端向 caster 推送走的是标准 HTTP 方法,而 1.0 时代用的那个伪方法至今仍被不少 caster 保留做兼容,这就是为什么有些老设备在新 caster 上能推不能拉,或者反过来。真正要花时间的是内容侧。
电文格式决定了你的终端能不能用。RTCM 2.x 是给老接收机准备的,报文短、带宽省;RTCM 3.x 里 1004/1012 这类经典电文兼容性最好,但每颗卫星只给一个频点;MSM 电文(MSM1 到 MSM7)才是现在做多系统多频 RTK 的主流,MSM4 及以上带伪距和载波相位,MSM5/7 还带多普勒和完整信号强度。选型上没有绝对答案,看你的下游终端固件支持到哪一档。
速率和带宽要一起算。给个我常用的粗算表,单位是 bps,只做容量规划参考:
| 电文组合 | 典型电文 | 大致带宽 | 适用场景 |
|---|---|---|---|
| RTCM 2.3 | 18/19/22 | 800 ~ 1500 | 老终端、亚米级差分 |
| RTCM 3 经典 | 1004 + 1005 + 1012 | 3000 ~ 5000 | GPS+GLONASS 双系统 RTK |
| RTCM 3 MSM4 | 1074/1084/1094/1124 | 7000 ~ 11000 | 多系统多频 RTK |
| RTCM 3 MSM7 | 1077/1087/1097/1127 | 12000 ~ 18000 | 全频点、后处理兼用 |
坐标这块,基准站的 1005/1006 电文必须发。1005 只给 ECEF 坐标,1006 额外带天线高。少了它,流动站能解出相对位置却报不出绝对坐标,表现就是「固定解有了,但位置整体偏几十米」。
2.4 2.0 相对 1.0 的四处关键改动,逐条对到实现上
第一处是状态行。1.0 时期 caster 回的是那个非标准的伪状态行,2.0 强制改成合法的 HTTP 状态行,200 就是 200。这意味着任何还在用字符串前缀判断「是不是 SOURCE 200 OK」的老代码,都必须改成解析标准状态行,否则接新 caster 一律失败。
第二处是分块传输编码。2.0 允许响应体使用 chunked,也就是说你从 socket 里读到的字节流带有长度前缀和块分隔符,不能直接当裸 RTCM 往解码器里塞。这块下面会给处理方式。
第三处是头部记录规范化,2.0 明确列出了标准 HTTP 头、NTRIP 专用头,以及被废弃的 1.0 头部清单。混合实现里最常见的症状就是「客户端发了废弃头,caster 静默忽略,然后行为跟预期不一致」。
第四处是源列表过滤和 RTSP 通路。过滤让客户端只拿自己关心的那部分源列表,RTSP 让数据面可以走 UDP。这两块对做大规模分发的人是刚需,对小规模接入其实可以先用 HTTP 通路跑通再说。
3. HTTP 通道落地:拉源列表、订阅挂载点、回传 NMEA 的完整构造
3.1 拉源列表:一条命令看清 caster 的能力清单
第一步永远不是订阅,是拉源列表。源列表就是 caster 自报家门的文本,里面有哪些挂载点、什么格式、要不要认证、坐标在哪,全在里面。
# CASTER 换成你自己的 caster 地址,默认端口 2101 CASTER="127.0.0.1:2101" # 拉源列表:Ntrip-Version 必须带上,否则部分 caster 会回落到 1.0 语义 curl -sS --max-time 10 \ -H "Ntrip-Version: Ntrip/2.0" \ -H "User-Agent: NTRIP-probe/1.0" \ "http://${CASTER}/" \ | head -n 30-H "Ntrip-Version: Ntrip/2.0"是最容易被省掉、也最容易出事的一个头。省掉之后 caster 可能按 1.0 的语法回你,于是拿到的正文少了分块编码、少了某些字段,你按 2.0 解析就全乱。-H "User-Agent"在规范里属于建议携带,但不少商用 caster 用它做统计和黑白名单,填一个可识别的名字比留空好。--max-time 10是防止某些网络环境下连接建起来但不回数据,卡死脚本。
拿到的正文长这样,每一行是一条以分号分隔的记录:
CAS;caster.example;2101;;;0;DEU;50.10;08.80; NET;EUREF;Example Operator;N;N; STR;BRDC;BRDC 0;RTCM 3;1004(1),1005(10),1012(1);2;GPS+GLO;EUREF;DEU;50.10;08.80;0;0;sNTRIP;none;B;N;5000;判断这一步成功与否,别看 curl 有没有报错,要看正文里有没有STR;开头的行。有,说明 caster 活着且源在线;只有CAS;和NET;没有STR;,说明 caster 活着但一个源都没推上来,这时候去查 NtripServer 侧,别在客户端上浪费时间。
3.2 订阅挂载点:请求行、Host、认证三件套
源列表里挑一个STR;行的第一段作为挂载点,然后构造订阅请求。用 curl 验证最快:
# 订阅挂载点,把 MOUNTPOINT 换成源列表里真实存在的那一段 MOUNTPOINT="BRDC" USER="your_user" PASS="your_pass" curl -sS -N --max-time 30 \ -H "Ntrip-Version: Ntrip/2.0" \ -H "User-Agent: NTRIP-probe/1.0" \ --basic -u "${USER}:${PASS}" \ "http://${CASTER}/${MOUNTPOINT}" \ -o stream.rtcm # 看看落地了多少字节,以及前几个字节是不是 0xD3 ls -l stream.rtcm xxd -l 16 stream.rtcm-N是关掉 curl 的输出缓冲,不加这个你会看到文件大小半天不动、然后一下涨一大截,误判成断流。--basic是显式指定认证方式,因为 curl 在某些握手场景下会先试探其他方案,对老 caster 反而添乱。-o落盘后一定要做首字节检查:合法的 RTCM 3 流第 0 个字节应该是0xD3,如果是3C或者0A之类,说明你下回来的其实是 HTML 错误页或者源列表,认证或挂载点名字出问题了。
如果用原生 socket 写,请求就这几行,注意行尾必须是 CRLF:
import socket req = ( "GET /BRDC HTTP/1.1\r\n" "Host: 127.0.0.1:2101\r\n" "Ntrip-Version: Ntrip/2.0\r\n" "User-Agent: NTRIP-client/1.0\r\n" "Authorization: Basic dXNlcjpwYXNz\r\n" # base64(user:pass) "\r\n" ) s = socket.create_connection(("127.0.0.1", 2101), timeout=10) s.sendall(req.encode("ascii")) # 先把响应头读完,头和数据以空行分隔 buf = b"" while b"\r\n\r\n" not in buf: buf += s.recv(1024) head, rest = buf.split(b"\r\n\r\n", 1) print(head.decode("latin-1"))关键点是那个空行。少一个 CRLF,caster 会一直等请求头结束,你在对面看到的现象就是「连上了但永远没数据」。rest里可能已经带了第一块数据,别丢掉。如果响应头里有Transfer-Encoding: chunked,后面读到的每一段都需要先读十六进制的块长度、再读内容、再跳过 CRLF,裸拼会污染数据。
3.3 NMEA 回传:VRS 场景下不给位置就没有数据
普通单基站挂载点,你连上去就出流;但网络 RTK 的 VRS 类挂载点,caster 需要知道你在哪,才能在你附近生成虚拟参考站。不给位置,有的 caster 用默认坐标糊弄过去(你会看到固定解但精度差),有的直接不推流。
位置用 NMEA 的 GGA 语句表达。协议里允许它出现在几个位置:作为请求 URI 的查询串、作为独立的 POST 请求体、或者放在请求头里。不同 caster 对这几条路的支持程度差别很大,常见做法是首选查询串或请求头,跑不通再看 caster 侧文档。GGA 本身的格式如下,重点是$GPGGA后第 2、4 段是纬度和经度,格式是度分拼写(ddmm.mmmmm):
$GPGGA,120000.00,3959.9999,N,11618.0000,E,1,12,0.8,50.0,M,-8.0,M,,*4A回传频率上,VRS 不需要你发得很快,1 Hz 足够,但别断。我一般设成 5 秒一次、连续 30 秒发不出去就判定会话废了、主动重连。这里有个反直觉的点:位置注入和流读取必须分两条路径处理,如果写成「发完 GGA 再读数据」的单线程循环,读阻塞的时候 GGA 就断了,caster 判定你位置失效,然后停推——现场表现是「每两分钟断一次,重连就好」,非常像网络问题,其实是自己的逻辑问题。
3.4 认证与分块传输:Basic 和 Digest 的实际取舍
规范里定义了 Basic 和 Digest 两种认证。Basic 就是把用户名:密码做 base64 塞进Authorization头,明文可逆;Digest 用 nonce 做一次挑战响应,密码不过网。理论上应该优先 Digest,实际部署里绝大多数 caster 只开 Basic,原因是客户端固件支持度差。既然只能用 Basic,那就别在明文 HTTP 上裸跑,外面套一层加密通道,把 2101 包在里面。
不管是哪种认证,密码里别出现冒号。Basic 的分隔符就是冒号,解析时按第一个冒号切成两段,密码里有冒号就会被截断,现象是「同样的账号密码浏览器能登、客户端一直 401」。这个坑我见过至少三次,每次都查半天。
分块传输的处理要单独说。当响应头出现Transfer-Encoding: chunked,读取循环要改成按块解析:
def read_chunked(sock): """按分块编码读取,返回解块后的纯数据块""" while True: size_line = b"" while not size_line.endswith(b"\r\n"): size_line += sock.recv(1) size = int(size_line.strip().split(b";")[0], 16) # 忽略块扩展 if size == 0: return payload = b"" while len(payload) < size: payload += sock.recv(size - len(payload)) sock.recv(2) # 丢掉块尾的 CRLF yield payload逻辑上有两处必须注意:块长度那行可能带;扩展参数,切掉再转十六进制;块尾那两个字节的 CRLF 一定要吃掉,否则下一轮读长度会读到脏数据,症状是「跑几秒后长度解析异常,然后连接断开」。yield出去的是干净的 RTCM 字节,可以直接喂给解码器。
3.5 错误处理:状态码比正文更可信
订阅失败时,不同 caster 的正文写得五花八门,有的给一行英文,有的给一个 HTML 页面,有的干脆空着。但状态码是规范的,按它分流最靠得住:
| 状态码 | 含义 | 现场处置 |
|---|---|---|
| 200 | 订阅成功,后续是数据流 | 校验首字节0xD3 |
| 401 | 认证失败或未提供凭证 | 查账号、查密码冒号、显式--basic |
| 403 | 认证过了但无权访问该挂载点 | 找 caster 管理员加白名单 |
| 404 | 挂载点不存在 | 重拉源列表,确认名字和大小写 |
| 400 | 请求格式不合法 | 检查 CRLF、Ntrip-Version、Host 头 |
还有一个隐蔽情况:状态码 200,但连着几秒一个字节都不来。这不是成功的 200,是挂载点存在但源当前没数据。别在这里死等,加个首包超时,比如 10 秒内没收到第一个0xD3就断开重来。
4. RTSP/RTP 通道:什么时候值得切 UDP,SETUP 和 PLAY 怎么发
4.1 RTSP 只是控制面,数据面才走 UDP
2.0 新增的这条通路,结构上分成两层:RTSP 走 TCP,负责建会话、协商端口、开始和结束传输;真正的 GNSS 数据打成 RTP 包走 UDP。这个拆分的好处是数据面没有 TCP 的重传和队头阻塞,丢一个包就丢一个包,对实时差分来说迟到的包本来也没价值;坏处是 NAT 和防火墙不友好,而且丢包在协议层完全不可见。所以选不选它,不取决于「UDP 是不是更快」,取决于你的网络环境能不能稳定回传 UDP,以及你的终端会不会自己检测丢包。
我的经验判断线是这样:局域网或者运营商专线、终端数量少、丢包率低于 0.5%,走 RTSP/RTP 值得;公网移动网络、跨地区、终端数量上百,老老实实用 TCP 那条路,稳定性远比那点延迟重要。
4.2 SETUP 的 Transport 头与端口协商
会话建立的第一条命令是 SETUP,核心是Transport头,它告诉 caster 我想用 UDP、我这边开哪两个端口(RTP 数据和 RTCP 控制各一个)。
SETUP rtsp://caster.example:2101/BRDC RTSP/1.0 CSeq: 1 User-Agent: NTRIP-rtsp-client/1.0 Transport: RTP/UDP;unicast;client_port=5000-5001CSeq是命令序号,每条 RTSP 命令必须递增,caster 靠它匹配请求和响应,序号重复或者不递增会被直接拒掉。client_port给的是偶数起始的一对端口,5000-5001表示数据发 5000、控制发 5001。caster 的 200 响应里会带Session和server_port,这两个都要记下来,后面 PLAY 和 TEARDOWN 都得用。
然后是 PLAY,把Session带上:
PLAY rtsp://caster.example:2101/BRDC RTSP/1.0 CSeq: 2 Session: 12345678收到 200 之后,数据就开始往你声明的 UDP 端口上打了。结束的时候发 TEARDOWN,别直接关 socket 走人,caster 那边的会话要过一段时间才超时回收,期间这个挂载点的资源一直被占着。
4.3 Keep-Alive 与初始 RTP 包:NAT 后面的那口气
RTSP 会话有超时,长时间没有命令往来,caster 会认为客户端死了。所以中途必须发保活请求,一般是周期性发 OPTIONS 或者重发 PLAY,间隔按 caster 的超时参数倒推,保守取超时值的二分之一到三分之一。客户端如果只收数据不发保活,现场现象是「连上能收两三分钟,然后无声无息断了」。
更麻烦的是防火墙。你的客户端在 NAT 后面,声明了client_port=5000,但 NAT 设备不知道你要往这个口收包,第一个从外面进来的 UDP 包会被直接丢掉。规范里为了解决这个问题,要求 caster 在数据流开始前先往客户端端口发一个初始 RTP 包——内容没有业务意义,作用是把 NAT 的表项打通。客户端如果严格校验 RTP 头的序列号或者载荷类型,会把这个包当成异常丢掉,然后发现「前几秒没数据」。正确做法是收到 RTP 包后先看序列号和 SSRC,第一个包只用来建表项,从第二个包开始才计入数据。这条在自研客户端上翻车的概率相当高。
4.4 纯 RTP 模式与 RTSP 模式的差别
规范里还有一条纯 RTP 的路:没有 RTSP 控制面,caster 直接往约定的地址和端口打 RTP 流。这种模式适合一对多广播,一个源同时喂很多终端,终端侧不需要建会话、不需要认证握手,配置简单到只有地址和端口。代价是没有会话管理,你没法告诉 caster「我不要这段了」,也没法做按客户端的差异化服务,出了问题只能看收没收到包。
三种通路放一起对比,选择逻辑其实很清楚:
| 通路 | 控制面 | 数据面 | 适合场景 | 主要风险 |
|---|---|---|---|---|
| HTTP | HTTP/1.1 over TCP | TCP | 默认选择、跨公网、终端多 | 丢包重传带来延迟抖动 |
| RTSP/RTP | RTSP over TCP | RTP over UDP | 局域网、低延迟、终端少 | NAT 打洞失败、丢包不可见 |
| 纯 RTP | 无 | RTP over UDP | 一对多广播、只读终端 | 无会话管理、排障困难 |
5. 避坑与排查:五个能把人卡一整天的现场问题
5.1 现象:curl 返回 200,文件一直是 0 字节
原因通常有三个,按概率排:一是漏了Ntrip-Version头,caster 按 1.0 语义返回,正文格式和你的解析对不上,看着像没数据;二是挂载点是 VRS 类型,你连上去了但没注入 NMEA,caster 在等你的位置,一直不推流;三是边界设备把 2101 端口做了半开处理,TCP 握手过了但数据被拦。
排查顺序我一般固定为:先加-H "Ntrip-Version: Ntrip/2.0"重跑;再查源列表里这个STR;行的 NMEA 必需位是不是 1;最后在客户端机器上直接nc -vz CASTER 2101确认端口通,再抓一次包看有没有回数据。三步走完基本能定位。
5.2 现象:源列表拉下来是一整行,或者带一堆奇怪数字
原因几乎总是分块传输没有被正确处理。规范要求响应体可以用 chunked 编码,直接curl到文件再cat,中间那些十六进制的块长度和 CRLF 会原样留在文件里,看起来就是「行被粘在一起」。另外行尾是 CRLF 不是 LF,用只认 LF 的解析器切行会切不干净。
解决办法是在客户端侧按分块解析完再落地,或者用支持自动解块的 HTTP 库。落地之后先做一次行尾清洗(去掉\r),再按\r\n或\n切行,最后只保留以CAS;、NET;、STR;开头的行,其余全丢。
5.3 现象:本机跑通,客户机房连不上
八成是出网策略。默认端口 2101 在很多机房的出网白名单之外,本机在内网测试当然通。还有一种情况是 RTSP/UDP 通路,TCP 的 RTSP 建会话成功,但 UDP 回程被拦,于是「会话建立成功、数据一包没有」。
处置上别一上来就改协议,先做连通性分层验证:TCP 层用nc -vz,HTTP 层用一次源列表请求,数据层用 30 秒的订阅测试。哪一层断的,问题就归哪一层。如果确认是 UDP 被拦,直接退回 HTTP 通路,不值得为了 UDP 去改机房策略。
5.4 现象:同一个挂载点,两个客户端拿到的解算结果不一样
这是 VRS 类挂载点的典型表现。caster 按每个连接注入的 GGA 位置各自生成虚拟参考站,两个客户端位置不同,拿到的改正数自然不同,解出来的坐标有分米级差异是正常的。真正的异常是「客户端明明没发 GGA,结果却和发了 GGA 的那个一模一样」——说明 caster 给未注入位置的连接用了默认坐标,这时候精度会明显劣化,但因为仍然有固定解,很容易被误判成「系统正常」。
判断方法很直接:订阅时故意把 GGA 的经纬度改一个方向,看解算结果有没有跟着变。变了是正常的 VRS 行为,没变说明位置注入根本没生效。
5.5 现象:认证一直 401,但同样的账号密码在别处能用
先查密码里有没有冒号,Basic 认证按第一个冒号切分,密码含冒号必然截断。再查客户端有没有做 URL 编码,某些实现会把密码里的@、#、/当成 URL 结构字符,实际发出去的密码已经变形。最后查认证方式协商,有的客户端默认先试 Digest,遇到只支持 Basic 的 caster 会多一轮 401,看起来像认证失败。
处置办法是显式指定 Basic,并且把账号密码从配置文件里读出来做一次长度打印,确认发出去的字符串和你以为的一致。这个打印我一般只在排障时加,平时不打日志,避免明文进日志文件。
6. 用 Sourcetable 字段和一串 RTCM 做交叉验证
协议文档看完容易有「好像懂了」的错觉,真正能验证你理解到位的,是把源列表字段和实际数据流对起来。STR 行的字段是按分号切分的固定顺序,最值得关注的几段是这几个:
| 位置 | 字段 | 说明 | 校验方式 |
|---|---|---|---|
| 2 | MountPoint | 挂载点名 | 与订阅 URL 的 path 完全一致 |
| 4 | Format | RTCM 2 / RTCM 3 / CMR 等 | 与解码器支持的格式对齐 |
| 5 | Format-Details | 消息号与间隔,如1004(1),1005(10) | 实际流里应能解出这些消息号 |
| 11 | NMEA | 是否必须注入位置 | 为 1 时客户端必须发 GGA |
| 13 | Solution | 0 单基站、1 网络解 | 决定你拿到的是原始观测还是改正数 |
| 19 | Bitrate | 标称比特率 | 与实测带宽比对,偏差大说明源配置有误 |
解析不用写得复杂,一个容错的切分函数就够了:
def parse_str(line: str): f = [x.strip() for x in line.strip().split(";")] if len(f) < 20 or f[0] != "STR": return None # 字段不足直接丢弃,别硬解析 return { "mountpoint": f[1], "format": f[3], "details": f[4], "nmea": f[10] == "1", "solution": f[12], "bitrate": f[18], }容错的关键是len(f) < 20这个下限判断。不同 caster 的 STR 行字段数并不严格一致,有的会补末尾空段,有的会省,写死索引又不做长度检查,遇到短行就抛 IndexError,整个源列表解析中断,表现为「明明有几十个源,程序只识别出前几个」。跑通解析之后,拿 Bitrate 字段和实测做对账:订阅 60 秒,数一下收到的字节,乘以 8 除以 60,和 Bitrate 差三倍以上就说明这条流的实际内容和源列表自报的不一致,多半是 caster 配置漂了。
最后一步交叉验证,我习惯把流落盘跑一遍 RINEX 转换。RTCM 3 的 MSM 电文本身可以转成 RINEX 观测文件,转出来打开看三件事:卫星数是不是和源列表声明的星座对得上、观测值里有没有整周模糊度连续的载波相位、时间标签间隔是不是稳定的 1 秒。这三条只要有一条不对,前面所有「连上了、有数据」的结论都要打折扣。
从那以后我每次接入一个新的 caster,都会强制走一遍「拉源列表 → 解析 STR → 订阅 60 秒对账带宽 → 转 RINEX 看观测质量」这条链路,不管对方怎么保证「我们这是标准实现」。标准是标准,落地是落地,中间那层差异只能靠自己的验证脚本兜住。希望帮到你。
本文还有配套的精品资源,点击获取