ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java实现104规约监听程序:从TCP字节流到遥测遥信报文解析

Java实现104规约监听程序:从TCP字节流到遥测遥信报文解析 简介面向电力调度与工业自动化场景的JAVA104协议监听工具包主要帮助Java开发者在Spring Boot项目中快速实现IEC 60870-5-104规约的主站连接、实时数据监听与报文解析解决传统十六进制报文难以直接阅读、调试定位困难的问题。压缩包共5个文件包含3个Java核心源码——J60870Client负责主站连接、J60870ClientListener负责协议监听、J60870Main负责启动运行另附readme说明文档和已打好的jar依赖包整体仅105KB轻量易用导入后可按readme指引自行调整环境。源码已将104报文统一解析成常规十进制数据并重写toString()方法把关键字段汇总为便于读取的字符串读者可根据业务自行调整输出格式同时提供通过POST请求将监听数据转发至客户端处理的参考实现可灵活适配本地展示或对接后端系统。资源包已上传所需jar包readme中也补充了Maven依赖方便快速集成。已有1411人学习浏览适合需要二次开发或深入理解104规约通信流程的初中级工程师。 做电力自动化调试这几年最常被问的问题之一就是104规约的数据到底怎么监听尤其是手头没有厂家专用的报文分析工具时面对一堆TCP字节流完全无从下手。前阵子我配合变电站做数据网交换机调试现场主站侧说收不到遥测子站侧说已经上送了两边都在甩锅。手边只有一台笔记本我索性用Java写了个小型104监听程序把链路上的TCP帧原样抓下来逐帧解析出遥测、遥信和遥控报文十分钟就定位到了问题。这篇文章就把这套监听程序的实现思路和踩过的坑完整记录下来给同样要做104规约调试、协议分析、或需要快速验证子站上送数据的同行一个能直接参考的路径。1. 需要写监听程序的几种现场场景1.1 主站不发遥测先分清链路没通还是数据没上送104规约调试中最常见的现象就是数据静默。但真正上手排查时你会发现数据静默至少有三层含义链路层没建立主站发起了TCP连接但U帧握手没有完成双方没有进入数据传输状态链路通了但没有数据连接正常、握手完成但主站没有下发总召唤或想召命令子站自然不会主动上送数据上了但没到主站子站正常上送遥测遥信但中间交换机、网关或防火墙把报文丢了或者改了端口。这三种场景用抓包工具看TCP层全都能看到但问题在于现场大多没有专门的104协议分析仪。而用Java写一个监听程序本质上是把看到TCP报文提升到看懂104报文的层面直接判断问题出在哪一层。我那次调试遇到的实际情况是主站已经发了总召唤激活命令子站也回了确认但主站始终不刷新遥测。监听程序一跑就发现子站上送的I帧序号一直没被主站确认主站的S帧确认报文也没有正常回来。问题根本不在应用层数据而在两边的传输确认机制上。没有监听工具这种问题只能靠猜。1.2 三种监听落点接在子站、挂在主站、串在链路中间写监听程序之前先想清楚你的监听程序要放在网络拓扑的哪个位置。这决定了你是做ServerSocket监听还是主动连接对端。作为服务端监听在子站侧子站测控装置、保护装置、远动通信管理机一般是104服务端主站是客户端主动连过来。你的Java程序监听2404端口等主站连接这样能完整捕获主站下发的遥控、遥调、召唤命令以及子站回给主站的一切响应。适合做从站仿真或主站报文观测。作为客户端连接主站或前置机有些场合主站侧开放了一个调试端口你的Java程序主动连接过去接收对方推送的报文。这种模式通常用于被动观测。串在中间做报文镜像利用交换机的镜像口或者用T型分光器把双向流量复制一份Java程序完全透明地抓取双向报文。这种方式最接近监听的本意不会影响原有链路也是我最推荐现场实测时使用的方式。定位不同代码入口就不一样。本文后面以服务端监听2404端口为主展开因为这是最容易复现、也最适合做协议验证的场景。如果需要镜像抓包程序核心的帧解析部分完全一样只需要把数据源从ServerSocket换成Pcap4J之类的抓包API即可。2. 解析前必须吃透的104帧结构从串口思维转换到TCP流2.1 一个字节一个字节拆开看APDU104规约的本质是IEC 60870-5-101规约在TCP/IP上的映射。它的每个应用层协议数据单元APDU结构非常规整起始字符固定为0x68第二个字节是APDU长度然后是4字节的控制域之后是ASDU应用服务数据单元。以一条最常见的I帧报文为例它的字节分布如下字节偏移内容说明00x68启动字符固定不变1APDU长度不含前两个字节但包含控制域和ASDU范围4-2532-5控制域决定帧类型I帧/S帧/U帧6ASDU业务数据包含类型标识、传送原因、公共地址、信息体注意一个容易记错的地方APDU长度字段的值包含了4字节控制域但不包含启动字符0x68和长度字段自己那一字节。也就是说如果一帧APDU只有控制域没有ASDU比如S帧长度字段的值是4整帧从0x68到最后一个字节共6个字节。很多初次写解析程序的人在这里栽跟头直接用长度字段值做数组下标导致越界或少读。用Java读取时最简单的做法是先循环读取直到遇到0x68再读一个字节得到长度len然后接着读len个字节这一帧就完整了。这里要强调一下TCP是字节流不是报文流0x68只是应用层帧头不代表TCP分段的边界所以必须自己维护找帧头、读长度、读完整帧的过程这就是后面要说的粘包拆包。2.2 控制域判断I帧、S帧、U帧的区别控制域的第一个字节即整帧的第2个字节的低两位决定了帧类型。这是解析程序里第一个要做的分支判断也是最基础的分流逻辑。I帧信息传输帧低两位为00即该字节bit00bit10。I帧携带ASDU业务数据是遥测、遥信、遥控、遥调这些正经数据的载体。控制域的第2-5字节中前两个字节拆出发送序号N(S)后两个字节拆出接收序号N(R)。序号都是15位范围0-32767循环使用。S帧监视帧控制域第一字节为0x01。S帧只有确认功能不携带ASDU用来确认对方已发来的I帧。控制域后两个字节是接收序号N(R)。U帧控制帧控制域第一字节为0x03。U帧用于链路控制携带STARTDT启动数据传输、STOPDT停止数据传输、TESTFR测试帧三类命令及确认。U帧没有序号。监听程序拿到一帧数据后第一步就是根据控制域第一字节的低两位判断类型然后分别处理。对监听来说I帧的价值最高因为业务数据全在ASDU里S帧和U帧的价值在于判断链路状态是否正常——如果长期只收到S帧而没有I帧说明链路是通的但没有数据流动如果连U帧握手都没完成那链路压根没建立起来。2.3 ASDU里的业务字段类型标识、传送原因、公共地址、信息体地址ASDU是104规约里真正有业务含义的部分。它的开头几个字段非常固定解析时按顺序读即可类型标识1字节这一字节决定了后面信息体的结构和含义。比如1代表单点遥信3代表双点遥信9代表归一化遥测11代表标度化遥测13代表短浮点遥测float45代表单点遥控50代表短浮点遥调。一张类型标识表是必须常备的资料。可变结构限定词VSQ1字节最高位表示信息体地址是否连续。bit70时后面的信息体不含地址只有一个起始地址自增累加bit71时每个信息体自带完整地址。低7位表示信息体个数。解析时先读VSQ确定有几个信息体以及要不要读地址才能正确游走到下一个信息体。传送原因COT2字节第一个字节的低6位表示传送原因如1周期/循环3突发4初始化5请求/召唤6激活7激活确认20响应站召唤。第二字节一般是源发地址通常为0。公共地址CA2字节站地址。低字节在前多站转发时靠它区分。信息体地址IOA3字节具体测点的地址。低字节在前结合类型标识就能唯一定位一个数据点。整个ASDU的解析逻辑就是先读类型标识和VSQ再读传送原因和公共地址然后根据类型标识进入不同的信息体解析分支。监听的最终目的无非就是把这条链路里所有I帧的ASDU解出来按类型归类、按公共地址分站、按信息体地址对应到具体测点。3. Java监听程序的落地Socket读取、帧边界划分与链路握手3.1 先用ServerSocket还是先连出去实现落点选择前面提到监听落点有三种实际写代码时我强烈建议优先以服务端监听为起点。原因很直接104主站作为TCP客户端会主动连接子站的2404端口我们只要在本机或现场笔记本上起一个ServerSocket(2404)让主站或前置机连过来就能立刻开始收报文。这种方式不需要改动网络拓扑不需要在链路上做镜像还能顺便在主站连接失败时第一时间暴露网络连通性问题。如果你要监听的是子站主动上送的数据而现场不允许你占用现网端口那就在自己笔记本上搭一个虚拟主站环境用Java程序模拟主站连接真实的子站设备同样走2404端口效果一样。两种模式的核心代码几乎没有差别区别只在ServerSocket.accept()和socket.connect()这两种建立连接的方式上。3.2 粘包拆包的正确处理方式TCP粘包拆包是写监听程序遇到的第一个技术门槛。104协议靠0x68定位帧头但TCP流不会保证每个0x68正好是一帧的开头可能出现一帧被拆成多个TCP段或多个104帧连在一个TCP段里。处理思路是维护一个累积缓冲区循环读流、找帧头、读长度、取整帧。承诺一个实用写法不要用read(byte[])直接读固定长度因为InputStream.read(byte[])并不能保证一次调用就读满数组长度网络数据不足时会返回实际读到的字节数。我之前见过不少同行在这里踩坑一帧数据没读完就硬套协议解析出来全是乱码。正确做法是循环读直到凑够整帧需要的字节数。public static byte[] readNextFrame(InputStream in) throws IOException { int first; // 找帧头 0x68 while ((first in.read()) ! -1) { if (first 0x68) { int len in.read(); if (len 4 || len 253) { continue; // 长度异常重新找帧头 } byte[] frame new byte[len 2]; frame[0] (byte) 0x68; frame[1] (byte) len; int readCount 2; while (readCount frame.length) { int n in.read(frame, readCount, frame.length - readCount); if (n -1) { throw new EOFException(连接已关闭); } readCount n; } return frame; } } return null; }这段代码的核心思路是先凑长度再读满。第二层while循环处理了拆包即使一帧被拆成两次TCP到达也能拼齐。3.3 完整代码骨架链路激活与心跳维持有了帧读取方法监听主流程就可以搭起来了。作为服务端监听时accept()到主站连接后下一步不是直接解析业务数据而是要完成104链路的握手主站会发U帧STARTDT激活0x68 04 07 00 00 00服务端要回一个STARTDT确认0x68 04 0B 00 00 00然后双方才进入数据传输状态。这一步不完成主站后续不会发送任何I帧。完整骨架大致如下try (ServerSocket server new ServerSocket(2404)) { System.out.println(104监听服务已启动端口2404); while (true) { Socket socket server.accept(); System.out.println(主站连接接入: socket.getRemoteSocketAddress()); new Thread(() - handleConnection(socket)).start(); } } private static void handleConnection(Socket socket) { try (InputStream in socket.getInputStream(); OutputStream out socket.getOutputStream()) { while (true) { byte[] frame readNextFrame(in); if (frame null) break; // 解析控制域第一字节 int control frame[2] 0xFF; if ((control 0x03) 0x03) { // U帧 if (control 0x07) { // STARTDT激活 out.write(new byte[]{(byte)0x68, 0x04, (byte)0x0B, 0x00, 0x00, 0x00}); out.flush(); System.out.println(链路启动确认已发送); } else if (control 0x43) { // TESTFR测试帧 out.write(new byte[]{(byte)0x68, 0x04, (byte)0x83, 0x00, 0x00, 0x00}); out.flush(); } } else if ((control 0x03) 0x01) { // S帧仅确认不处理数据 System.out.println(收到S帧确认); } else { // I帧 // 处理ASDU解析业务数据 parseASDU(frame); // 一定要回复S帧确认否则主站序号无法滑动 int recvSeq ((control 0xFE) 1) | ((frame[3] 0xFF) 7); int ackSeq recvSeq; byte[] sFrame new byte[]{ (byte)0x68, 0x04, 0x01, 0x00, (byte)((ackSeq 1) 0xFE), (byte)((ackSeq 7) 0xFF) }; out.write(sFrame); out.flush(); } } } catch (IOException e) { System.out.println(连接断开: e.getMessage()); } }要特别说明一下S帧确认不是可选项是掉了就会出事的必选项。104协议的收发序号机制类似TCP的滑动窗口主站发出N个I帧后如果收不到你的确认发送序号达到窗口上限就会停下来表现为主站只发了几个帧就没下文了。我在调试现场见过多次只收到前几帧数据后面再也不来了的现象一查就是监听端或子站没有回复S帧。4. 数据解析这一关从字节到遥测遥信的换算细节4.1 遥测的三张表归一化、标度化、短浮点104规约里遥测是高频数据也是最需要在监听解析时做换算的部分。三个常用类型标识必须烂熟于心类型标识9归一化遥测M_ME_NA_1信息体数据为2字节有符号整型short单位为原始值的千分之一。比如收到的值为-5000实际遥测值就是-5.000。这类数据的精度和量程由双方的整定决定监听端无法自动还原成工程值最好的做法是先原样打印short值再对照点表手工换算。类型标识11标度化遥测M_ME_NB_1同样是2字节有符号整型但比例尺由ASCII字符cfloat描述。很多老装置的功率、电流用这种格式监听时要额外解析后面的比例因子。类型标识13短浮点遥测M_ME_NC_1信息体数据为4字节IEEE 754浮点数Java中直接用Float.intBitsToFloat()转换即可。这是目前主流综自设备最常用的遥测格式也是现场排查时最直观的。收到一帧13类型4个字节拼出一个float基本不需要额外处理。以短浮点遥测为例单信息体的ASDU段解析代码int typeId asdu[0] 0xFF; int vsq asdu[1] 0xFF; int count vsq 0x7F; boolean isContinuous (vsq 0x80) 0; int offset 6; // 跳过类型、VSQ、COT(2)、CA(2) int ioa 0; for (int i 0; i count; i) { if (isContinuous i 0) { ioa; } else if (!isContinuous || i 0) { ioa (asdu[offset] 0xFF) | ((asdu[offset 1] 0xFF) 8) | ((asdu[offset 2] 0xFF) 16); offset 3; } if (typeId 13) { int bits (asdu[offset] 0xFF) | ((asdu[offset 1] 0xFF) 8) | ((asdu[offset 2] 0xFF) 16) | ((asdu[offset 3] 0xFF) 24); float value Float.intBitsToFloat(bits); System.out.println(IOA ioa , 遥测值 value); offset 5; // 4字节float 1字节品质 } }需要特别提醒品质字节是信息体的一部分不是可忽略的尾巴。很多监听工具把最后1字节品质描述当成填充字节丢掉导致解析结果错位后续所有信息体全部错乱这属于新手常见问题。4.2 遥信和遥控的位与字节遥信相对简单但有几个细节值得说明。类型标识1是单点遥信每个信息体的数据就是1个字节bit0表示分合状态0为分1为合。类型标识3是双点遥信bit0和bit1组合01合10分其他为中间状态或不确定状态。后面通常跟随1字节品质描述表示是否有效、是否被取代等。监听遥信时最实用的是一个位视图打点法。别急着把每个字节都解释成分/合先按信息体地址把收到的状态存在一个Map里等数据攒几轮再对比变化。104调试中最常排查的就是某个遥信位置不上来或状态突变用监听程序拿到IOA和对应状态后直接跟后台点表比对问题立刻浮出水面。遥控解析要小心的是激活与执行分开处理。类型标识45单点遥控、46双点遥控的控制命令主站先发一帧激活COT6子站回激活确认COT7执行完成后回激活终止COT10。监听程序要把三个阶段的帧都抓下来才能完整还原一次遥控过程。如果只盯着激活帧看会漏掉装置未执行的真相。4.3 品质描述字节为什么不能当噪声丢掉品质描述QDS是信息体最后1字节其bit位含义如下bit位含义bit0是否被取代SB0未取代1取代bit1是否带时标NT0不带1带bit2是否有效IV0有效1无效bit3是否被封锁BL0未封锁1封锁bit4-7保留我想强调的是bit2IV位。我遇到过不少现场后台一直显示某个测值不对抓包发现装置确实上送了数据但IV位是1说明这个值是无效的——可能是通讯中断保护置位、手动置数或检修压板投入导致的。如果监听程序不考虑品质位就会把这帧当成有效数据误判。所以解析时务必把这个字节打印出来或展示为有效/无效/取代/封锁哪怕只是用来快速排除异常。5. 调试实录TCP字节流里的几个隐藏坑5.1 帧头0x68和业务数据里的0x68撞车这是我在一次监听写完后遇到的第一个灵异事件程序解析出来的帧长度忽长忽短偶发性地丢掉一帧。排查后发现APDU的数据区内尤其是浮点数的字节序列里完全可能出现0x68这个值。我的第一版找帧头逻辑是看到0x68就认为是一帧的起点结果把数据区里的0x68当成了帧头后面所有字节错位。解决思路是在找帧头时增加合理性校验以0x68为候选起点后读取长度字段len必须满足4≤len≤253且后面的len个字节能完整读够才认为这是一个合法帧如果读不齐或者后续解析控制域时发现既不像I/S/U帧的合法组合就说明帧头定位错误回退重新找。实际工程中最好配合连续两帧校验来定位帧头。在缓冲流里一旦找到一个合法的0x68len结构解析完这帧后检查下一帧的起始字节是否还是0x68且长度合法。如果多轮解析都稳定说明缓冲区对齐正确。如果中途错位立即清空缓冲重新从原始字节流里搜索。5.2 序号不确认主站直接沉默前面提过S帧确认的重要性这里说一个更隐蔽的情况序号计算错误。104规约的序号是15位循环的很多初写者在做S帧确认时直接用收到的发送序号原样回填但S帧里的接收序号需要左移一位因为S帧控制域第二字节的低位是0接收序号放在bit1到bit7。我早期写的确认代码byte sFrame 0x01; byte nR (byte)((recvSeq 1) 0xFE);这里如果recvSeq超过127左移后溢出就会出问题。正确做法是用两个字节分别承载15位序号int nRSend ((control 0xFE) 1) | ((frame[3] 0xFF) 7); // 发送序号 int ackSeq nRSend; // 确认对方发来的序号 byte[] s new byte[]{ (byte)0x68, 0x04, 0x01, 0x00, (byte)((ackSeq 1) 0xFE), (byte)((ackSeq 7) 0xFF) };主站收到错误序号的S帧后轻则忽略重则直接断开连接。如果你发现程序收了几帧后链路就不再前进优先检查确认帧的序号拼装。5.3 端口占用、防火墙与2404的玄学另一个现场高频坑程序启动报BindException: Address already in use。原因大多数是上一个监听进程没有完全退出端口还处于TIME_WAIT状态。我的经验是开发调试阶段用ServerSocket时加上setReuseAddress(true)ServerSocket server new ServerSocket(); server.setReuseAddress(true); server.bind(new InetSocketAddress(2404));还有windows和部分Linux服务器的防火墙会拦截非本机的TCP连接。现场笔记本做监听时如果主站连不上不要只盯着代码先在本机用netstat -an | findstr 2404看端口是否处于LISTENING状态再用telnet 10.x.x.x 2404测试主站到本机的连通性。很多协议解析错误的假象最后发现是TCP层就没通。5.4 报文时间戳和缓存落盘监听程序跑起来后如果只是把报文解析结果打印在控制台一旦数据量上来打印本身就会拖慢读取速度甚至造成丢帧。实用的做法是解析线程和业务处理线程分离。网络读取线程只负责把原始帧放进一个阻塞队列另一个解析线程从队列里取帧、打时间戳、解析和落盘。这里的时间戳务必以帧接收时刻为准不要用解析时刻否则在高并发场景下时间误差会很大。我在实际监听工具里还加了原始报文hex落盘功能每帧除了解析内容顺便把整帧十六进制字节写进一个日志文件。这样即使后来发现某个字段解析逻辑写错了还能用原始报文重新复盘不用再到现场抓一次包。对调试来说留一手原始数据永远不亏。6. 从监听工具到调试平台选型与扩展建议6.1 用现成库还是自己解析如果你的目标只是快速看数据不必重复造轮子。Java生态里有几个现成的104协议库最常用的是OpenMUC的j60870它已经实现了连接管理、ASDU解析、类型标识映射等底层逻辑。直接引入依赖几行代码就能监视一个连接ClientConnection connection new ClientConnection(10.1.1.1, 2404); connection.connect(); connection.setConnectionListener(new ConnectionListener() { public void connectionOpened(Connection c) { c.startDataTransfer(); } public void newASdu(ASdu asdu) { System.out.println(asdu.getTypeIdentification() asdu.getCommonAddress()); } });用库的优势是省时省力但它也有明显的局限库帮你封装了太多细节一旦遇到非标准厂家的私有类型标识或特殊信息体结构反而很难排查而且你拿不到解析过程的中间态比如原始帧缓冲、控制域序号对深入调试帮助有限。我的经验是现场快速验证用库深入到协议层面的问题排查必须自己解析关键帧。6.2 一套可复用的监听日志格式最后分享一个实战习惯。我最终实现的监听工具不只是打印解析结果而是输出一行紧凑的结构化日志方便事后用文本工具检索[2025-01-15 10:23:45.123] [I帧] 发送序号1024, 接收序号332, 类型13, COT3, CA1 IOA1001, 值220.50, 品质有效 IOA1002, 值10.02, 品质有效这样的日志格式既能在控制台实时观察数据变化又能落盘后用grep或awk过滤特定IOA的曲线。对于动辄几万条记录的遥测报文按IOA过滤是定位问题最快的方式。总而言之写一个104规约的Java监听程序并不复杂难的是把TCP字节流、帧边界、序号确认和ASDU嵌套结构这些层层细节串起来并在真实现场的环境里保持稳定。如果你正准备做类似的工具建议按先解析帧-再握手-再解析ASDU-最后加日志落盘的顺序逐步实现不要想着一步到位。手里有了一套能随时解析原始报文的工具后续不管是处理主站通信异常还是验证设备上送逻辑都会从容很多。本文还有配套的精品资源点击获取
返回列表