尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Netty的Java IEC 104规约监听与报文解析实践

基于Netty的Java IEC 104规约监听与报文解析实践 简介这份资源面向电力行业或工业通信领域的Java开发人员用于实现IEC 60870-5-104即电网104规约协议的主站连接与数据监听。资源基于Spring Boot构建包含主站连接、监听器及运行入口能够将104协议报文解析为直观的十进制数据便于快速接入电网调度或变电站自动化系统。压缩包共5个文件涵盖3个Java源码文件、1个Maven依赖说明readme.md及1个已打包的j60870-1.4.0.jar整体大小仅105KB轻量易部署。目前已有1411人学习/下载。文件中重写了toString()方法以整理关键监听数据并演示了通过POST请求将数据转发给客户端的处理思路同时附带了所需jar包和pom依赖说明读者可按需注释或修改降低上手门槛。适合需要快速搭建104规约客户端原型或进行协议调试的初中级Java工程师参考。1. 为什么要在Java里做104规约监听干电力自动化的朋友对IEC 60870-5-104规约习惯叫104规约肯定不陌生。只要是调度主站和变电站之间传数据基本绕不开这个协议。它的全称是“采用标准传输协议子集的IEC 60870-5-101网络访问”简单说就是把101规约的报文搬到TCP/IP网络上跑默认端口2404。我先说清楚一件事104规约监听和调度主站本身是两回事。监听程序不参与控制、不抢主站功能它只是在TCP链路上把报文“接住”解析出里面包含的遥测、遥信、遥控数据存起来或者转发给其他系统用。我做过不少这样的需求比如第三方系统要读取变电站的实时数据但不能直接动调度系统于是单独起一个Java服务作为通信副站把数据接出来。为什么选Java而不是C当时的考虑很直接——团队里Java人多而且项目后续要把数据接进消息队列和数据库Java生态更顺。再加上104规约本身是状态机驱动、报文定长解析Java的NIO框架Netty处理TCP粘包拆包非常成熟完全撑得住变电站这种每秒几十帧的报文量。这篇文章就基于我实际做过的一个项目来写。它监听模拟调度端和变电站前置机之间的104链路把实时数据解析后落到本地文件和内存队列便于其他模块消费。适合刚入手电力规约开发的同行也适合正在为第三方数据接入头疼的朋友。我会把报文结构、监听架构、解析代码、踩坑记录全部展开讲不说废话直接能抄作业。2. 104规约报文结构必须先吃透2.1 帧格式和三种帧类型104规约的帧格式不算复杂但细节多。我先给一个整体认知它有两种帧长一种是固定帧长6个字节用于心跳和确认一种是可变帧长至少12个字节用于传数据。可变帧长的结构是启动字符68H、帧长从控制域第一个字节开始算到ASDU结束、控制域4个字节、ASDU应用服务数据单元。这里容易搞混的就是帧长字段的计数起点后面我会专门列一个示例。控制域的第一个字节的低2位决定了帧类型00I帧信息传输帧带发送序号和接收序号是传数据的主力01S帧监视帧只带接收序号用于确认11U帧控制帧用于启动/停止/测试链路最常见的TESTFR心跳就是它我用一个生活化的类比来解释这3种帧的关系I帧就像你给朋友发微信消息S帧是“已读回执”U帧是“你在吗”这种连接测试。104链路要正常工作三样缺一不可。2.2 ASDU类型标识决定了数据含义ASDU里最关键的是类型标识TypeID字段它告诉接收方这帧数据是什么。我在项目里遇到最多的就这几种类型标识1M_SP_NA单点遥信一个bit表示一个开关/刀闸状态类型标识13M_ME_NC短浮点遥测4字节float用于电压、电流、功率类型标识45C_SC_NA单点遥控主站下发合分闸命令类型标识100C_IC_NA总召唤命令链路建立后主站拉全量数据这里有个实际开发中很容易踩的坑类型标识13的短浮点字节序是反的必须做反转才能用Java的Float.intBitsToFloat()解析。我第一次没注意这个解析出来的电压值全是天文数字排查了半天才发现是字节序的问题。2.3 公共地址和信息体地址的解析规则ASDU里除了类型标识还有公共地址通常指站地址2字节、传送原因2字节、信息体地址3字节但在短帧里可能只占1字节或2字节。信息体地址对应的是数据在站内的点号——比如“1号主变A相电压”可能就对应地址4097。强烈建议在动手写解析器之前先找一套真实的104报文抓包看一眼。没有真实报文也简单用模拟器生成。我一般是用开源工具模拟变电站端主动上送遥测遥信帧这样调试解析逻辑特别方便。报文在手比看十遍规范都有用。3. Java监听程序的环境搭建与整体架构3.1 开发环境与依赖我的项目环境如下给你做个参考JDK 8以上我用的是JDK 11语法上够用就行Netty 4.1.x处理TCP粘包拆包和通道管理Spring Boot 2.x做数据落库和对外接口非必需但省事日志用Logback异步输出防止高并发时IO阻塞如果只是做纯监听和转发不落库那连Spring Boot都可以省直接Netty 主循环就够。但后面要接消息队列或者存历史数据Spring Boot就划算了。这个取舍看你项目的边界在哪里。3.2 基于Netty的监听服务设计Netty做服务端监听104端口不算复杂。核心就三步配置ServerBootstrap绑定2404端口初始化ChannelPipeline加入解码器、业务处理器维护一个ChannelGroup用于管理所有接入的104链路我把Netty选型的原因说一下104规约的TCP报文存在粘包半包问题Netty的ByteToMessageDecoder让我们可以自定义拆包规则基于帧长字段精确地把一条条完整报文切出来。这个功能自己用原生Socket实现也不难但Netty把这些都封装好了还自带线程池模型省去了很多并发细节。注意接入104链路的方向要搞清楚。调度主站是TCP Server还是Client取决于具体场景。我做的这个项目里Java程序是Server端模拟变电站设备主动连上来。实际项目中如果Java程序要连真实的前置机那就要作为Client反向连接。两种模式对应的代码写法不同但ChannelPipeline的处理逻辑完全一样。4. 核心实现数据监听与报文解析的完整代码4.1 服务端监听与拆包解码器下面这段是我项目里的解码器核心代码。它的职责是从TCP字节流中切出完整的104报文帧public class Iec104FrameDecoder extends ByteToMessageDecoder { Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, ListObject out) { // 等待可读字节数满足最小帧长 if (in.readableBytes() 6) { return; } in.markReaderIndex(); byte start in.readByte(); if (start ! 0x68) { // 启动字符不对说明链路错位重置 in.resetReaderIndex(); in.skipBytes(1); return; } short len in.readUnsignedByte(); // 长度字段是从控制域第一个字节开始计算的长度 int totalLength len 2; if (in.readableBytes() len) { in.resetReaderIndex(); return; } ByteBuf frame in.readRetainedSlice(totalLength - 2); out.add(frame); } }这段代码有两个关键点。第一判断启动字符0x68不是就跳过1个字节这能解决TCP字节流乱序导致的错位。第二长度字段读取后要等足够字节数再截取否则半包就会导致解析失败。Netty的ByteToMessageDecoder天然支持这种“攒够再发”的机制。4.2 报文解析器的实现细节拿到完整帧之后要按帧类型、ASDU类型逐层解析。我写了一个工具类重点解析遥测和遥信。下面是解析短浮点遥测的核心片段public static ListTelemetryData parseTelemetry(ByteBuf frame) { ListTelemetryData list new ArrayList(); frame.skipBytes(4); // 跳过控制域 int typeId frame.readUnsignedByte(); frame.skipBytes(1); // 可变结构限定词 int cause frame.readUnsignedShort(); // 传送原因 int commonAddr frame.readUnsignedShort(); // 公共地址 // 读取信息体 while (frame.isReadable()) { int infoAddr frame.readUnsignedMedium(); // 3字节信息体地址 int raw frame.readInt(); float value Float.intBitsToFloat(Integer.reverseBytes(raw)); byte qds frame.readByte(); // 品质描述符低3位为0表示有效 boolean valid (qds 0x03) 0; list.add(new TelemetryData(infoAddr, value, valid, commonAddr, cause)); } return list; }这段代码里的关键一行是Integer.reverseBytes(raw)这个反转转换就是我前面说的字节序坑。104规约里短浮点使用小端序而Java的Float.intBitsToFloat()期望的是大端字节序不反转解析出来的float就是错的。品质描述符qds的处理也值得展开说。qds的低3位分别代表是否被取代、是否溢出、是否被封锁第7位是IV位无效标志。我做数据质量判断时只取了低3位是否为空闲但这在实际工程中可以做得更细——比如把IV位置位的数据单独标记不参与统计分析避免给调度人员错误的遥测值。4.3 链路状态管理与心跳处理104规约的心跳机制很典型。主站会周期发送U帧TESTFR激活0x68 0x04 0x43 0x00 0x00 0x00从站必须回复TESTFR确认0x68 0x04 0x83 0x00 0x00 0x00。Java监听程序也要遵守这个机制否则对端会认为链路断了。我的做法是用Netty的IdleStateHandler空闲5秒触发一次心跳事件发送TESTFR帧。这段逻辑放在自定义的ChannelInboundHandler里Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent event (IdleStateEvent) evt; if (event.state() IdleState.WRITER_IDLE) { // 发送TESTFR激活帧 ByteBuf test ctx.alloc().buffer(6); test.writeByte(0x68); test.writeByte(0x04); test.writeBytes(new byte[]{(byte) 0x43, 0x00, 0x00, 0x00}); ctx.writeAndFlush(test); } } }这个业务还有一个隐形要求I帧的发送序号和接收序号要正确维护。104规约用序号做消息确认和排序如果序号错乱对端会一直不确认导致数据重传和链路重置。我的做法是维护两个AtomicInteger发I帧时自增发送序号收I帧时校验接收序号不匹配就打印告警日志。5. 实战中的六大常见问题与排查技巧5.1 常见问题速查表先把我在项目里遇到的典型问题整理成表方便你对照现象可能原因解决办法链路一直建立不起来TCP连接被拒绝或防火墙拦截检查2404端口监听状态telnet测通能连上但收不到数据没有发送启动帧或总召唤命令链路建立后先发STARTDT激活再发总召唤解析出来的电压值是天文数字短浮点字节序没反转Integer.reverseBytes后再转float程序运行一会儿自动断线心跳超时未收到响应检查U帧TESTFR响应处理逻辑数据点号全是乱的信息体地址偏移没按规约调整对照点表确认信息体地址计算规则收到数据但质量位一直异常品质描述符解析错误或站端数据本身异常打印qds原始值定位5.2 粘包与半包的解码器踩坑记104报文在TCP层传输时会存在多条报文粘在一起或者一条报文被拆成多个TCP包的情况。Netty解码器能自动攒数据但有个细节容易忽略——报文错位。我遇到过一种情况链路中间有一帧数据意外丢失了几个字节导致后续所有帧的0x68定位错位解码器把错误的字节流当成帧头发送。如果只判断帧头不判断长度就会出现解析出超长垃圾数据。解决思路是在解码器里加一个“最大值保护”如果帧长字段超过255实际上可变帧最长也就253视为非法主动断开连接重新建链。这个方法简单粗暴但特别有效可以避免链路长时间处于错乱状态。提示如果对接的站端设备比较老主动上送的报文频率不高可以把I帧序号校验告警开成debug级别别直接打印info否则日志量会特别大。5.3 链路闪断与数据连续性保障104链路偶尔闪断是很正常的关键是断线后如何处理。我的做法是监听程序检测到channelInactive后不立即清空历史数据而是保留最近5秒的数据在内存缓冲里等链路重新建立、总召唤完成后先补发缓冲区的数据再发新的实时数据。这套“补数机制”在监控场景下特别有用能显著减少数据空洞。另外总召唤的处理也有讲究。链路刚激活时站端会主动上送一遍全量数据这时候我一般会把数据直接覆盖写入本地快照文件而正常运行时则走增量更新。这个设计思路避免了对实时库的频繁全量写操作降低了IO压力。5.4 从应用到性能优化的几点心得104监听的性能瓶颈通常不在解析而在数据落盘。我实际测试过当站端有上千个遥测点、每帧带多个信息体时数据量很容易达到每秒几千条。此时如果每条都走一次数据库INSERT连接池会被打爆。我的优化方案是解析完的数据先进入一个内存阻塞队列由单独的一个消费者线程批量批量比如每条200条或者500ms刷一次库。这个模式代码量不大但效果立竿见影。还有个细节是SQL要用批量INSERT别一条条执行——MyBatis的foreach标签就能实现效率差距一般在5倍以上。6. 经验总结与扩展建议6.1 我在实际项目中摸索出的几条经验第一规约开发一定要先把报文结构画出来再写代码。我在纸上画了三张图——帧结构图、ASDU结构图、状态转换图后面写代码没走多少弯路。你拿到的第一份源码别急着跑先把类型标识、传送原因、信息体地址这几个字段的偏移地址用注释标清楚后面排查会快很多。第二注意线程模型的隔离。Netty的IO线程只做解码和分发千万不要在IO线程里做数据库操作或远程调用否则一旦阻塞会拖垮整个链路的处理能力。我的做法是IO线程把解析结果放到队列里业务线程池消费。第三日志要打关键点但别打太多。链路建立、总召唤完成、断线重连、解析异常这四类必打遥测数据每条都打会把磁盘写爆。我设置了一个开关调试期全量打印上线后只保留异常日志这个开关通过配置中心控制不用重新发版。6.2 后续可以扩展的方向如果只做一个监听程序那价值有限。我做完基础监听后又扩展了三个方向把解析好的数据通过MQTT推给上层应用实现移动端实时查看把历史数据写入时序数据库供后续做趋势分析和告警预测加入规约一致性测试模式反向模拟主站用于测试新接入的站端设备特别是第三个方向对于需要经常接入新变电站的团队来说非常实用。改造一下程序让它能发起总召唤、下发遥控命令、解析站端响应就变成了一个轻量级的规约测试工具。我后来很多现场联调工作都是靠这个“同一套代码、两种模式”的方案搞定的省了不少工作量。说回监听程序本身——它真正的价值不只是“把数据拿出来”而是让你在不影响现网调度业务的前提下拥有了一个稳定的数据出口。别小看这一点在电力这种对稳定性要求极高的行业里能够安全旁路地拿到实时数据很多以前不敢做的分析、展示、告警功能就都能落地了。希望这篇文章能帮你减少一些入门弯路。本文还有配套的精品资源点击获取
返回列表