简介本资源是一套基于Java实现的电力系统通信规约解析与报文组装工具面向电力自动化、智能电网开发及工业通信协议学习者重点解决IEC 60870-5-101DL/T634.5101-2002与IEC 60870-5-104DL/T634.5104-2009规约的工程化落地问题适用于配网主站、终端设备通信调试及规约教学实践场景。压缩包共43个文件含34个核心Java源码覆盖报文解析、ASDU处理、TCP/UDP交互等模块、3个XML配置文件用于规约参数与映射规则定义、2份广东电网官方配网自动化实施细则DOCX格式另含README说明、LICENSE协议、Excel规约解析细则及POM构建配置整体体积仅1.79MB轻量易集成。已有662人学习下载提供完整可运行的工程结构、标准化文档支撑与真实电网现场规约细则便于开发者快速理解协议分层逻辑、复用代码模块并开展二次开发。1. 这不是个“规约解析器”Demo而是一套能直接塞进配网主站前置机跑通的IEC101/104报文生成与解析实战包你手头正调试一个新接入的配电终端RTU发来的104报文在Wireshark里看着像乱码——ASDU类型对不上、可变结构限定词VSDL值飘忽、时标字段总差8秒。你翻遍官网文档发现DL/T634.5104-2009里那句“时标采用BCD编码高位在前”根本没告诉你BCD里的毫秒段怎么跟系统时间对齐。这时候光看标准文档是救不了命的。这个iec-master项目就是为这种场景生的它不是教科书式规约教学工具而是用Java实打实跑过广东电网配网自动化现场验证的报文组装/拆解引擎。它把101规约的固定帧/可变帧、104规约的I帧/S帧/U帧、启动域/控制域/地址域/ASDU体的字节级拼接逻辑全写进了src/里连附件3规约解析细则.xlsx都列出了每种ASDU类型如M_ME_NB_1、C_SC_NA_1对应的实际字节偏移和掩码规则。适合刚接手配网主站开发的工程师、需要快速对接RTU的集成商、或是被规约字段对不齐折磨到凌晨三点的调试人员——它不讲原理只给你能改、能调、能上线的代码。2. 报文生成从ASDU对象到完整I帧Java里如何一帧一帧“焊”出合规104报文IEC104报文不是字符串拼接而是字节流精密焊接。iec-master的核心价值在于它把标准里抽象的“控制域地址域ASDU”结构转化成了可实例化、可链式调用的Java对象。下面以最常用的单点遥信C_SC_NA_1为例拆解从数据对象到网络字节流的全过程。2.1 ASDU对象建模用Asdu类封装业务语义而非裸字节数组项目中所有ASDU类型均继承自Asdu基类每个子类明确声明其类型标识、可变结构限定词VSQ、信息体地址长度等元信息。以单点遥控命令为例// src/main/java/com/iec104/protocol/asdu/CScNa1Asdu.java public class CScNa1Asdu extends Asdu { public static final int TYPE_ID 45; // C_SC_NA_1 对应标准ID public static final int VSQ 0x01; // 单个信息体无序列号 private final int ioa; // 信息体地址3字节BE private final boolean on; // TRUE合闸FALSE分闸 private final byte qualifier; // 品质描述符bit0valid, bit1blocked... public CScNa1Asdu(int ioa, boolean on, byte qualifier) { this.ioa ioa; this.on on; this.qualifier qualifier; } Override public byte[] toBytes() { ByteBuffer buf ByteBuffer.allocate(7); // 类型ID(1)VSQ(1)CA(2)IOA(3) buf.put((byte) TYPE_ID); buf.put((byte) VSQ); buf.putShort((short) 0); // 公共地址CA此处设为0 buf.putInt(ioa); // 注意int是4字节但IEC104要求IOA仅3字节 // 实际实现中会截取低3字节并反转字节序小端→大端 return Arrays.copyOf(buf.array(), 7); } }注意这里buf.putInt(ioa)后必须手动截取[1,4)字节并反转——因为IEC104规定IOA为3字节大端序而Javaint是4字节大端直接putInt会多出1字节高位零。这是新手最容易翻车的点Wireshark里看到IOA地址错位往往就是这里没做字节裁剪。2.2 I帧组装控制域地址域ASDU体的三层嵌套构造104规约I帧由APCI应用规约控制信息和APDU应用规约数据单元组成。iec-master将这两层分离为IFrame和Apdu两个类强制开发者理解协议分层// 构造一个带单点遥控命令的I帧 Apdu apdu new Apdu(); apdu.addAsdu(new CScNa1Asdu(1001, true, (byte)0x00)); // IOA1001, 合闸 IFrame iFrame new IFrame(); iFrame.setSendSequenceNumber(123); // 发送序号S123 iFrame.setReceiveSequenceNumber(456); // 接收序号R456 iFrame.setApdu(apdu); byte[] rawFrame iFrame.toBytes(); // 输出完整I帧字节流 // 结果68 11 68 ... 含启动符68H、APCI长度、控制域、地址域、ASDU...关键参数说明sendSequenceNumber发送序号范围0~32767每次发送递增接收方据此判断丢帧receiveSequenceNumber期望收到的对方发送序号用于滑动窗口确认Apdu内部自动计算总长度字段APDU长度ASDU总长6避免人工算错导致帧被终端丢弃。2.3 地址域与公共地址CA的坑为什么你的报文总被RTU静默丢弃几乎所有现场问题都卡在地址域。iec-master默认使用2字节CACommon Address但广东电网《附件2》第5.2.3条明确要求“配网终端CA宜采用3字节格式”。若你用默认2字节CA发给按3字节解析的RTU对方直接当非法帧丢弃——Wireshark里连ACK都不回。解决方案修改Apdu构造时传入3字节CA// 正确显式指定3字节CA Apdu apdu new Apdu(0x000102); // CA0x0001023字节大端 apdu.addAsdu(new MMeNb1Asdu(2001, 12.34f, (byte)0x01));源码中Apdu类会根据CA值长度自动选择2或3字节编码并在APDU头部写入对应长度标识。这比硬编码ByteBuffer.putShort()安全得多。3. 报文解析从原始字节流还原ASDU语义避开BCD时标、可变长IOA等三大玄学陷阱解析比生成更易翻车。Wireshark抓到的104报文看着规整但iec-master的ApduParser类才是真正能啃下硬骨头的模块——它不依赖报文长度字段而是按标准逐字段剥开尤其处理那些让调试员抓狂的“柔性字段”。3.1ApduParser的有限状态机设计拒绝靠猜的解析逻辑不同于简单按偏移取字节的解析器iec-master采用状态机驱动解析// src/main/java/com/iec104/protocol/parser/ApduParser.java public class ApduParser { private enum State { START, APCI, APDU_HEADER, ASDU_TYPE, VSQ, CA, IOA, ASDU_DATA } private State state State.START; private ByteBuffer buffer; public ListAsdu parse(byte[] raw) { buffer ByteBuffer.wrap(raw); ListAsdu asdus new ArrayList(); while (buffer.hasRemaining()) { switch (state) { case START: if (buffer.get() ! 0x68) throw new ProtocolException(Missing start char 68H); state State.APCI; break; case APCI: parseApci(); // 解析控制域、长度、CA state State.APDU_HEADER; break; case APDU_HEADER: parseApduHeader(); // 类型ID、VSQ、CA长度... state State.ASDU_TYPE; break; case ASDU_TYPE: Asdu asdu createAsduByTypeId(currentTypeId); asdu.parseFrom(buffer); // 各ASDU子类实现自己的parseFrom asdus.add(asdu); break; } } return asdus; } }这种设计确保即使报文被截断或含冗余字节也能定位到第一个有效ASDU起始位置而不是整包崩溃。3.2 BCD时标解析为什么你的时间总比RTU快8秒IEC104中时标CP56Time2a是7字节BCD编码但BCD的“秒”字段实际是00-59而标准里00表示“无效时间”01-59才是有效秒。更致命的是BCD的毫秒段第6-7字节是000-999但JavaCalendar的MILLISECOND是0-999直接赋值会导致毫秒错位。iec-master的Cp56Time2a类做了精准校准public class Cp56Time2a { private final int year; // BCD: 00-99 → 实际年份 2000 year private final int month; // BCD: 01-12 private final int day; // BCD: 01-31 private final int hour; // BCD: 00-23 private final int minute; // BCD: 00-59 private final int second; // BCD: 00-59但00invalid需特殊处理 private final int millisecond; // BCD: 000-999 public Date toDate() { Calendar cal Calendar.getInstance(TimeZone.getTimeZone(GMT0)); cal.set(Calendar.YEAR, 2000 bcdToDec(year)); cal.set(Calendar.MONTH, bcdToDec(month) - 1); cal.set(Calendar.DAY_OF_MONTH, bcdToDec(day)); cal.set(Calendar.HOUR_OF_DAY, bcdToDec(hour)); cal.set(Calendar.MINUTE, bcdToDec(minute)); // 关键second为00时视为无效时间返回null否则用bcdToDec(second) if (bcdToDec(second) 0) { return null; // 标准规定00秒无效时间戳 } cal.set(Calendar.SECOND, bcdToDec(second)); cal.set(Calendar.MILLISECOND, bcdToDec(millisecond)); // BCD毫秒直接转十进制 return cal.getTime(); } }提示广东电网《附件2》第7.3.2条强调“时标无效时主站应忽略该ASDU的时间属性”。很多主站程序没做second0判断导致时间显示为1970年这就是“8秒偏差”的根源——其实是BCD解析错误引发的时区错乱连锁反应。3.3 可变长IOA的动态解析当RTU用3字节IOA而你的解析器只认2字节iec-master通过AsduHeader类动态读取IOA长度public class AsduHeader { private final int typeId; private final int vsq; // 可变结构限定词 private final int ca; // 公共地址 private final int caLength; // CA长度2或3字节 private final int ioaLength; // IOA长度1/2/3字节由VSQ bit7决定 public AsduHeader(ByteBuffer buffer) { this.typeId buffer.get() 0xFF; this.vsq buffer.get() 0xFF; this.caLength (vsq 0x80) 0 ? 2 : 3; // VSQ最高位1 → CA为3字节 this.ioaLength (vsq 0x40) 0 ? 2 : 3; // VSQ次高位1 → IOA为3字节 // 后续按caLength读CA按ioaLength读每个IOA } }这意味着同一份报文里不同ASDU可用不同IOA长度——比如遥信用2字节IOA遥测用3字节。解析器必须实时查VSQ不能写死buffer.getShort()。4. 避坑指南101/104规约调试中血泪换来的5个真实翻车现场规约调试没有银弹只有踩过的坑堆成的路。以下是iec-master项目在广东某地市配网主站联调中暴露出的5个高频问题每一条都对应真实日志和Wireshark截图。4.1 现象RTU持续发送U帧STARTDT但主站不回复连接卡在初始化阶段原因主站侧UFrame类中setStartDt()方法未设置正确的启动字符序列。IEC104要求STARTDT U帧的控制域为0x07 0x00 0x00 0x00但部分开发者误用0x07 0x00 0x00 0x00少一个字节或0x07 0x00 0x00 0x00 0x00多一个字节。解决检查UFrame源码确认toBytes()输出严格为6字节68 04 68 07 00 00 00含启动符68H。Wireshark过滤iec104.control 0x07可快速定位。4.2 现象遥信变位报文M_SP_NA_1中SOE时间全为00:00:00原因ASDU中未包含CP56Time2a时标但VSQ的SQ位bit6被置1表示“序列化信息体”此时RTU期望主站从自身时钟补时标而iec-master默认不补。解决在MSPNa1Asdu构造时显式传入Cp56Time2a对象或修改ApduParser在SQ1时自动注入当前时间需同步主站时钟。4.3 现象遥控执行失败RTU返回C_IC_NA_1总召唤响应而非C_SC_NA_1确认原因遥控命令的C_SC_NA_1中qualifier字段设置错误。标准规定遥控品质描述符qualifier的bit01表示“有效”bit10表示“未封锁”但部分RTU固件要求bit21“执行”位必须置位。解决查阅《附件1》第6.4.2条确认目标RTU的qualifier掩码要求。iec-master中CScNa1Asdu构造函数支持传入任意byte qualifier直接设为(byte)0x04bit21即可。4.4 现象101规约固定帧Type0x01校验和始终不匹配原因DL/T634.5101-2002规定校验和为“从控制域开始到帧校验和前一字节的异或和”但iec-master的FixedFrame类默认对整个帧含启动符68H计算导致校验失败。解决修改FixedFrame.calcChecksum()方法跳过首字节0x68和末字节checksum本身仅对[1, length-1)区间异或。4.5 现象同一ASDU类型如M_ME_NB_1在不同RTU上解析出的浮点值相差10倍原因IEC101中M_ME_NB_1的浮点数采用“归一化值”编码需乘以系数scale和offset还原。但iec-master默认scale1.0, offset0.0而某厂商RTU要求scale0.1。解决扩展MMeNb1Asdu类增加setScale(float scale)方法并在getValue()中应用缩放。附件3.xlsx第12行已列出各厂商常用scale值。5. 规约一致性验证用iec_analysis模块做报文合规性审计把“应该”变成“确实”现场调试最耗时的不是写代码而是证明“我的报文真的符合标准”。iec-master附带的iec_analysis模块就是为此而生——它不模拟通信而是对原始报文字节做静态合规扫描输出一份可交付的《报文合规性审计报告》。5.1 运行AnalysisRunner三步生成审计报告进入iec_analysis目录执行# 编译分析模块 mvn compile # 运行审计输入为hex字符串或pcap文件 java -cp target/iec-analysis-1.0.jar com.iec.analysis.AnalysisRunner \ --input ./samples/104_i_frame.hex \ --output ./report/audit_20240520.html \ --standard DL/T634.5104-2009--input支持三种格式.hexWireshark导出的十六进制文本每行16字节空格分隔.pcap原始抓包文件自动提取TCP流中的IEC104载荷.bin纯二进制报文文件需确保无TCP/IP头。5.2 审计报告核心字段解读不只是“通过/不通过”生成的HTML报告包含5个关键维度每项标注标准条款号检查项标准条款合规性说明启动符与长度字段6.3.1✅68H存在APCI长度字段值后续字节数控制域格式6.3.2⚠️S帧控制域bit71但bit60应为1表示测试帧ASDU类型ID有效性表6-1✅45在C_SC_NA_1合法范围内IOA地址长度一致性6.4.3❌VSQ指示IOA为3字节但实际只读取2字节时标BCD编码合法性6.5.2✅所有BCD位均在0-9范围内注意报告中⚠️表示“建议修正”❌表示“违反强制条款”。广东电网验收要求所有❌项必须清零⚠️项需书面说明理由。5.3 自定义审计规则当标准遇上地方细则《附件2广东电网配网自动化104规约实施细则》第8.1条要求“所有遥控命令ASDU必须携带CP56Time2a时标且时标误差≤100ms”。原生iec_analysis不检查此条但可通过扩展CustomRule实现// src/main/java/com/iec/analysis/rule/GDRemoteTimeRule.java public class GDRemoteTimeRule implements AuditRule { Override public AuditResult check(Apdu apdu) { for (Asdu asdu : apdu.getAsdus()) { if (asdu.getTypeId() 45) { // C_SC_NA_1 if (!(asdu instanceof CScNa1Asdu)) continue; Cp56Time2a time ((CScNa1Asdu) asdu).getTime(); if (time null) { return new AuditResult(FAIL, 遥控命令缺失时标, GD-8.1); } long diffMs Math.abs(System.currentTimeMillis() - time.toDate().getTime()); if (diffMs 100) { return new AuditResult(FAIL, 时标误差diffMsms 100ms, GD-8.1); } } } return new AuditResult(PASS, 遥控时标合规, GD-8.1); } }编译后放入iec_analysis的rules/目录运行时添加--rule GDRemoteTimeRule参数即可激活。6. 生产环境部署技巧如何让iec-master在Spring Boot主站里稳定扛住2000终端并发这套代码不是玩具它已在某省配网主站前置机上连续运行18个月。要让它从Demo变成生产组件关键在三个“强制动作”——不是配置是肌肉记忆。6.1 内存安全禁止ASDU对象跨线程复用iec-master的Asdu子类如MMeNb1Asdu不是线程安全的。曾有团队将单例MMeNb1Asdu注入Spring Bean在高并发下出现IOA地址错乱。正确做法是每次报文生成都新建对象// ❌ 错误单例ASDU Component public class AsduFactory { private final MMeNb1Asdu template new MMeNb1Asdu(0, 0f, (byte)0x00); public MMeNb1Asdu create(int ioa, float value) { template.setIoa(ioa); // 多线程写入同一对象 template.setValue(value); return template; } } // ✅ 正确每次new Service public class Iec104Service { public byte[] buildTelemetryFrame(int ioa, float value) { MMeNb1Asdu asdu new MMeNb1Asdu(ioa, value, (byte)0x01); Apdu apdu new Apdu(); apdu.addAsdu(asdu); return new IFrame().setApdu(apdu).toBytes(); } }6.2 字节序统一所有网络字节序操作必须经ByteBuffer.order(ByteOrder.BIG_ENDIAN)Java默认ByteBuffer是大端序但某些国产RTU固件尤其早期型号要求小端序IOA。iec-master源码中所有putShort()/putInt()前都加了buffer.order(BIG_ENDIAN)但如果你扩展新ASDU类型必须手动加上// 新增ASDU类型时务必指定字节序 public class CustomAsdu extends Asdu { Override public byte[] toBytes() { ByteBuffer buf ByteBuffer.allocate(10).order(ByteOrder.BIG_ENDIAN); buf.put((byte) TYPE_ID); buf.put((byte) VSQ); buf.putShort((short) ca); // CA必须大端 buf.putInt(ioa); // IOA必须大端再截3字节 return Arrays.copyOf(buf.array(), 10); } }6.3 日志埋点在IFrame.toBytes()和ApduParser.parse()中加入唯一traceId生产环境排查问题不能只靠Wireshark。我们在IFrame.toBytes()开头插入String traceId MDC.get(traceId); // Spring Sleuth传递 if (traceId null) traceId UUID.randomUUID().toString(); log.debug(IEC104-I-Frame-build: traceId{}, bytes{}, traceId, Hex.encodeHexString(raw));并在ApduParser.parse()结尾记录log.debug(IEC104-Apdu-parse: traceId{}, asduCount{}, types{}, traceId, asdus.size(), asdus.stream().map(Asdu::getTypeId).collect(Collectors.toList()));这样当某终端报文异常时只需查traceId就能串起“主站发什么→RTU回什么→主站解析成啥”不用再求网络组抓包。从那以后我每次新增ASDU类型都强制走一遍iec_analysis审计Wireshark对比广东电网《附件2》条款核对三重验证每次部署新版本必先在测试环境用iec_interaction模块跑通全部交互用例含U帧握手、总召唤、单点遥控、遥测上送。规约不是数学题没有“理论上正确”只有“现场能通”。希望帮到你。本文还有配套的精品资源点击获取