news 2026/10/26 2:09:09

Java版IEC104主站实现与工程避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java版IEC104主站实现与工程避坑

简介:面向工业自动化、电力采集与能源管理领域的 Java 开发者,这份 IEC104 规约主站程序围绕 IEC 60870-5-104 2002 版实现,可用于读写符合西门子 IEC104 协议的电表与网关,帮助解决主站侧通信建立、报文交互、数据采集与设备联调等实际问题。压缩包共 253 个文件,约 10.17MB,包含 86 个 java 源码与 97 个 class 文件,便于查看实现并直接运行验证;22 个 jar 提供依赖库,db 与 sql 文件对应数据存储脚本,xml、properties、springbeans、conf 等承载工程与协议参数配置,bat、exe 及 dll 则用于启动部署和本地组件调用,整体构成较完整的主站工程。页面显示已有 4176 人学习下载,说明其在 IEC104 主站开发与电表网关对接场景中具有一定参考价值。读者可据此梳理主站程序结构、理解规约报文处理逻辑,并在此基础上进行二次开发、接口调试与现场问题排查,也可作为规约学习与联调对照材料。

1. 工业通信里的 IEC104 主站:为什么值得单独写一个 Java 版本

做过电力、能源、轨交这类上位系统的开发者大概都有过类似经历:现场给了一份 IEC104 规约点表,要求三天内把主站侧跑通,结果手头只有 C++ 或 C# 的组态软件,Java 后端根本接不进去。IEC104 主站程序要处理的就是这一类任务——通过 TCP 连上 RTU 或子站,完成总召唤、遥测遥信上送解析、遥控下发、时钟同步这些动作,本质上是一套带明确报文格式的异步通信中间层。Java 版本的价值不在于重造轮子,而在于能直接嵌进 Spring 服务、能把规约层和业务层拆干净。这份资源面向的就是需要把 IEC104 接入自有平台的 Java 开发者。

2. 项目定位与技术选型:为什么用纯 Socket 手撕 APDU 而不是套框架

IEC104 在协议栈上并不复杂:应用层是 APDU,下面挂 ASDU,再往下是 TCP 承载。真正让人头疼的是它的“状态机”特性——序号 N(S)、N(R) 必须严格自增和确认,t1/t2/t3 三个定时器管着发送确认、接收确认和空闲心跳,u 帧负责 STARTDT/STOPDT/TESTFR。这些如果用现成框架包一层,调试时出了错你会非常被动,报文对不对、序号有没有跳、超时是谁触发的,全都藏在黑匣子里。

2.1 技术栈构成与分层思路

这个项目走的是轻量路线,核心依赖很少,基本可以按下面这套理解:

层次承担职责常见技术选择
传输层TCP 长连接、断线重连JDKSocket/SocketChannel
帧解析层拆 APDU、区分 I/S/U 帧自写字节解析工具
ASDU 层类型标识、可变结构限定词、信息体解析枚举 + 工厂方法
调度层总召唤、定时心跳、超时重发ScheduledExecutorService
业务层点位映射、事件回调接口回调 / 观察者

选 JDK 原生 Socket 而不是 Netty,理由很实际:IEC104 是单连接、低频次、报文短小的场景,一个链路每秒撑死几百帧,Netty 那套 EventLoop 在这里属于过度设计,还会让字节缓冲区的读写变得绕。用DataInputStream配ByteArrayOutputStream反而直观,出问题时打个断点就能看清每一个字节。

2.2 关键参数与规约默认值

规约里这些参数不是随便定的,现场调试翻车十有八九出在这里:

参数含义常用取值
t0连接建立超时30s
t1发送后等确认超时15s
t2收到 I 帧后发确认时限10s(须小于 t1)
t3链路空闲发 TESTFR 的周期20s
k未确认 I 帧最大数量12
w收到 w 个 I 帧后必须确认8

约束关系是t2 < t1、w <= 2k/3这类经验配比。很多现场设备只认默认值,你改之前最好先问清楚对端配置,否则会出现“我这边发得好好的,对方一直不认”的玄学现象。

2.3 帧结构先认清楚,后面才不会乱

IEC104 的 APDU 只有两种长度:控制域 4 字节的 I/S 帧,和只有 6 字节的 U 帧。起始字节固定0x68,第二个字节是长度(最大 253)。判型看控制域第 1 字节的最低位:

// 依据控制域首字节低两位判断帧类型 // I帧: bit0=0 S帧: bit0=1,bit1=0 U帧: bit0=1,bit1=1 public static FrameType parseType(byte ctrl1) { int v = ctrl1 & 0x03; if (v == 0x00) return FrameType.I; if (v == 0x02) return FrameType.S; return FrameType.U; }

这段判断是整条链路的地基。注意 I 帧的发送序号占 2 字节、接收序号占 2 字节,都要& 0xFFFF后处理,且有 15 位有效(最高位固定 0),序号到 32767 要回绕到 0。这一点在长时间挂机测试时最容易暴露:跑几小时后突然全乱了,多半是回绕处理漏了。

3. 目录结构与核心模块拆解:一条报文从进来到落库的完整链路

拿到代码先看目录,能省一半时间。主站程序按职责切分得比较干净,典型结构长这样:

iec104-master/ ├── src/main/java/ │ ├── net/ # 连接管理、心跳、重连 │ ├── frame/ # APDU 编解码、帧类型 │ ├── asdu/ # 类型标识解析、信息体 │ ├── session/ # 会话状态机、序号管理 │ ├── service/ # 总召唤、遥控、对时 │ └── handler/ # 业务回调接口 └── src/main/resources/ └── points.yaml # 点位映射表

3.1 连接管理与链路建立

链路建立顺序是有讲究的,不能上来就发总召唤:TCP 连上 → 发 STARTDT_ACT → 收到 STARTDT_CON → 才开始数据传输。少一步对端直接丢弃你的 I 帧。

public void connect() throws IOException { socket = new Socket(); socket.connect(new InetSocketAddress(host, port), T0); // t0 连接超时 socket.setTcpNoDelay(true); // 关掉 Nagle,否则小报文会攒包 in = new DataInputStream(socket.getInputStream()); out = new DataOutputStream(socket.getOutputStream()); sendUFrame(UType.STARTDT_ACT); // 请求启动数据传输 state = LinkState.WAIT_STARTDT_CON; scheduler.scheduleAtFixedRate(this::idleCheck, T3, T3, TimeUnit.SECONDS); }

setTcpNoDelay(true)是血泪经验,默认开启 Nagle 会让 6 字节的 U 帧在缓冲区里等,对端等不到心跳就判你链路超时。idleCheck负责 t3 周期内没发过帧时补一个 TESTFR_ACT,对端回 TESTFR_CON 即认为链路活。断线重连不要用while(true) { connect() }死循环猛冲,容易把对端连接队列打满,退避 1s、2s、5s、10s 递增更稳。

3.2 APDU 编解码:I 帧的拼装与拆解

发送 I 帧时,控制域里塞的是发送序号和接收序号,后面跟整个 ASDU。这里最容易写错的是字节位:

public byte[] buildIFrame(int sendSeq, int recvSeq, byte[] asdu) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write(0x68); // 启动字符 bos.write(asdu.length + 4); // 长度 = 控制域4 + ASDU // 发送序号:低8位在前,高8位左移1位(最高位保留) bos.write(sendSeq & 0xFF); bos.write((sendSeq >> 8) & 0x7F); // 接收序号:同样低位在前 bos.write(recvSeq & 0xFF); bos.write((recvSeq >> 8) & 0x7F); bos.write(asdu, 0, asdu.length); return bos.toByteArray(); }

参数说明:sendSeq每次发完自增取模 32768,recvSeq是当前已收到的最后一个 I 帧序号。第二个字节写asdu.length + 4,别写成+6,否则对端按长度切包会把下一帧的头部吃进去,表现出的现象是“偶尔丢帧、越跑越乱”。

接收侧反过来走:先读 2 字节定长头,校验0x68,再按长度读剩余部分,交给parseType分流。注意读流要做“攒包”处理,TCP 是字节流,一次read不一定拿到完整一帧。

3.3 ASDU 解析与总召唤

总召唤是主站上线后第一个业务动作,发的是 C_IC_NA_1(类型标识 100),传送原因 6(激活)。子站回一帧确认,然后连续上送类型标识 1/3/9/11/13 等遥信遥测数据,最后一帧带传送原因 10(激活终止)。

// 构造总召唤 ASDU:类型标识100,可变结构限定词1,传送原因6 byte[] buildGeneralInterrogation(int commonAddr) { ByteArrayOutputStream b = new ByteArrayOutputStream(); b.write(100); // 类型标识 C_IC_NA_1 b.write(0x01); // SQ=0, 信息体个数=1 b.write(0x06); // 传送原因低字节,6=激活 b.write(0x00); // 传送原因高字节 b.write(commonAddr & 0xFF); // 公共地址低字节 b.write((commonAddr >> 8) & 0xFF); b.write(0x00); // 信息体地址低 b.write(0x00); b.write(0x00); // 信息体地址高 b.write(0x14); // QOI=20 站总召唤 return b.toByteArray(); }

解析回包时,可变结构限定词 SQ 位决定信息体是“连续寻址”还是“每个都带地址”。SQ=0 时每个信息体前都有 3 字节地址,SQ=1 时只有第一个带地址,后续按地址递增。这块写错的话,点位会整体错位三字节,遥测值全部变成天文数字——典型踩坑现场。点位映射建议单独放points.yaml,用信息体地址当 key 去查业务点位号,规约层和业务层就彻底解耦了。

3.4 遥控下发与超时重发

遥控走的是 SELECT(传送原因 6)→ 执行(传送原因 7)两步,中间等子站确认,t1 内不回就得重发。遥控这种带副作用的操作千万别重发两次执行帧:

public void control(int ioa, boolean on) { sendIFrame(buildControlAsdu(ioa, on, Cause.SELECT)); // 先选择 pending = ControlCtx.of(ioa, on); // 记下待确认 timeoutTask = scheduler.schedule(() -> { if (pending != null) { pending = null; log.warn("选择超时 ioa={}", ioa); } }, T1, TimeUnit.SECONDS); }

收到选择确认后才发执行帧,执行帧超时只记录告警、不自动重发,交人工确认。这条规则建议写进代码注释里,否则后来接手的人一优化就把重发加上了。

4. 跑起来:本地联调与用模拟子站验证

规约类项目最怕的就是只有主站没有子站,压根没法验。务实做法是本地先起一个模拟子站,或者用抓包对照。

4.1 编译与启动

# 编译打包,跳过测试加快首次启动 mvn clean package -DskipTests # 指定对端地址和端口启动 java -jar target/iec104-master.jar --host=127.0.0.1 --port=2404

2404 是 IEC104 的注册端口,本地联调时对端也起在 2404 即可。启动后日志里应该依次出现:TCP connected → STARTDT_ACT sent → STARTDT_CON received → General Interrogation sent。四步齐了才算链路正常,缺哪步就查哪段。

4.2 用 Wireshark 对报文

规约调试绕不开抓包。在过滤器里输入tcp.port == 2404,把“解码为”设成 IEC60870-5-104,抓到的帧会被拆成类型标识、传送原因、公共地址、信息体地址,一目了然。我一般会重点看三处:总召唤的激活终止帧有没有来、遥测值跟你点表能否对上、S 帧的接收序号是不是每次都加对了。抓包比读日志可靠得多,尤其是序号回绕这类问题,日志根本看不出来。

提示:抓包建议在链路两端的机器上同时抓,主站侧看到的发送序号和子站侧收到的序号对不上的话,问题多半在中间网络设备做了拆包重装。

4.3 点位表配置与首轮验证

把points.yaml里的点位跟现场点表逐条核对,重点看信息体地址和数据类型(归一化值、标度化值、短浮点)。首轮验证别一上来就跑全站,先挑三五个遥信、两个遥测,确认变位能上送、总召唤值正确,再放全量。见过太多项目全量一挂上去,日志刷屏把真正的解析异常淹掉了。

5. 避坑与排查:五条现场踩出来的记录

5.1 总召唤发出去没反应

现象:日志显示已发送总召唤,对端没有任何回包。原因:九成是 STARTDT 没完成就发了 I 帧,对端处于 STOPDT 状态直接丢弃。解决:把链路状态机做成严格串行,只有LinkState.RUNNING状态才允许发业务帧,其余状态进来的发送请求一律排队。

5.2 遥测值持续偏大或为负

现象:某几个点的值总是不对,量级差几千倍。原因:类型标识对应的信息体长度算错,短浮点按 2 字节读了,或者归一化值没除以 32767 就转成工程量。解决:按类型标识建一张长度表,解析前先查表;归一化值统一转 double 后再做量程换算。

5.3 跑几小时后链路自己断

现象:前两小时一切正常,之后子站主动断开。原因:接收序号回绕没处理,或者 t3 心跳被业务线程阻塞没发出去。解决:序号统一% 32768处理;心跳任务用独立的单线程调度器,别和业务解析共用线程池。

5.4 遥控执行了两次

现象:现场开关动作两次,非常危险。原因:执行帧超时后代码里做了自动重发。解决:执行帧绝不自动重发,超时只告警;同时在业务侧加幂等,同一 IOA 在短时间内只接受一次执行结果。

5.5 并发下报文字节错乱

现象:偶发解析异常,报文字节对不上。原因:多个线程同时往同一个DataOutputStream写,帧被交叉拼接了。解决:发送方法统一加锁,或者用单线程队列串行化所有发送动作,这是最常见也最容易被忽略的一条。

6. 二次开发:把规约层改成能嵌入业务平台的形态

这套代码真正上手改的时候,第一刀应该切在解耦上。规约层只负责“收到什么、发出什么”,业务层只关心“点位变了、遥控结果如何”,中间用接口隔开:

// 业务回调:规约层不感知任何业务实体 public interface Iec104Listener { void onData(int ioa, Object value, long timestamp); // 遥测遥信上送 void onControlResult(int ioa, boolean success, String reason); void onLinkState(LinkState state); }

把这个接口实现成 Spring 的 Bean,规约层的会话对象通过构造注入拿到它,点位映射表也从points.yaml换成数据库配置,现场改点就不用重新打包了。第二刀切在数据模型上:信息体解析出来的是原始值,加一层PointConverter做量程和单位换算,别让×0.1这种魔法数散落在解析代码里。

验证方法建议做两套:单元测试用构造好的字节数组喂给解析器,覆盖 SQ=0/1、各类型标识、序号回绕;联调环境用模拟子站跑长稳,至少挂 12 小时观察序号和内存。我一般会在onData里打个采样日志,每分钟统计一次收到帧数和最大序号,回绕问题一眼就能看出来。

还有个实用技巧:把收发原始报文做成可开关的 hex dump 日志,用-Diec104.dump=true控制。现场出问题的时候,让运维把这个开关打开抓十分钟,比远程连上去猜半天强得多。改这块的时候顺手把日志里的敏感信息(公共地址、具体点位含义)脱敏,交出去的日志才不会惹麻烦。

从那以后我每次接这类规约项目,都强制先把链路状态机和收发日志跑通、抓包对上,再动业务解析——顺序反了,后面全是猜。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 2:08:18

VPP插件开发实战:源码、节点注册与feature arc挂载排错

简介&#xff1a;面向网络数据平面开发者与 VPP 初学者&#xff0c;这份软件/插件资料围绕可编译、可对照的 VPP 源码和示例插件展开&#xff0c;帮助理解图节点机制、插件注册流程、CLI/API 扩展以及报文从入口到转发的处理路径&#xff0c;适合具备一定 C 语言与 Linux 基础的…

作者头像 李华
网站建设 2026/10/26 2:07:24

DL/T 376.1协议Java实现:生产级电表通信底座

简介&#xff1a;本资源是面向电力行业Java开发者的376.1通信协议&#xff08;ANSI C12.18-2004标准&#xff09;完整实现方案&#xff0c;专为智能电表与电网管理系统间的数据解析、报文构建及安全通信提供开箱即用的工程化支持&#xff0c;适用于AMR/AMI系统集成、电表数据采…

作者头像 李华
网站建设 2026/10/26 2:07:23

DQN车间排产实战:马尔可夫建模、奖励塑形与训练排查

简介&#xff1a;这套代码面向车间排产调度优化&#xff0c;聚焦DQN算法在动态生产环境中的策略学习与调度决策&#xff0c;适合强化学习学习者、运筹优化研究人员及工业工程师参考复现。资源包共3个Python文件&#xff0c;整体约5KB&#xff0c;体量轻巧却结构完整&#xff0c…

作者头像 李华
网站建设 2026/10/26 2:06:52

支付回调验签连续失败三天,最后发现是网关的时间戳单位问题

三天、五个错误方向&#xff0c;最后发现是「毫秒」和「秒」的默认值不一致。图 1&#xff5c;失败点不在支付服务&#xff0c;在网关的时间戳窗口——这是三天里最后才看到的一格。 读完你能拿到三样东西&#xff1a;① 验签失败的 5 步拆解排查法&#xff08;不用再猜&#x…

作者头像 李华
网站建设 2026/10/26 2:06:28

Android WebDAV客户端:Kotlin+OkHttp目录浏览与断点续传

简介&#xff1a;这是一份面向 Android 开发者的 WebDAV 客户端项目源码包&#xff0c;围绕移动端访问支持 WebDAV 协议的远程文件服务器展开&#xff0c;适合需要实现远程目录浏览、文件上传下载、重命名、移动与删除等功能的开发者参考学习。压缩包共 149 个文件&#xff0c;…

作者头像 李华
网站建设 2026/10/26 2:05:03

pandas手搓海龟法则:量化回测从信号到仓位与绩效

简介&#xff1a;面向量化交易初学者与已具备pandas基础的个人交易者&#xff0c;这套海龟交易法则Python实现资料把经典趋势跟踪规则落到可运行的数据分析流程中&#xff0c;重点处理历史行情读取、指标计算、突破判断与交易管理等问题。压缩包共3个文件&#xff0c;包含2个CS…

作者头像 李华