简介:《Q/GDW 11709.3—2017 电动汽车充电计费控制单元 第3部分:与车联网平台通信协议》面向国网TCU研发、充电设施平台接入及协议联调人员,聚焦车联网平台与计费控制单元之间的数据交互规范,可用于指导接口设计、报文解析和现场问题排查。包内为1个PDF文件,约1.23MB,正文涵盖规范性引用文件、术语和缩略语、总则、TCP/IP通信接口、端口号与连接方式、协议结构、应用层报文帧格式及通信过程。附录给出计费控制单元信息数据项定义,并涉及车联网平台作为TCP服务器、TCU作为客户端的连接关系,便于对照标准核对字段含义、报文类型和交互顺序。已有302人学习,适合充电桩企业、平台开发商及测试人员理解国网TCU接入车联网平台时的连接建立、数据上送与命令下发等流程约束,也可作为协议联调、问题定位及内部培训的参考资料。
1. 从「桩主动连平台」说起:这套协议到底在管什么
电动汽车充电计费控制单元(TCU)与车联网平台之间的通信协议,最反直觉的一条是:TCP 连接必须由桩主动发起,平台只被动等待。桩散落在停车场、小区地库、园区角落,地址由运营商临时分配,平台不可能维护一张「每台桩此刻在哪个 IP」的拨号表;而平台地址固定,桩只要记住一个 IP 加端口 2406 就能重新连上。
第二个反直觉点:它没用 MQTT 加 JSON,而是把电力远动 104 规约的骨架整段搬了过来——68H 起头、11 位长度域、4 字节控制域、1 字节类型标识决定报文走哪条流水线。一帧初始化报文只有 30 个字节,却要塞进终端编号、CP56 时标、协议版本、充电接口号四样东西。
它要解决的问题非常具体:让一笔充电的钱,在桩、平台、密码机三方之间不重不漏地走完,哪怕中途断网、哪怕桩离线了一整天才恢复。
2. 协议栈的裁剪:TCP 之上为什么还要再套一层 104 骨架
2.1 连接方向定死:桩是客户端,平台是服务端
规范里写得很干脆:车联网平台为 TCP 服务器端,TCU 为客户端,TCU 主动连接,平台被动响应,端口号宜采用 2406。这一句决定了后面所有状态机的形状。
现场部署的现实是,桩装在企业内网、商场弱电井、小区物业机房,出网要走 NAT,IP 是动态的,平台侧永远拿不到一个稳定可达的入站地址。反过来,平台域名或 IP 是长期固定的。把发起权交给桩,等于把「谁该记着谁」这件事交给了数量更多、但逻辑更简单的那一侧——每台桩只需要记住一个目标地址,平台只需要维护一张「终端编号 → 当前连接」的会话表。
代价也很清楚。平台必须自己扛住海量长连接,还要处理连接与业务身份的绑定:TCP 连上了不等于这台设备就是它自称的那台,所以初始化阶段必须先上送协议标识帧,把 8 字节终端编号报上来,平台拿这个编号去索引会话。桩这边则必须自己实现断线重连和链路保活,不能指望平台来探活。
2.2 68H 与 11 位长度域:TCP 是字节流,必须自己划边界
TCP 只保证字节按序到达,不保证「一次 recv 等于一个完整报文」。你发两帧,对端可能一次读到一帧半,也可能两帧粘在一起。所以应用层必须自带帧定界,这套协议用的是最经典的两段式:一个固定启动字符加一个长度域。
启动字符固定为 68H,作用是在字节流里找起点。长度域占 2 个字节,但只有低 11 位有效,高位保留为 0,取值范围 0~2047,而且计数范围是「控制域 + ASDU」,不含启动字符和长度域本身。11 位这个上限不是随手取的,它直接决定了单个 APDU 最大约 2KB——超过就得在业务层自己分包。后面排查章节里会专门讲这条限制怎么把一个大配置文件坑掉。
4 字节控制域承载的是防止报文丢失和重复传送的序号机制,具体定义沿用 104 规约第 5 章:I 帧带发送序号 N(S) 和接收序号 N(R),各 15 位;S 帧只做确认;U 帧做启停和测试。TCP 已经保证了字节可靠,这里再加一层序号,防的是应用层语义上的重发与乱序——比如桩在 t1 超时后重发了一帧,平台其实已经收到并处理过,靠序号才能识别出这是重复而不是新业务。
四个定时器的取值在规范里是写死的,工程上最好别乱改:
| 定时器 | 含义 | 默认值 | 超时后的典型动作 |
|---|---|---|---|
| t0 | 连接建立超时 | 20 s | 放弃本次连接,退避后重连 |
| t1 | 发送或测试 APDU 的超时 | 15 s | 判定链路异常,关闭连接重来 |
| t2 | 无数据报文确认超时 | 10 s | 补发 S 帧确认 |
| t3 | 长期空闲时发送测试帧 | 20 s | 发 TESTFR 探活 |
t1 和 t3 的关系值得多看一眼。t3 是「闲着没事干的时候,每隔 20 秒发一个测试帧」,t1 是「我发出去一个东西,15 秒内没回应就算死」。也就是说,链路真正被判定为断掉,通常发生在空闲测试帧发出后 15 秒仍未收到确认的那一刻。很多现场反馈「桩显示在线但平台收不到数据」,用抓包一看,是桩侧根本没发 TESTFR,或者发了但没处理对端的确认,导致连接在半开状态挂了一整天。
2.3 U 帧启停:TCP 通了不等于可以发数据
这是最容易翻车的一处。TCP 三次握手成功,只说明网络层通了,不代表应用层准备好收业务报文。规范要求车联网平台准备就绪后发送 U 帧 STARTDT,TCU 回 STARTDT_CONT,双向确认后数据传输才算真正启动;需要停止时用 STOPDT 和 STOPDT_CONT 成对出现。
为什么非要多这一步?因为平台侧的初始化是异步的:连接建立的一瞬间,平台可能还在加载这台终端的计费模型、黑名单、费率参数,此时如果桩抢着发业务包,平台只能丢弃或者缓存。有了 STARTDT,平台可以用它当一道门闸——配置没就绪就不发,桩那边也老老实实等着。
常见的实现思路是,桩把链路状态拆成「TCP 已连」「已收 STARTDT」「业务可发」三级,任何一级不满足都不允许上层业务线程写 socket。我见过不少自研桩把这三态压成两态,结果就是在平台重启的窗口期疯狂重连又疯狂发注册包,把平台侧日志刷满。
3. ASDU 的字段账本:8 字节公共地址和 1 字节记录类型号是怎么挤进去的
3.1 先看整体布局:APDU = APCI + ASDU
应用规约数据单元由两部分组成:APCI 负责定界和控制,ASDU 负责业务。ASDU 的固定头是这套协议改造最狠的地方,按字节偏移排下来是这样:
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | 类型标识 | 1 B | 决定后续怎么解 |
| 1 | 可变结构限定词 VSQ | 1 B | 高 1 位 SQ,低 7 位 N(0~127) |
| 2 | 传送原因 COT | 2 B | 低 6 位是原因码 |
| 4 | 公共地址 | 8 B | 终端编号,压缩 BCD,尾部补 0 |
| 12 | 信息体时标 | 7 B | CP56 |
| 19 | 记录类型号 | 1 B | 仅业务包类报文携带 |
| 20 | 信息对象 | 变长 | 由记录类型号决定 |
标准 104 的公共地址只有 2 字节,这里直接扩到 8 字节,等于每个 ASDU 的固定开销从 6 字节涨到 12 字节。这不是随手加长,而是因为终端编号本身是平台生成的业务主键,长度超过 2 字节能表达的范围。用业务主键当协议地址,好处是平台收到报文第一眼就知道是谁,不需要再做一次「连接 → 设备」的映射;坏处是每个心跳包都白背 8 个字节,按百万级终端算,这部分流量不便宜。
3.2 类型标识:先分流,再解业务
类型标识是 1 个字节,它决定了这条报文走哪条流水线。规范把方向拆成两套:
| 方向 | 类型标识 | 名称 | 用途 |
|---|---|---|---|
| 监视(桩→平台) | 1 | M_SP_NA_1 | 单点遥信 |
| 监视 | 11 | M_ME_NB_1 | 测量值,标度化 2 字节 |
| 监视 | 130 | M_RE_NA_1 | 上行业务数据 |
| 监视 | 132 | M_MD_NA_1 | 扩展测量值,长度大于 2 字节 |
| 监视 | 134 | M_JC_NA_1 | 实时监测数据 |
| 控制(平台→桩) | 99 | C_ST_NA_1 | 协议启动 |
| 控制 | 100 | C_IC_NA_1 | 总召唤 |
| 控制 | 133 | C_SD_NA_1 | 下发数据项 |
11 和 132 的分工很有意思:测量值长度等于 2 字节用 11,大于 2 字节用 132。132 在每个信息对象前多一个「信息对象长度」字节,本质上是给变长数据留的通用容器。这种「窄类型 + 宽类型」并存的设计,是为了让绝大多数电量、电压、电流这类小数值走最紧凑的路径,只有累计电量这种可能溢出 2 字节的量才走宽路径。
3.3 记录类型号:1 个字节撑起三十多种业务
业务包不靠新增类型标识来区分业务,而是统一用 130(上行)和 133(下行),再在 ASDU 里塞一个记录类型号。这是一个非常务实的选择:类型标识的语义空间已经被 104 规约占满了,随便新增会破坏既有工具链的解析,而业务种类是会一直长的,必须留一个可扩展的口子。
关键映射摘一部分出来,能看清业务全貌:
| 记录类型号(上行/下行) | 业务 | 归属 |
|---|---|---|
| 7 / 6 | 双向认证(终端发起 / 平台下发随机数) | 安全 |
| 8 / 7 | 双向认证(终端上送认证数据 / 平台下发认证数据) | 安全 |
| 4 / 4 | 交易取随机数 | 交易前置 |
| 5 / 5 | 请求下发计费模型 / 下发计费模型 | 计费 |
| 22、23 / 32 | 电价模型、服务费模型上行 / 服务费模型下发 | 计费 |
| 1、2 / 2、3 | 账号在线充电鉴权与交易 | 交易 |
| 3 / 11 | 有卡离线充电交易 | 离线补传 |
| 14、15 / 17、18 | 解灰上行与结果 / 解灰下发与结果 | 灰锁处理 |
| 16、17 / 19、20 | 验证码在线充电鉴权与交易 | 交易 |
| 9、10 / 8 | ESAM 密钥更新文件与结果 / 更新下发 | 安全 |
| 12、24 / 1、27 | 黑名单增量与全量确认 / 下发 | 风控 |
| 20 / 24 | 请求停止充电确认 / 停止充电 | 控制 |
| 21、27 / 30、31 | 二维码充电启动确认与交易记录 | 交易 |
| 28、29 / 35、33 | 时钟查询与同步 | 运维 |
| 30、31 / 34 | 在线注册激活 | 运维 |
| 32 / 36 | 资产编号变更 | 运维 |
| 33 / 37 | 注册检验 | 运维 |
| 26 / 28、29 | 软件升级过程 / 升级通知与过程 | 运维 |
上下行的编号空间是两套独立的表,同一个数字在两个方向上含义完全不同:上行 7 是「终端发起双向认证」,下行 7 是「平台下发认证数据」。所以解析时永远是「方向 + 记录类型号」这个二元组才能定位业务,只看数字必错。这一点在联调阶段能让人怀疑人生。
3.4 CP56、QDS、QOI 与那几个「写死」的字段
信息体时标是 7 字节的 CP56:毫秒占 2 字节,后面依次是分、时、日、月、年(年减 2000)。年份只留 1 字节且以 2000 为基准,意味着这套格式的时间表达上限是 2255 年,够用,但也意味着跨世纪解析时不能简单按 1970 纪元处理。
品质描述词 QDS 在这套协议里规定置 0x00,召唤限定词 QOI 置 0x14。0x14 十进制是 20,对应传送原因表里「响应站总召」的语义,也就是站级总召唤。这些字段在标准 104 里是有丰富取值的(比如 QDS 的无效位、取代位、抖动位),在这里被压成常量,本质上是裁剪:桩的场景不需要那套品质语义,写死能少一批分支判断和解析错误。
信息对象地址固定 3 字节,控制方向当命令字用,监视方向当源地址用,取值范围 0x000000~0xFEFF01。上限没有取满 0xFFFFFF,留出的那段大概率是给扩展或私有用途预留的。另外还有一个充电接口标识,一机多枪时用来标记枪号,从 1 开始顺序编号;一机一枪时该项为 0。这个字段看着不起眼,但在多枪桩上如果填错,平台会把两台车的电量记到同一个枪口上,对账时就是一锅粥。
4. 三个阶段的时序:初始化帧、总召与业务包确认如何咬合
4.1 第一条报文:30 字节的协议标识帧
TCP 连上之后,桩发的第一帧不是业务数据,而是协议标识帧。平台收到后原样返回一个内容一致的确认帧。这一帧的字段是固定的,其中启动字符 68H、帧长度固定 1B00H、控制域固定 00H、类型标识固定 63H、VSQ 固定 01H、传送原因固定 0400H。
这里有个值得较真的地方:帧长度写 1B00H,如果按大端读是 0x1B00,低 11 位是 0x300,明显不合理;但按小端读成 0x001B,就是 27,正好等于「4 字节控制域 + 23 字节 ASDU」。ASDU 的 23 字节拆开来是:类型标识 1 + VSQ 1 + 传送原因 2 + 公共地址 8 + 时标 7 + 连接终端类型 1 + 接口标识 1 + 主版本 1 + 次版本 1。加上启动字符 1 和长度域 2,整帧正好 30 字节。这个自洽关系非常好用,可以拿它当链路健康度的探针:抓包看到 68H 后面跟的不是 00 1B,基本可以断定组包代码有问题。
用代码把这一帧拼出来,比对着表看更清楚:
import struct, time def bcd_terminal(no: str) -> bytes: """终端编号转 8 字节压缩 BCD,不足 16 位在尾部补 0 到 16 位""" no = no.ljust(16, "0")[:16] out = bytearray() for i in range(0, 16, 2): hi, lo = int(no[i]), int(no[i + 1]) out.append((lo << 4) | hi) # 低位数字放高半字节,联调时务必抓包核对 return bytes(out) def cp56_now() -> bytes: """7 字节 CP56:毫秒(2) 分 时 日 月 年-2000""" now = time.time() ms = int((now % 1) * 1000) t = time.localtime(now) return struct.pack("<HBBBBB", ms, t.tm_min, t.tm_hour, t.tm_mday, t.tm_mon, (t.tm_year - 2000) & 0xFF) def build_init_frame(terminal_no: str, gun_no: int = 0) -> bytes: asdu = bytes([0x63]) # 类型标识:协议标识 asdu += bytes([0x01]) # VSQ:N=1,SQ=0 asdu += struct.pack("<H", 0x0004) # 传送原因:初始化 asdu += bcd_terminal(terminal_no) # 8 字节公共地址 = 终端编号 asdu += cp56_now() # 7 字节时标 asdu += bytes([0x01]) # 连接终端类型:离散 TCU asdu += bytes([gun_no & 0xFF]) # 充电接口标识,单枪填 0 asdu += bytes([0x03, 0x00]) # 主版本 03H,次版本 00H length = 4 + len(asdu) # 期望 27 = 0x1B,覆盖控制域+ASDU apci = b"\x68" + struct.pack("<H", length & 0x07FF) + b"\x00\x00\x00\x00" return apci + asdu frame = build_init_frame("2024010100000001", gun_no=0) print(len(frame), frame[:3].hex()) # 30 68001b代码里的三个参数最容易被改错:length必须把 4 字节控制域算进去,漏掉就短 4 字节,平台会一直等剩余数据;terminal_no补位方向是尾部,不是头部;gun_no单枪必须填 0 而不是 1,多枪才从 1 开始编。版本号写死 03H 和 00H,改错的话平台可能直接回 STOPDT 拒绝服务。
4.2 总召:一次把全量遥信遥测拉平
平台用总召唤命令(类型标识 100)获取桩侧的实时监测数据,QOI 固定 0x14,即站级总召。桩收到后要回两批:先上送全遥信(类型标识 1),再上送全遥测。遥测按值长度分流,等于 2 字节走 11,大于 2 字节走 132。
为什么要有总召这条路径,而不是全靠变化上传?因为变化上传只在上线后才有效,链路断开期间的任何状态变化,平台都错过了。重连之后必须有一次全量对齐,否则平台侧的设备状态会长期漂移。这是典型的「增量 + 全量」双通道设计:日常靠变化上传省带宽,异常恢复靠总召兜底。
总召还承担了一个隐含职责——它是一次完整的状态快照,顺序不能乱。规范写的是先遥信后遥测,如果桩把顺序发反了,有些平台的解析器会一直等第一个遥信包,把后面的遥测全丢掉。
4.3 变化上传:COT 里那个「突发」的含义
数据发生变化时,桩要主动上送。变化遥信走类型标识 1,变化遥测走 11 或 132,传送原因用 3,也就是表里的「突发(自发)」。传送原因字段是 2 字节,但真正有语义的是低 6 位,高位留给 P/N 位和测试位,解析时要先做一次掩码。
变化上传的密度是需要节制的。电量这类累计量每一秒都在变,如果每次变化都发一帧,一条 2KB 的分片很快就被心跳级别的碎包占满。工程上通常的做法是设一个最小上报间隔和死区,比如电压变化超过阈值才发、或者最多每 5 秒发一次。规范没有强制这两个参数,但它决定了你在现场会不会被运营商投诉流量异常。
4.4 业务包:130 和 133 的一问一答
业务交互是严格的一问一答,但确认的方向是反的,这一点特别容易写错:
- 上报业务数据:桩发类型 130,平台回类型 133 表示确认。
- 下发业务数据:平台发类型 133,桩回类型 130 表示确认。
也就是说,同一对类型标识在两个方向上互为请求与确认,方向完全靠类型标识本身区分,跟传送原因无关。第一次读规范的人常常会去找「确认」的原因码,结果找不到——因为确认是靠类型标识表达的。
解析侧可以这么写:
def parse_business(buf: bytes): p = 0 type_id = buf[p]; p += 1 vsq = buf[p]; p += 1 n, sq = vsq & 0x7F, (vsq >> 7) & 0x01 cot = int.from_bytes(buf[p:p+2], "little") & 0x3F; p += 2 # 低 6 位才是原因 pub = buf[p:p+8]; p += 8 # 终端编号 ts = buf[p:p+7]; p += 7 # CP56 if type_id not in (130, 133, 134): # 非业务包,没有记录类型号 return type_id, cot, None, buf[p:] rec_no = buf[p]; p += 1 # 记录类型号决定后面怎么解 return type_id, cot, rec_no, buf[p:]n是信息对象个数,sq为 1 表示后续信息对象地址连续、只写一次地址,为 0 表示每个对象都带自己的地址。业务包里n基本都是 1,但遥信遥测包会用到 0 和 1 两种情况,写解析器时不能假设。
5. 排查清单:链路通、报文对、业务错,先看这五个地方
5.1 现象:加密报文永远验签失败
现象很典型——明文业务包收发正常,一旦走到需要加密的流程,比如双向认证、离线交易上送,平台侧就一直返回认证失败,桩侧日志显示「已发送、已收到响应」,但内容对不上。
原因几乎都出在字节序切换上。规范在附录里明确要求:数据项全部按小端字节序组包,加密报文部分按小端组包后送 ESAM 或密码机加密,加密后的报文要按大端字节序组包传输。也就是说,同一份数据在加密前和加密后用的是两种字节序,中间那道转换必须自己做。
解决办法是把「组包」和「加密」拆成两个函数,中间强制过一次反转,并且用固定向量做单元测试:拿一组已知明文,算出密文,再逐字节比对。我一般会在代码里写死一句注释提醒后来人——加密前后的字节序不同,别看漏了。这类问题抓包是看不出来的,密文在链路上永远是「看起来正常」的乱码,只能靠向量比对定位。
5.2 现象:大配置文件发一半就断,平台报长度非法
现象是大包发不出去,或者发出去平台解不出来,日志里出现长度域异常。
原因在 APDU 长度域只有低 11 位有效,上限 2047。一条 ASDU 的固定头就占掉 20 字节左右,实际能塞的业务数据不到 2000 字节。ESAM 密钥更新文件、软件升级包、黑名单全量下发这几类业务,原始数据动辄几十 KB,如果不分包,组包函数里那个& 0x07FF就会把高位悄悄截掉,长度域变小,平台按小长度读完把剩余字节当下一帧的帧头,从此彻底错位。
解决办法是在业务层做分片,每片控制在 1.5KB 以内留足余量,并且给每片带上序号和总片数,由接收方重组。判断有没有中招很简单:抓包看帧头第三、第四字节,如果高 5 位不是 0,说明长度被写超了。
5.3 现象:同一台桩换个平台就注册不上
现象是桩在 A 平台跑得好好的,接到 B 平台之后连初始化都过不去,平台侧压根不认这个终端编号。
原因是公共地址那 8 字节压缩 BCD 的补位规则和字节序,各家实现细节不完全一致。规范说的是「不足 16 位时,在末尾补 0」,补的是十进制位而不是字节,补完再压缩成 BCD;但字节内部高低半字节的顺序、以及整体 8 字节的排列顺序,规范只给了原则。有些实现是低位数字在高半字节,有些反过来。
解决办法是把终端编号的编解码抽成独立模块,配一组用例覆盖「短编号」「满 16 位」「含前导零」三种情况,联调第一件事就是拿这三组去和对方对。别等到业务跑通了才发现编号映射反了——那时候所有历史数据的主键都是错的,只能重来。
5.4 现象:链路空闲半小时后「假死」,重启就好
现象是桩在平台显示在线,但下发任何命令都超时,把桩重启一下立刻恢复。
原因是 t3 空闲测试帧没实现,或者实现了但没处理对端的确认。TCP 连接在没有数据时不会自己断,中间任何一跳 NAT 设备的会话表超时,连接就变成半开:桩这边以为还连着,平台那边早就没了。规范给的解法就是 t3 定时器,空闲 20 秒发一次测试帧,配合 t1 的 15 秒超时判断,两个周期内就能发现死链并触发重连。
解决办法是把测试帧当成和业务帧同等重要的东西来做,别糊弄。另外要注意 STOPDT 的处理:收到停止命令后必须回 STOPDT_CONT 并停止发数据,否则平台侧会认为设备不守规矩,直接踢连接。
5.5 现象:离线交易记录重复上传或整批丢失
现象是桩断网期间攒了十几笔离线充电记录,恢复后平台只收到一部分,或者同一笔收到了两三次。
原因是有卡离线充电这条链路依赖「上行 + 下行确认」的闭环:桩上传记录,平台回确认,桩收到确认后才把本地记录标记为已上传。如果桩在等确认的时候超时重发,而平台其实已经入库,就会重复;如果桩发完就把本地记录删了,确认丢了就永久丢失。规范里对离线交易还有额外要求——冻结 CPU 卡金额后要取钱包脱机交易序号、终端机编号、终端交易序号、交易日期时间、GMAC 等数据,交易失败时置交易标识 1 并把这些数据一并上传,这些字段正是平台侧做幂等去重的依据。
解决办法是本地记录带状态机,至少分「待上传」「已上传待确认」「已确认」三态,只有收到下行确认才推进到终态;同时把交易流水号作为幂等键透传给平台。这件事没有捷径,就是血泪经验堆出来的——丢一笔账,对账能对到你怀疑人生。
6. 设计取舍:为什么宁可牺牲可读性也要保住位级确定性
这套协议最容易被吐槽的地方是可读性。今天随手一个 JSON over WebSocket 就能解决的问题,它偏偏要用 68H 起头、11 位长度、1 字节类型标识、1 字节记录类型号,调试全靠抓包和十六进制。但把几个取舍摊开看,会发现它并不是守旧。
第一是带宽和算力。桩侧主控很多是资源紧张的嵌入式方案,一条总召响应可能要塞几十个遥信加十几个遥测。用二进制位域,一个遥信连地址带值带品质也就 5 个字节;换成 JSON,光字段名就能把一帧撑到几 KB。在 4G 流量按量计费的年代,这个差距是实打实的成本。
第二是与既有运维体系复用。104 规约的报文结构、抓包工具、解析习惯在电力远动领域已经沉淀了很多年,把它裁剪过来,意味着现成的诊断手段能直接用上,而不是让运维重新学一套。代价也很明显:公共地址从 2 字节扩到 8 字节,类型标识的语义被改造(130、132、134 这几个在标准里没有),这套协议已经不能和标准 104 直接互操作,只能算「同源不同宗」。
第三是扩展性设计。业务种类会一直长,所以留了记录类型号这个 1 字节的口子,目前用掉三十多个,还有两百多个坑位。这个设计的好处是新增业务不用动报文结构,坏处是上下行两套编号空间独立,同一个数字在两个方向上含义完全不同,解析时必须靠「方向 + 记录类型号」二元组定位,写错一个映射就是业务错乱。
第四是安全链路的字节序割裂。小端组包、送密码机加密、密文转大端传输,这个要求在规范里是明写的,但它把「字节序」这个隐形依赖暴露在了业务流程中间。任何一处遗漏都会导致验签失败,而且从链路上完全看不出来。
验证这套协议实现得对不对,我一般用三个动作:第一,抓第一帧,确认是不是 30 字节、第三第四字节是不是 00 1B,这是最快的链路探针;第二,跑一遍总召,看遥信和遥测的顺序对不对、11 和 132 的分流有没有搞反;第三,构造一笔离线交易,人为断网再恢复,看记录会不会重也不会丢。
从那以后我每次接手这类协议对接,都强制先写一版「初始化帧 + 总召 + 一笔业务」的最小闭环,然后再往上堆业务。因为这套东西的坑不在业务逻辑有多复杂,而在那些写死在规范里、又不会被编译器提醒的常量上——8 字节补零的方向、11 位长度的高位、加密前后的大小端、上下行的记录类型号。这些细节在文档里各占一行,在现场各能让人熬一个通宵。希望帮到你。
本文还有配套的精品资源,点击获取