简介:本资源是一套基于Java实现的IEC104协议主站客户端程序,属于微电网管理系统的一部分,面向从事电力自动化、配电监控及微电网系统开发的工程师与学习者。程序围绕IEC104这一基于TCP/IP的电力行业通信协议展开,涵盖U帧、S帧、I帧三种帧格式的解析与应答式数据传输逻辑,可实现遥信、遥测上行采集与遥控、遥调下行控制,帮助读者理解主站客户端与从站服务端之间的通信机制。压缩包共105个文件,约3.59MB,包含30个class、23个java源码、22个png图示、10个json配置、9个jar依赖及properties、xml等工程文件,源码与依赖齐全,便于直接导入调试。目前已有734人学习下载,适合作为微电网通信模块开发、协议解析学习与工程二次开发的参考实例。
1. IEC104_microgrid:一个Java实现的IEC104主站客户端,微电网管理系统的通信底座
如果你在电力自动化或新能源行业待过,大概率听过IEC 60870-5-104这个规约。它跑在TCP之上,用来做变电站和调度主站之间的远动通信。微电网管理系统里,主站需要实时采集逆变器、储能PCS、电表等设备的数据,同时下发功率调度指令——这套通信链路的核心,就是一个稳定的IEC104主站客户端。IEC104_microgrid这个项目,用Java把主站侧的协议栈完整实现了一遍,包括APCI帧解析、ASDU报文组装、总召唤、遥测遥信上送处理、遥控遥调下发等。它适合两类人:一是做微电网EMS/SCADA的Java后端,需要把104协议集成进自己的系统;二是想学电力规约怎么落地到代码里的开发者,拿它当参考实现比啃规约文档快得多。下面我从项目结构、核心代码、运行方式到二次开发,把它拆开讲一遍。
2. 项目结构与技术选型:为什么用Java做104主站
2.1 整体模块划分
这个项目的代码组织遵循典型的通信中间件分层思路,从上到下大致分为四层:
- 应用层:对外暴露的API接口,业务系统通过它来发送总召唤、遥控、遥调命令,以及注册数据上送的回调。
- 协议层:IEC104的核心实现,包括APDU拆包/组包、ASDU类型识别与分发、时序控制(t1/t2/t3定时器)、序号管理(发送序号VS和接收序号VR)。
- 传输层:基于Netty或原生Socket的TCP客户端,负责与RTU/网关建立长连接、断线重连、心跳维持。
- 数据层:遥测(YC)、遥信(YX)、遥控(YK)、遥调(YT)的数据模型和缓存,通常用ConcurrentHashMap按信息对象地址(IOA)索引。
常见做法是把协议层做成无状态的编解码器,传输层只管字节流收发,应用层通过回调或Future拿到结果。这样换传输框架(比如从Netty换成Mina)时,协议层代码不用动。
2.2 技术栈与依赖
项目基于Java 8+,构建工具用Maven。核心依赖通常包括:
| 依赖 | 用途 |
|---|---|
| Netty 4.x | TCP长连接、编解码链、心跳 |
| SLF4J + Logback | 日志,规约调试时关键 |
| FastJSON / Jackson | 如果需要把采集数据转JSON上送 |
| JUnit | 协议编解码的单元测试 |
选Netty而不是原生Socket,主要因为104主站需要处理粘包/拆包、空闲检测、断线重连这些事,Netty的LengthFieldBasedFrameDecoder和IdleStateHandler能省不少代码。当然,如果项目规模很小,用Socket加一个读线程也能跑,但维护成本会高一些。
2.3 目录结构参考
一个典型的目录长这样:
iec104-microgrid/ ├── src/main/java/ │ ├── com/microgrid/iec104/ │ │ ├── frame/ # APCI帧结构:I帧、S帧、U帧 │ │ ├── asdu/ # ASDU类型定义与编解码 │ │ ├── session/ # 会话管理、序号控制、定时器 │ │ ├── transport/ # Netty客户端、重连策略 │ │ ├── model/ # 遥测遥信遥控数据模型 │ │ └── api/ # 对外接口 │ └── resources/ │ └── logback.xml ├── src/test/java/ # 编解码测试 └── pom.xml这个结构的好处是,你拿到源码后能快速定位:要改协议细节去frame和asdu,要改连接行为去transport,要加新功能在api层扩展。
3. 核心协议实现:从APCI帧到ASDU报文的完整链路
3.1 APCI帧的三种类型与编解码
IEC104的APDU由APCI(控制域)和ASDU(数据单元)组成。APCI固定6字节:1字节启动字符0x68,1字节长度,4字节控制域。控制域决定了帧类型:
- I帧:信息传输帧,控制域第1位为0,包含发送序号VS和接收序号VR。
- S帧:确认帧,控制域第1位为1、第2位为0,只含接收序号VR。
- U帧:控制帧,控制域第1位为1、第2位为1,用于启动、停止、测试。
下面是一个I帧的编码示例:
// I帧编码:控制域4字节 = VS左移1位 | VR左移1位 public byte[] encodeIFrame(int vs, int vr, byte[] asduBytes) { int ctrl1 = (vs & 0x7FFF) << 1; // 发送序号,bit0固定0 int ctrl2 = (vr & 0x7FFF) << 1; // 接收序号,bit0固定0 ByteBuffer buf = ByteBuffer.allocate(6 + asduBytes.length); buf.put((byte) 0x68); // 启动字符 buf.put((byte) asduBytes.length); // APDU长度(不含启动字符和长度本身) buf.put((byte) (ctrl1 & 0xFF)); // 控制域第1字节 buf.put((byte) ((ctrl1 >> 8) & 0xFF)); // 控制域第2字节 buf.put((byte) (ctrl2 & 0xFF)); // 控制域第3字节 buf.put((byte) ((ctrl2 >> 8) & 0xFF)); // 控制域第4字节 buf.put(asduBytes); return buf.array(); }这里的关键参数是vs和vr,它们各占15位,范围0~32767,超过后回绕到0。发送每一条I帧后VS加1,收到对端I帧后VR加1。如果序号对不上,说明链路出了问题,需要触发链路复位。实际调试时,序号错乱是最常见的翻车点之一,后面避坑章节会细说。
3.2 ASDU类型识别与分发
ASDU是104的载荷,类型标识(TypeID)决定它是什么报文。微电网场景下常用的类型有:
| TypeID | 含义 | 方向 |
|---|---|---|
| 100 | 总召唤命令 | 主站→从站 |
| 101 | 总召唤确认 | 从站→主站 |
| 103 | 时钟同步 | 主站→从站 |
| 9 | 归一化遥测值 | 从站→主站 |
| 13 | 浮点遥测值 | 从站→主站 |
| 1 | 单点遥信 | 从站→主站 |
| 45 | 单点遥控命令 | 主站→从站 |
| 49 | 归一化遥调命令 | 主站→从站 |
分发逻辑通常用一个Map把TypeID映射到处理器:
// ASDU分发器:根据类型标识路由到对应处理器 public class AsduDispatcher { private final Map<Integer, AsduHandler> handlers = new HashMap<>(); public void register(int typeId, AsduHandler handler) { handlers.put(typeId, handler); } public void dispatch(AsduFrame frame) { AsduHandler handler = handlers.get(frame.getTypeId()); if (handler == null) { log.warn("未注册的ASDU类型: {}", frame.getTypeId()); return; } try { handler.handle(frame); } catch (Exception e) { log.error("ASDU处理异常, typeId={}", frame.getTypeId(), e); } } }AsduFrame里封装了类型标识、可变结构限定词、传送原因、公共地址、信息对象地址列表和具体值。处理器注册在会话初始化时完成,比如把TypeID=9注册为遥测处理器,收到后更新本地缓存并触发业务回调。
3.3 总召唤流程与时序控制
总召唤是主站上线后的第一个动作,用来获取从站的全部数据。流程是:主站发总召唤命令(TypeID=100,传送原因=6激活),从站回确认(TypeID=101),然后从站依次上送遥信、遥测数据,最后发一个总召唤结束(TypeID=101,传送原因=10激活终止)。
时序控制靠三个定时器:
- t1:发送I帧后等待确认的超时,默认15秒。超时未收到S帧或I帧确认,断开连接。
- t2:收到I帧后最迟发送S帧确认的时间,默认10秒,必须小于t1。
- t3:链路空闲时发送测试帧的间隔,默认20秒。
// 简化的t1定时器:发送I帧后启动,收到确认后取消 public void startT1Timer() { t1Future = scheduler.schedule(() -> { log.error("t1超时,链路可能断开,触发重连"); session.close(); reconnect(); }, t1Millis, TimeUnit.MILLISECONDS); } public void onAckReceived() { if (t1Future != null) { t1Future.cancel(false); t1Future = null; } }这三个定时器的值在规约里有推荐范围,但实际项目中要根据网络质量调整。t1设太短,网络抖动就断链;设太长,真断了半天才发现。我一般会在配置里暴露出来,现场调试时用。
4. 运行与调试:把主站客户端跑起来
4.1 环境准备与配置
跑这个项目需要JDK 8以上、Maven 3.6+。如果只是本地验证协议逻辑,不需要真实的RTU设备,可以写一个模拟从站的测试类,或者用开源的104模拟器。配置项通常放在application.properties或YAML里:
iec104.master.host=192.168.1.100 iec104.master.port=2404 iec104.master.commonAddress=1 iec104.master.t1=15000 iec104.master.t2=10000 iec104.master.t3=20000 iec104.master.reconnectInterval=5000commonAddress是公共地址,也叫ASDU地址,用来区分不同从站。微电网里如果有多台设备,每台的公共地址不能重复。
4.2 启动流程与关键日志
启动类通常长这样:
public class Iec104MasterApplication { public static void main(String[] args) { Iec104Config config = Iec104Config.load("application.properties"); Iec104Master master = new Iec104Master(config); // 注册数据上送回调 master.registerTelemetryListener((ioa, value, quality) -> { log.info("遥测上送: IOA={}, value={}, quality={}", ioa, value, quality); // 业务处理:写入时序库、判断越限等 }); master.registerTelemetryListener((ioa, value, quality) -> { log.info("遥信上送: IOA={}, value={}", ioa, value); }); master.start(); // 建立TCP连接,发送STARTDT,启动总召唤 } }启动后日志里应该能看到:TCP连接建立、发送U帧STARTDT、收到STARTDT确认、发送总召唤命令、收到总召唤确认、遥测遥信数据陆续上送、总召唤结束。如果卡在某一步,对照日志定位。
4.3 用模拟器验证
没有真实设备时,可以用Python写一个简易的104从站模拟器来验证主站行为。核心是监听2404端口,收到总召唤后回确认并构造几条遥测数据:
# 简易104从站模拟:仅用于验证主站总召唤流程 import socket, struct def build_asdu(type_id, cause, common_addr, ioa, value): # 构造ASDU:类型标识+可变结构限定词+传送原因+公共地址+信息对象地址+值 asdu = struct.pack('BBBHB', type_id, 1, cause, common_addr, ioa) asdu += struct.pack('f', value) # 浮点遥测 return asdu s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 2404)) s.listen(1) conn, addr = s.accept() print('主站已连接:', addr) # 后续处理STARTDT、总召唤、数据上送...这个模拟器不用写完整,能回总召唤确认和几条数据就够了。主站收到数据后打印日志,说明协议链路通了。
5. 避坑与常见问题:调试104主站时踩过的坑
5.1 序号错乱导致链路复位
现象:运行一段时间后,日志里出现“接收序号不连续”或“发送序号确认异常”,然后链路断开重连。
原因:VS和VR是15位循环序号,每发一个I帧VS加1,每收一个I帧VR加1。如果代码里在重发或异常分支里多加了或漏加了序号,就会错位。另一个常见原因是S帧确认没有及时发出,对端t1超时后断开。
解决:把序号管理集中到一个类里,所有增减操作走同一个方法,加日志打印每次变化。收到I帧后立即更新VR并检查是否需要发S帧,不要拖到业务处理完再发。
5.2 总召唤数据不完整
现象:总召唤后只收到部分遥测,或者总召唤结束帧一直不来。
原因:从站上送数据是分帧的,一帧ASDU里可能包含多个信息对象。如果解析时只取了第一个IOA,后面的就丢了。另外,总召唤结束的判断要看传送原因是否为10(激活终止),不能只看TypeID。
解决:解析ASDU时遍历所有信息对象,每个都回调。总召唤结束单独处理,收到后标记本轮总召唤完成。如果长时间没收到结束帧,可以设一个超时,超时后重新发起总召唤。
5.3 遥控命令没有返回到位信息
现象:下发遥控命令后,从站回了激活确认,但一直没有激活终止或遥控返校信息。
原因:104遥控是“选择-执行”两阶段,先发选择命令(传送原因=6),收到确认后再发执行命令(传送原因=6),最后从站回激活终止(传送原因=10)。如果代码里把选择和执行合并成一步,或者没等确认就发执行,从站会拒绝。
解决:把遥控流程拆成状态机:选择→等待选择确认→执行→等待执行确认→等待终止。每个状态设超时,超时后回滚。
5.4 网络抖动导致频繁断连
现象:现场网络不稳定时,主站频繁重连,数据采集断断续续。
原因:t1设得太短,或者重连间隔太短,导致还没恢复就再次发起连接。
解决:t1适当放大到20~30秒,重连间隔用指数退避,第一次5秒,第二次10秒,最多到60秒。同时开启TCP的keepalive,让操作系统帮忙检测死连接。
5.5 公共地址配错导致数据对不上
现象:连接正常,总召唤也有响应,但遥测值全是0或者明显不对。
原因:公共地址(ASDU地址)配错了,主站发的总召唤指向了错误的从站,或者从站上送的数据公共地址和主站预期的不一致,被过滤掉了。
解决:抓包看ASDU里的公共地址字段,和从站配置核对。微电网里如果通过网关汇聚多台设备,网关可能会做地址映射,要确认映射关系。
6. 二次开发:扩展点与改造思路
拿到这个项目后,二次开发通常集中在几个方向:接入新的设备类型、对接自己的数据平台、增加协议扩展。
扩展ASDU类型:如果设备用了非标准类型标识,在AsduDispatcher里注册新的处理器即可。处理器实现AsduHandler接口,在handle方法里解析自定义载荷。注意保持编解码的无状态性,不要在处理器里持有会话状态。
对接时序数据库:遥测数据上送频率可能很高,直接写库会成瓶颈。我一般会在回调里先写内存队列,再用批量线程写入InfluxDB或TDengine。队列用Disruptor或LinkedBlockingQueue都行,关键是批量提交,比如每500条或每1秒刷一次。
增加WebSocket推送:如果前端需要实时看数据,可以在数据回调里加一个WebSocket广播。用Netty的ChannelGroup管理所有前端连接,收到遥测后遍历发送。注意做背压,前端消费慢的时候别把内存撑爆。
协议一致性测试:改完协议层代码后,一定要跑一遍编解码的单元测试。我习惯用抓包工具录一段真实的104报文,存成二进制文件,测试时读进来解码再编码,对比字节是否一致。这个后悔药能省掉很多现场调试时间。
配置热更新:现场调试时经常要改t1/t2/t3或公共地址,如果每次都要重启就太慢了。可以把配置放在一个volatile字段里,加一个管理接口或者监听配置中心的变化,运行时动态生效。但序号和会话状态不能热更新,改了必须重连。
性能考量:单主站连接下,104的吞吐量通常不是瓶颈,几千个点位的总召唤也就几秒。但如果一个主站要连几十个从站,就要考虑用连接池或异步IO。Netty的EventLoopGroup线程数要调,默认的CPU核数×2在连接数多的时候可能不够。
从那以后我每次改完协议层代码,都强制走一遍“单元测试→模拟器联调→抓包对比”三步,不再直接连现场设备试。希望帮到你。
本文还有配套的精品资源,点击获取