1. 为什么调试工具里永远要留一扇手动打开的“后门”
先讲一个我最近遇到的实际场景。某天下午,现场网关设备的控制逻辑还没联调完,但我们这边的前置机程序已经具备下发命令的条件了。对方提了一句“你们可以先发一条合闸试试,看看链路通不通,我们这边抓包先确认一下上送方向”。这时候我手边的工具里没有任何现成的报文模板,临时去改代码又要把整个功能重新编译打包,既不现实也没必要。最靠谱的办法,就是直接在调试工具的报文管理面板里手动新增一条发送报文,填好字段点发送。
这个场景不是我一个人遇到。做电力规约、工业通信、物联网设备调试的工程师,尤其是跟前置机、采集器、RTU打交道比较多的,基本都会碰到三类必须手动新增报文的时刻:
第一类,对端的业务逻辑还没实现完,但链路和基础解析已经通了,需要用一条手动构造的报文去验证主站到设备的物理链路、Socket连接、防火墙端口、对端收包线程是不是正常。这时候你要是没有手动入口,就只能干等着对端功能写完。
第二类,要故意构造一些不太“正常”的报文,比如超长的数据域、异常的校验码、重复的序列号,用来测试对端的健壮性,看它会不会崩、会不会误处理、会不会把错误数据当成有效业务。模板化的工具往往约束太多,你填进去一个错误值它可能直接弹窗拦你,只有手动模式能原样发出去。
第三类,联调过程中偶发一个难以复现的问题,你手里只有一条历史报文的十六进制字节,想要精确回放一遍,看看对端当时到底接收成什么样。这种时候必须有一个“逐字节输入”的入口,而不是依赖一组固定字段的模板。
所以我说这是“后门”。它不是日常使用最频繁的功能,但等到需要它的时候,没有它就只能彻底卡死。现在我们用的报文调试平台里保留了这个入口,我在前几节介绍了平台的整体界面、连接管理和报文模板库的导入方式,这一节开始正式讲怎么从零到一新增一条发送报文。
要特别说明的一点是,手动新增和模板发送的本质差别,不在于“填表的方式不同”,而在于心智模型不同。模板发送是你站在“我已经知道这条报文的用途”的前提下去配置,手动新增则是你站在“我只想验证一个假设”的前提下去构造。前者适合批量生产,后者适合探索和排障。两者不该互相替代,而是互补。
2. 动手之前先拆清楚:一条报文里到底有哪些字段
先别急着打开工具填数据。手动新增报文最容易犯的错误,就是连帧结构都没理清就往界面上填字节,填完发给对端根本解析不出来,你还以为是工具问题。我得先把帧格式讲清楚。
我们这个调试平台对接的系统采用一种自定义规约,链路层走TCP,一次发送一个完整帧。帧结构是下面这样:
| 字段 | 长度 | 取值示例 | 说明 |
|---|---|---|---|
| 帧头 | 2字节 | AA 55 | 固定标识,对端用它来同步帧边界 |
| 长度 | 2字节 | 08 00 | 小端序,表示长度字段之后到校验码之前的字节数 |
| 类型 | 1字节 | 03 | 0x01心跳、0x02读遥测、0x03控制、0x04参数配置 |
| 设备地址 | 2字节 | 01 00 | 小端序,目标设备的逻辑地址 |
| 数据域 | N字节 | 01 00 00 00 | 业务内容,长度随类型变化 |
| 校验码 | 1字节 | 0D | 累加和校验,取低8位 |
这套结构不是行业通用标准,但思路很典型。很多类似的内部系统都长这样:先来个固定帧头,再给个长度字段,然后是类型、地址、业务数据,最后跟一个校验。你要是理解了这套逻辑,换到别的规约上也是一样的思路,只是字段名和长度有变化。
逐个字段说。帧头的作用是让接收方在字节流里找到“从这一帧从哪里开始”。TCP是流式传输,不像串口一帧一帧有明确的边界,接收方必须靠帧头来识别起点。所以帧头一般选0xAA55这类具有比较明显比特特征的值,尽量和普通数据区分开。
长度字段很多人理解不到位。它表示的不是“整个帧的长度”,而是“从长度字段之后开始算,到校验码为止,一共有多少字节”。为什么这样设计?因为接收方在读到帧头之后,必须赶紧知道后面要收多少数据,才能把缓冲区准备好。如果长度的定义是“整个帧长度”,那接收方还需要先知道帧头占了几个字节,多一次换算。所以工程上一般都约定成“长度字段之后的字节数”,简洁直接。
类型字段决定了对端该怎么解释后面的数据域。同一条链路上可能跑着心跳、遥测、控制、配置多种帧,对端收到帧之后第一个分发依据就是类型。设备地址则是多设备级联时的路由依据,前置机知道哪台设备挂在哪个Socket下。
数据域最灵活。控制帧里可能放合闸/分闸命令字、执行序号、操作原因码;遥测帧里可能放起始地址、读取数量。没有固定长度,完全取决于类型。最后是校验码,常见的有累加和和CRC两类,我们这套用的是累加和:把长度字段之后到校验之前的每一个字节求和,取低8位,对端用同样的方式重算一遍,不一致就丢帧。
咱们用一条具体的合闸控制帧来走一遍。假设目标设备地址是1号机,数据域内容是“合闸命令字=01,序列号=0000”,整帧写成十六进制就是:
AA 55 08 00 03 01 00 01 00 00 0D
从前往后拆:AA 55是帧头;08 00是小端长度,算一下长度字段之后到校验之前,有类型(1字节)+设备地址(2字节)+数据域(4字节)=7字节,不对,8 是小端序的0x0008,也就是8字节,但后面加上校验码应该是1+2+4+1=8,正好。类型03是控制帧;设备地址01 00小端表示0x0001;数据域4个字节是控制字和序列号;最后的0D是校验。
这里还没涉及到一个关键概念:字节序。很多人第一次手动填报文就栽在这里。小端序的意思是低字节在前,比如设备地址0x0001在报文里写成01 00,不能写成00 01。源地址、长度、数据里的数字量如果超过1字节,都要确认是哪种字节序。这个坑我放到后面自检部分专门讲,先有这个意识就行。
3. 实测:在报文调试平台里新增并发送一条报文
背景交代清楚,现在进入实际操作。我这个调试平台的界面大致分三块:左侧是设备和连接列表,中间是报文管理区,右侧是收发日志区。报文管理区默认有好几个分组,什么“心跳组”“遥测组”“控制组”,但我们现在不用这些现成分组,直接从空白开始。
第一步,打开顶部菜单栏的“报文管理”,在下拉里找到“新增报文模板”,点击之后会弹出一个新建窗口。注意窗口标题是“模板”,但这里的模板只是承载我们手工输入内容的载体,不填自动化逻辑,本质还是手动模式。窗口打开后,先给它起一个容易识别的名字,比如“手动-合闸测试-1号机”,千万别起什么“test01”这种名字,不然第二天你自己都认不出那是干嘛的。
第二步,填写帧头。有些工具的帧头是自动带出来的,默认就是AA55,不用改。如果不是自动带出,就手动输入AA55两个字节。这里有一个小细节,界面上的输入框是按“字节”来分割的,每个框填两个十六进制字符,不需要再自己加空格。
第三步,填写长度字段。这一步通常是整个界面里最能体现“工具友好度”的地方。我们这个平台支持两种模式:自动计算和手动锁定。自动计算模式下,长度字段会根据你后面填写的类型、地址、数据域自动刷新,不需要你操心。但我建议你在初学阶段不要依赖自动计算,先把手动锁定打开,自己算一遍长度填进去,然后肉眼对比一下自动计算的结果。为什么要这样?因为很多人就是栽在长度上,工具帮你算好了你反而不理解它为什么是这个值,出问题的时候永远不会意识到长度字段已经错了。
第四步,选类型。把类型字段填成03,代表控制帧。如果你不确定某个类型值代表什么,把鼠标悬停在字段旁边,一般会有提示,或者直接查系统里的规约文档。这一步我强烈建议你把类型值和对应的业务含义做一个自己的速查表,尤其是频繁和设备厂家打交道的人,不同厂家的类型定义很可能完全不同。
第五步,填设备地址。按小端序输入01 00。这个地址得和左侧连接的设备地址严格对应,如果填错了,对端可能直接按路由表查不到设备,然后回一个“设备不存在”的错误帧。
第六步,填数据域。控制字填01代表合闸,序列号填0000。这里我想多说一句,数据域里的每个字段含义完全由规约决定,工具不会帮你校验,你要额外小心。
第七步,保存模板。保存之后,左侧分组里会出现刚才新建的这条“手动-合闸测试-1号机”,双击这一行,右侧日志区闪了一下“预发送报文已载入”,但此时还没有真正发出去。这一步很重要——很多初学者以为填完保存就算发了,等到对端问起来才发现根本没人收到。
第八步,真正发送。我倾向于在报文管理里右键这条记录,选择“立即发送”,而不是双击。因为双击有时候会进入编辑状态,不太好区分。点击“立即发送”之后,会再弹出一个确认框,确认框里会显示目标设备当前选中的Socket连接和目的IP、端口,同时还会再把这条报文的完整十六进制展示一遍。你确认无误后点发送。
发送按钮按下去的瞬间,右侧日志区会刷新一行新的日志,包括发送时间、报文内容、发送结果字节数。如果对端有回复,几十毫秒后还会收到一条对端的响应帧日志。那天我们实测平台连的是本机回环地址加一个模拟端口,所以点完发送不到50毫秒,日志区就出现了“发送成功(12字节)”的字样。
整个流程走下来,核心就一句话:手动新增是一条流水线操作,填字段、核对长度和校验、选发送方式、看日志,每一步都不能少。工具有时候会把一些字段自动计算好,那是福利,不是让你忽略原理的理由。
4. 发送前的三分钟自检:长度、校验、大小端
这一节我想展开讲三个高频坑,因为它们不是“偶尔会错”,而是“几乎每个人都至少错过一次”,而且错的形态极其隐蔽,发出去不一定报错,但对端解析就是不对。
第一个坑是长度字段里的“从哪到哪”问题。还是拿合闸帧说事。假设你填了帧头AA55,长度08 00,类型03,地址01 00,数据域1字节(只填了控制字,序列号忘了填),那整帧变成AA 55 08 00 03 01 00 01 0A这样的长度。此时长度字段还写着08,但实际长度字段之后的字节数是类型1+地址2+数据1+校验1=5,对端按8字节去收,会把下一帧的帧头也当成本帧数据,整个链路就错乱了。更麻烦的是,TCP流模式下,错位后不会自动恢复,必须连续多个帧头重新对齐才行。所以发送前第一件事,就是重新数一遍长度字段后面的字节数(含校验码),然后看长度字段的十六进制值和它是否一致。
第二个坑是校验算法。我们这套用累加和:从长度字段的第1个字节开始,一直加到校验之前的那个字节,所有字节求和取低8位。还拿刚才那帧来说,08 00 + 03 + 01 00 + 01 00 00 00 = 0x08 + 0x00 + 0x03 + 0x01 + 0x00 + 0x01 + 0x00 + 0x00 + 0x00 = 0x0D,所以校验码是0D。看起来简单,但很多人在“从哪个字节开始求和”上理解不同。有些系统的校验从帧头开始,有些从长度字段开始,还有些对数据域每个字节做异或而不是求和。你要是换了系统还沿用旧习惯,发出去的帧基本都会被丢弃。我的经验是:发送前先明确你这套规约的校验范围,亲手算一遍再发。工具上有自动校验计算器,但只是辅助,你得能通过手动计算把结果验证一遍。
第三个坑也是最大的坑,字节序。我在这一节开头说的那个“地址字段填反”的案例,真实发生在我同事身上。当时他要给2号设备发控制指令,设备地址按小端应该填02 00,他填成了00 02。工具没有报任何错误,因为工具只检查格式,不检查业务语义。对端收到后,发觉地址不在本端路由表里,回了一帧错误码。他对着错误码查了半天,最后把日志里的帧逐字节对比,才发现是地址的高低位反了。遇到这种问题,唯一的办法就是给自己做一个自检习惯:凡是超过1字节的数值字段,先问自己一句这是大端还是小端。通常在规约文档里都有明确说明,但就是没人看。
我把这三项整理成一个“发送前自检清单”,每次手动发送前对着过一遍:
- 长度字段的值是否等于长度字段之后到校验之前的实际字节数?
- 校验范围是否按规约要求正确划定?手动计算的校验值和工具计算值是否一致?
- 所有多字节字段的字节序是否按规约要求排列?
- 数据域的每个子字段长度是否和类型定义吻合?
- 帧头是否被意外修改过?
- 目标设备地址是否和当前连接匹配?
这六项检查完大概花不到三分钟,但能避免绝大多数低级错误。我见过不少工程师花两个小时在抓包上找错,最后发现就是田字格多填了一个字节。
5. 报文发出之后,怎样确认它真的被对端收到
发送点的瞬间提交成功,日志显示“发送成功(12字节)”,但这不代表对端真的收到了。我现在养成了一个习惯:发送之后不只依赖工具自身的日志,还会从另一个视角确认链路,尤其是在和外部系统联调的时候。
最直接的办法是抓包。因为我们这套系统走TCP,在本地就能抓回环流量。用抓包工具选择回环接口,设置过滤器为tcp端口加对端端口,然后点发送,就能看到这一帧的原样内容。抓包的价值在于,它反映的是真正落到网卡上的字节,不管工具内部怎么处理,网卡上是什么样才是什么样。我记得有一次工具界面上显示的报文明明是完整的,但抓包一看数据段少了一位,最后发现是工具发送线程里把一个缓冲区截断了。如果没有抓包对照,这个bug可能要带到现场才能暴露。
第二个办法是对端的应用日志。很多前置机程序会打印接收日志,格式类似“收到连接数据,长度12字节,类型03,地址01”。把对端日志和工具日志放在一起对比,不仅能确认链路通不通,还能确认对端是不是按预期解析了。如果你发出去的帧和对方解析出来的字段不一致,那问题基本出在字段定义上,不太可能出在网络传输上。
第三个办法是看回复帧。在联调阶段,只要对端能正常解析你的控制帧,理论上会回一个确认帧,确认帧里一般带同样的序列号或者操作结果码。你最好把“发送帧”和“响应帧”的序列号对应起来,这样整个链路就形成了一个闭环验证。
这里我要分享一个我踩过的坑。有一次联调,第一批手动报文发送很成功,对端也回了确认。但第二次、第三次发送同样的内容,对端就不再回任何东西了。排查了很久,发现是数据域里的序列号字段没有递增。对端有一个防重放机制:同一序列号只处理一次,后续相同的序列号直接丢弃。我用的模板序列号固定写成0000,第一次发出去被处理,第二次再发就会被判定为重复帧。解决方法是把模板数据域里的序列号改成递增,或者勾选工具里的“发送后自动加一”选项。这个细节后来我在自己的工具模板里做了固话,每次手动发送都实时更新序列号值。
所以我现在发送报文的固定流程是这么走:手动构造好一条报文,先在界面上核对长度和校验,点发送后马上看工具日志,接着开抓包工具验证网卡字节,最后看对端日志的解析结果和回复帧。三个确认点都通过,这条报文才算是真正“打通”了。
这节内容是一个起点,手动新增发送报文这个能力,往深了说还能衍生出定时发送、连续压力发送、乱序发送等测试手段。我在实际使用中体会到,手动模式最大的价值不是“代替模板”,而是逼着你去理解每一条报文到底由什么组成、为什么这么组成。等你能熟练地在空白的报文管理面板里构造出对端能正确解析的帧时,再回去看模板、看自动生成工具,思路会清晰很多。下一节我打算讲把这个“手动新增”的能力扩展到定时发送和周期轮询里,看看怎么在手动基础上搭建一个简单的自动测试流程。