资讯详情

JAVA104协议监听包:从字节流解析到数据落地的完整实践

📅 2026/10/11 12:00:39 | 华诺云谱 👁 阅读
JAVA104协议监听包:从字节流解析到数据落地的完整实践
简介面向电力自动化与电网二次系统开发者的IEC 60870-5-104规约Java实现资源专门解决变电站主站与子站之间的104协议通信连接、实时数据监听与报文解析难题。代码基于Spring Boot框架设计完整包含主站连接类、实时监听类和运行入口三个Java核心源文件并提供readme.md依赖说明与所需jar包导入工程即可快速运行。资源已将104报文统一解析为十进制格式省去繁琐的协议解码环节对监听数据重写了toString()方法便于直接提取所需字段进行展示或落库同时附带通过POST请求将监听数据转发至客户端的参考逻辑可按实际业务自行启用或注释。压缩包共5个文件其中Java源码3个、说明文档1个、依赖包1个整体大小仅105KB轻量易部署。目前已有1413人学习下载适合具备Java基础、正在从事104规约接入或电网调度数据采集的开发者参考使用。1. 104规约监听这件事为什么值得用现成包电网104规约IEC 60870-5-104是调度自动化、配电自动化里绕不开的协议做过java104接入的开发者都有体会写一个能连上、能收数据的主站程序不难难的是调试期看不到数据。抓包工具拿到的是原始字节流APCI和ASDU混在一起时标是BCD编码品质位要按位拆对着规约文档翻半天才敢确定一帧的含义。这个JAVA104协议监听包解决的就是这个痛点把链路字节流自动解析成带类型标识、带时标、带品质位的结构化对象直接输出遥测、遥信、遥脉数据省掉最枯燥的字节级解析环节。适用人群很明确做配电终端联调的、做主站系统对点的、在调度数据网里做104协议监听的运维开发者。如果你正在搞电网104规约相关的调试或对接又不打算把规约解析从零写一遍这套监听工具值得先跑起来看看。2. 帧结构与监听模型解析器得先看懂字节流2.1 传输层与控制域APDU怎么切104规约承载在TCP/IP之上没有额外的链路层应用层数据单元叫APDU。每个APDU的头部结构固定第1字节是启动字符0x68第2字节是APDU长度这个长度包含后面4字节控制域再往后4字节是控制域APCI剩下的部分是ASDU。控制域的低两位决定帧类型。低两位为0是I帧信息帧I帧带发送序号和接收序号承载实际的ASDU数据低两位为1是S帧监视帧只带接收序号用于确认收到的I帧低两位为3是U帧控制帧用于链路启动STARTDT、停止STOPDT和测试TESTFR。分包时最容易错的地方有两个。第一个是长度计算apduLength字段的值是从控制域开始数的所以ASDU的实际长度是apduLength减4。不少人解析时直接用这个长度去切ASDU结果多切了4个字节后面的信息体地址、数据值全部错位。第二个是半包问题TCP是流式协议一帧APDU可能被拆成两次到达如果读到的字节数不够一个完整APDU必须缓存等下一次读事件不能直接把残片丢给解析器。// 按0x68启动字符和长度字段切割APDU public static Apdu parseApdu(byte[] buffer, int offset, int available) { if (buffer[offset] ! 0x68) { throw new Iec104Exception(起始字符错误期望0x68实际0x String.format(%02X, buffer[offset])); } int apduLen buffer[offset 1] 0xFF; if (apduLen 4 || apduLen 253) { throw new Iec104Exception(APDU长度非法: apduLen); } if (available apduLen 2) { throw new Iec104Exception(半包数据等待后续字节当前available available); } byte[] apci Arrays.copyOfRange(buffer, offset 2, offset 6); byte[] asdu Arrays.copyOfRange(buffer, offset 6, offset 2 apduLen); return new Apdu(apci, asdu); }这段代码里available是当前缓存里的有效字节数。半包判断放在长度校验之后逻辑顺序不能反——如果长度字段本身就是坏的先谈半包没有意义。这个包的监听模块用的也是同一套思路只是底层换成了Netty的ByteBuf切帧时靠索引定位0x68不做逐字节遍历高频流量下性能差距比较明显。2.2 监听的两种场景旁路和模拟主站处理完全不同写主站接入程序是主动行为建立TCP连接、发STARTDT激活、收数据、回S帧确认链路状态完全在自己手里。104协议监听则是另一套逻辑而且分两种场景处理方式天差地别。第一种是纯旁路监听接交换机镜像口或者TAP设备。旁路设备只收不发不能回任何确认帧。真实链路上主站会正常回S帧和U帧测试确认旁路设备如果自作聪明地回帧从站会认为出现了第二个主站直接判定链路异常严重的会把正常链路打断。第二种是模拟主站接入程序伪装成主站去连从站这种就必须完整实现链路握手和确认逻辑否则从站侧t3超时一到就把你断开数据根本收不完整。这个包如果两种模式都支持用之前必须先确认自己的场景。我见过有人把纯旁路当成模拟主站用开了协议应答开关结果把联调现场的真实链路搞断了排查了整整一下午才发现是自己这边在捣乱。更稳妥的做法是纯旁路只收数据不参与链路模拟主站才开应答两条代码路径分开走互不干扰。提示纯旁路监听和模拟主站是两套不同的链路交互逻辑配置前先确认自己属于哪种场景。开关开错了轻则数据收不到重则把现场链路搞断。2.3 从Socket到数据回调一条链路上的四段逻辑包内部的典型处理链路分四段NIO线程读字节流按0x68帧边界做分包分包后的APDU进入解码线程解码线程先解析控制域判断帧类型I帧再走ASDU解析解析出的ASDU按TypeID匹配已注册的handler匹配到的handler在回调线程里执行把数据交给业务层。这段链路里最容易出问题的是handler的线程模型。同步回调在收包线程里执行业务处理慢了会阻塞收包流量大的时候接收缓冲直接堆满异步回调走独立线程池又可能出现同一个信息体地址的先后顺序错乱。我一般这样选调试期用同步回调数据量小逻辑简单出错直接在当前线程打日志方便追踪正式环境换成异步线程池但同一个信息体地址的数据要按顺序处理方法是按地址分队列或者给数据对象加序号处理前校验序号连续性。// 处理链路的简化示意读流、拼缓存、切帧、解析、分发 while (channel.isActive()) { ByteBuf chunk channel.read(); // 1. 读TCP流 ByteBuf cached assemble(chunk); // 2. 拼接半包缓存 while (cached.readableBytes() 6) { // 3. 最少一个APCI byte[] frame cutByFrame(cached); // 4. 按0x68切整帧 Apdu apdu parseApdu(frame); if (isIFrame(apdu.getApci())) { // 5. 只处理I帧 Asdu asdu parseAsdu(apdu.getAsdu()); dispatch(asdu); // 6. 按TypeID分发 } } }cutByFrame是性能关键点用ByteBuf的indexOf定位0x68找到后用getUnsignedByte读取长度字段按长度字段跳转不要对整个缓冲做逐字节遍历。半包缓存要按连接维度维护连接关闭时清空对应缓存防止残留对象长期占用内存。3. 搭建监听服务工程引入、参数配置与启动验证3.1 工程引入与依赖先确认拿到的包是编译好的jar还是源码。Maven工程的话安装到本地仓库或在pom里按坐标引入。这里最需要注意的是JDK版本匹配基于Netty的新版本要求JDK8以上老项目如果还跑在JDK7上要么升级运行时要么换回基于BIO的老版本。我在一个维护中的老项目上就遇到过这个情况那边服务器是内网环境JDK不能随便升最后只能选兼容JDK7的分支。dependency groupIdcom.iec104/groupId artifactIdiec104-listener/artifactId version1.0.0/version /dependency源码包直接导入IDE的话确认IDE的编译级别和pom里配置一致。还有一个隐蔽点监听包如果依赖Netty而项目里已经有一个Netty版本会触发版本冲突。解决办法是用dependencyManagement统一版本或者排除监听包里的Netty依赖用项目里已有的版本。启动时如果抛NoClassDefFoundError八成是Netty版本冲突先查依赖树再动代码。3.2 核心配置参数说明配置文件支持YAML或properties格式。我习惯YAML层次清晰。下面是一份常用配置模板iec104: # 监听端口104规约默认2404 port: 2404 # PASSIVE等待从站连接ACTIVE主动连接远端 mode: PASSIVE # 心跳U帧TESTFR的发送间隔秒模拟主站时必填 heartbeat: 10 # 接收超时秒超过无帧则触发断线 timeout: 30 # 序号连续性校验I帧序号断档时输出告警 sequenceCheck: true # 最大帧积压量超过则丢弃并告警 maxQueue: 2048参数含义典型值注意事项port监听端口2404从站侧写死就不要乱改modePASSIVE/ACTIVEPASSIVE决定谁主动建连heartbeatTESTFR间隔10s必须小于对端t3超时timeout接收超时30s太短容易误判断线sequenceCheck序号校验true旁路监听建议开启maxQueue帧积压上限2048防止解析速度跟不上heartbeat和timeout的比值很关键。模拟主站模式下你的心跳间隔必须小于从站的t3链路超时参数典型从站t3是20秒心跳设10秒比较稳。timeout设30秒留足网络抖动的余量。如果心跳和timeout配成一样的值网络稍微抖一下就触发断线重连日志会反复刷屏。3.3 启动与验证收数启动代码不长但启动后的正确性验证要花心思。启动后先看日志里有没有链路建立成功的标志再看有没有ASDU输出。这里有个常见误解启动后没数据就以为包有问题。不少终端设备只在数据变化时上送变化上送线路平稳时可能几分钟不发一帧。这时候要么触发一次遥控操作制造变化量要么从站侧用调试软件手动置数否则干等着什么都看不到。// 完整启动加载配置、注册handler、启动、阻塞 Iec104Config config Iec104Config.loadFromYaml(iec104.yml); Iec104Listener listener new Iec104Listener(config); // 注册遥测handler只关心短浮点测量值 listener.addAsduHandler(TypeId.M_ME_NC, asdu - { for (MeasuredValue mv : asdu.getMeasuredValues()) { log.info(CH{} addr{} value{} quality{}, channelId(), mv.getInfoAddr(), mv.getValue(), mv.getQuality()); } }); listener.start(); listener.awaitTermination(); // 阻塞保持进程存活日志里必须带通道IDchannelId。多从站场景下这个包会把每个TCP连接识别成一个通道日志不带通道标识多台从站的数据混在一起后续回放和排查根本分不清是谁发的。我一开始没注意这个细节联调时两台终端同时上送日志刷屏全是数据完全对不上号后来给每个通道单独打日志文件才算理顺。4. 把数据转成可用格式遥测遥信遥脉的解析与落地4.1 类型标识先分清三类数据104规约用类型标识TypeID区分数据类型。监听方向从站到主站最常碰到的是这几类TypeID名字含义1M_SP_NA单点遥信3M_DP_NA双点遥信9M_ME_NA遥测归一化值11M_ME_NB遥测标度化值13M_ME_NC遥测短浮点14M_ME_TC遥测短浮点带时标15M_IT_NA遥脉累计量16M_IT_TA遥脉累计量带时标TypeID 1到40是监视方向45到70是控制方向。做104协议监听时如果收到控制方向的TypeID先别急着解析——说明镜像口把主站发下来的控制报文也复制进来了。这类帧不该出现在数据落地链路里建议在分发层直接过滤掉只保留监视方向的帧否则数据仓库里混入下行命令做统计报表的时候会莫名多出一堆假测点。另外还要留意ASDU里的公共地址字段。一条链路上通常只有一个站址如果同一个TCP流里出现多个公共地址说明镜像口串接了多个站点的流量。这种场景下数据落地前必须按公共地址拆分不能混在一起入库。4.2 时标解析与品质位数据可用的关键CP56Time2a时标占7字节全BCD编码不带时区信息。字节分布是毫秒占2字节低字节在前然后是分、时、日、月、年。月份字节的低5位是BCD月份值高3位是星期几解析时必须先掩掉高3位否则得到的月份会乱跳。// BCD转十进制 private static int bcdToBin(byte b) { return ((b 0xF0) 4) * 10 (b 0x0F); } // CP56Time2a时标解析返回毫秒时间戳 public static long parseCp56Time2a(byte[] b, int offset) { int ms (b[offset] 0xFF) | ((b[offset 1] 0xFF) 8); int minute bcdToBin(b[offset 2]); int hour bcdToBin(b[offset 3]); int day bcdToBin(b[offset 4]); int month bcdToBin((byte) (b[offset 5] 0x1F)); // 掩掉星期位 int year 2000 bcdToBin(b[offset 6]); Calendar c Calendar.getInstance(TimeZone.getTimeZone(Asia/Shanghai)); c.clear(); c.set(year, month - 1, day, hour, minute, 0); c.set(Calendar.MILLISECOND, ms); return c.getTimeInMillis(); }解析时统一按设备所在时区构造Calendar不要用系统默认时区。服务器在别的时区或者跨省部署时时区不显式指定存进去的时间戳跟设备实际时间可能偏差好几个小时。品质位QS在遥测里是一个字节关键bit位bit0是IV无效、bit1是NT非当前时刻采集、bit2是SB被替换、bit3是BL被封锁、bit4是OV溢出。IV为1的测点已经失效NT为1说明值是缓存值而不是当前采集值OV为1说明数值溢出。落库前我的处理规则是品质位全0的数据入正式库SB替换的入辅助库并加标记IV和OV丢弃但记录告警。这样脏数据不会污染统计告警还能帮你提前发现测点故障。4.3 数据落地从CSV到时序库调试阶段落CSV最直观。两个原则按通道分文件按日期滚动。一个文件里混多个通道的数据后面回放和对比分析会非常痛苦。// 按通道和日期分文件的CSV写入 public class CsvSink { private final MapString, BufferedWriter writers new ConcurrentHashMap(); public void write(String channelId, long ts, Asdu asdu) { String key channelId _ new SimpleDateFormat(yyyyMMdd).format(new Date(ts)); BufferedWriter w writers.computeIfAbsent(key, this::openFile); for (MeasuredValue mv : asdu.getMeasuredValues()) { try { w.write(String.format(%d,%d,%.4f,%d%n, ts, mv.getInfoAddr(), mv.getValue(), mv.getQuality())); } catch (IOException e) { log.error(CSV写入失败, key key, e); } } // 注意数据量大时每1000条flush一次避免频繁IO } }正式环境建议直接用时序数据库测点信息打tag站点ID、通道ID、测点类型浮点值存field品质位存独立字段。查询曲线时按tag过滤非常快。有统一数据总线的话推Kafka也行下游做实时校验和报警。不管走哪条落地路径通道ID、信息体地址、品质位三样必须保留缺一个后续排查问题就得重新翻原始报文等于把解析省下的时间又还回去了。5. 104协议监听避坑指南五个我踩过的坑5.1 频繁断连心跳参数与模式没配对现象监听服务启动后能正常收数但每隔几分钟从站就断开重建连接日志里全是connection reset数据断断续续拿不全。原因从站侧有链路超时监视t3参数规定时间内收不到主站的任何帧就判定链路中断。模拟主站模式下如果你没开心跳或者心跳间隔大于从站t3从站就会主动断开重连。还有一种情况是纯旁路模式下监听的TCP连接占用了从站允许的连接数上限导致真实主站反而连不进来。解决先确认自己跑在哪种模式。模拟主站时把heartbeat设成5到10秒保证明显小于对端t3典型20秒纯旁路时确认交换机镜像口配置正常不要占用从站的业务连接数。另外模拟主站模式下开了协议应答的话确认帧的收发序号一定要按从站侧的状态管理序号对不上比不回确认更糟糕从站会直接踢你下线。5.2 序号跳变镜像口丢包与网卡缓冲现象日志里I帧发送序号从120直接跳到130中间缺了多个序号sequenceCheck持续告警。原因交换机镜像口在流量高峰会随机丢包或者监听服务器的网卡接收缓冲溢出。I帧序号是从站侧连续编号的丢一帧缺一个序号跳过校验逻辑是不可能的。解决开序号连续性校验断档时记录区间并持续告警但后续数据要继续解析不要因为断档就跳过后续所有帧。同时按网卡实际情况调大缓冲减少丢包# 查看网卡环形缓冲当前值 ethtool -g eth0 # 调大接收环形缓冲注意不要超过网卡硬件上限 ethtool -G eth0 rx 4096 # 调大socket接收缓冲 sysctl -w net.core.rmem_max16777216丢帧的幂等处理原则是告警归告警解析不中断。我见过有人写了序号断档就跳过的逻辑结果序号一乱后面所有帧都被当成异常丢掉损失比镜像口丢包本身大得多。5.3 时标偏了8小时时区与BCD解析现象解析出来的时间戳比设备当地实际时间晚了8个小时或者某个站点的时间整体偏移。原因CP56Time2a时标本身不含时区设备按本地时间编码。解析进程如果用了系统默认时区而服务器和设备不在同一个时区就会出现固定偏移。另外月份字节没掩掉高3位的星期位的话时间会偶发乱跳。解决解析时显式指定TimeZone不要依赖服务器默认时区月份解析必须用0x1F掩码。还有一个容易忽略的点设备如果长期未对时时标偏差是分钟级的这种要在数据质量模块里加时间偏差检测偏差超过阈值标记为需校时测点别把慢钟数据当准确值入库。5.4 长时间运行内存持续涨回调与缓存泄漏现象监听服务跑了一周内存从300MB慢慢涨到2GB最后OOM重启数据中断。原因我排查过的几起OOM根源各有不同。最常见的是业务层在调试时反复注册handler旧的没注销分发表越积越大其次是半包缓存没按连接清理连接断开后缓存对象还留在Map里另外有一起是业务代码把ASDU对象塞进了静态List打算稍后分析那个List吃掉了大部分堆内存。解决注册handler时保存注册ID重连或重启时先注销再注册半包缓存按连接维度管理连接关闭事件里清理对应缓存业务层不要长期持有ASDU引用。排查内存问题最有效的办法是启动时加-XX:HeapDumpOnOutOfMemoryError参数OOM时自动dump堆然后用MAT查对象引用树定位到持有者类基本就破案了。5.5 端口启动失败2404被占与配置路径现象start()抛BindExceptionAddress already in use进程起不来。原因同一台机器上跑了多个监听实例或者系统其他服务占用了2404。有时候是配置文件路径写错两个进程加载了同一个配置后启动的那个必然撞端口。解决先查端口占用再决定动作# Linux查看2404端口占用 ss -lntp | grep 2404 # Windows对应命令 netstat -ano | findstr 2404如果从站侧把端口写死了2404你只能停掉旧实例不能为了省事换端口。多实例部署时不同实例用不同端口日志文件按实例ID区分配置文件路径在启动参数里显式指定不要用相对路径避免两个进程读到同一份配置。6. 进阶监听到的数据怎么验证正确性拿到数据不等于数据是对的。我每次部署完104协议监听服务都要做一轮验证三个手段配合能覆盖绝大多数解析问题。第一个手段是模拟器回放验证解析正确性。用104规约模拟器生成一批固定数据从站侧发出监听服务收完后和模拟器的发送记录逐条对比。这个手段专门抓时标解析错误、品质位掩码错位、类型标识分发错误这三类问题。模拟器最好能控制发送序号故意制造序号跳变用来验证丢帧检测逻辑是否真的生效而不是只在日志里当摆设。第二个手段是帧计数校验量化链路质量。APCI里I帧的发送序号是连续递增的我写了一个计数器每收到一帧检查序号是否等于上一个加一。统计窗口内收了多少帧、序号断档几次、断档总跨度多少三个数字一算链路质量好坏一目了然。这个方法在旁路监听场景下特别有用因为旁路不参与链路握手唯一的链路质量信号就是序号连续性。第三个手段是站点读数对比验证业务正确性。挑几个稳定的遥测点把监听数据和场站后台的实际值对比。偏差超过阈值的重点排查品质位——脏数据入库的问题十有八九出在IV为1但业务层没判断。我还会在落地前加一道公共地址校验一条监听链路上如果出现多个公共地址说明镜像口串了其他站的流量这种数据必须拦下来不能批量入库。三个手段的验证顺序也有讲究先回放确认解析没错再帧计数确认链路没丢最后读数对比确认业务含义正确。反过来做的话读数对不上时你根本不知道是解析错、链路丢还是业务逻辑错排查范围会大很多。从那以后我每次部署完104规约监听服务都强制自己走一遍「模拟器回放→帧计数校验→站点读数对比」三步流程不验证完不接业务。这套流程帮我挡掉过太多次看起来正常、实际解析错位的翻车现场希望也能帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑