ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

IEC 60870-5-104规约解析实战:TCP连接、ASDU编解码与CRC校验源码级实现

IEC 60870-5-104规约解析实战:TCP连接、ASDU编解码与CRC校验源码级实现 简介本资源是一套面向电力自动化系统开发工程师、SCADA通信协议学习者及嵌入式远动设备调试人员的101/104规约解析实践代码包聚焦DL/T 634.5101-2002与IEC 60870-5-104协议的底层实现与报文解析逻辑。压缩包共112个文件含34个Java源码如ASDU、Telemetry104、TransferReason等核心类、34个编译后class文件、11个XML配置及9个sample测试用例完整覆盖TCP连接管理、ASDU/TCPU构建拆解、序列号控制、CRC校验与事件响应等关键模块整体3.7MB结构清晰便于逐层研读协议栈实现细节。已有583人学习下载读者可直接复用源码理解104规约APDU封装、101帧结构解析流程并结合Util.class等工具类快速定位通信异常显著提升电力监控系统联调与协议兼容性问题排查能力。1. 这不是“协议文档翻译”而是能直接跑通的104规约解析黑匣子6个核心类TCP层闭环ASDU字段级可调试源码你手头有一台新接入的RTU主站收不到遥信变位或者调试时Wireshark抓到一串0x68开头的十六进制流却卡在“ASDU类型标识0x01到底对应哪个遥测点”上——这时候翻DL/T 634.5104-2002标准PDF不如直接看这段Java源码。它不是教学Demo而是从真实SCADA网关里抠出来的、带完整TCP连接管理、ASDU组装/拆解、序列号滑动窗口、超时重传逻辑的工业级解析骨架。Telemetry104.class能把原始字节流还原成带时间戳、品质码、值域的遥测量TransferReason.class精确映射原因码如0x06周期、0x07突发ParamePreset104.class预置了常见控制命令参数模板。它不依赖Spring或Netty纯JDK NIOByteBuffer实现部署在嵌入式ARM盒子或Windows工控机上都无压力。如果你正在做电力终端接入、规约转换器开发、或需要逆向分析某家厂商私有扩展ASDU这份源码就是你的第一块调试板——不是教你“什么是启动字符”而是让你改两行代码就能验证CRC校验失败时的重传行为。2. 从TCP握手到ASDU解包104规约通信链路的六层落地实现2.1 TCP连接生命周期三次握手后谁在维护心跳与断连检测104规约的可靠性根基不在应用层而在TCP连接的健壮性。这份源码没用Netty封装而是用java.nio.channels.SocketChannel手动管理连接状态。关键逻辑藏在ParamePreset104.class的静态初始化块和Telemetry104.class的connect()方法中// Telemetry104.java 片段 public boolean connect(String host, int port) { try { channel SocketChannel.open(); channel.configureBlocking(false); channel.connect(new InetSocketAddress(host, port)); // 启动非阻塞连接等待线程 while (!channel.finishConnect()) { Thread.sleep(10); // 避免忙等 } // 连接成功后立即发送STARTDT启动链路请求 sendStartDt(); return true; } catch (Exception e) { logger.error(TCP connect failed: {}, e.getMessage()); return false; } }注意finishConnect()返回true才代表三次握手完成此时必须立刻发STARTDT0x68 04 07 00 00 00否则对方主站不会进入数据传输态。源码里sendStartDt()调用的是Util.class的buildApdu()它按IEC 60870-5-104标准构造APDU启动字符0x68 APDU长度固定4字节 控制域0x07表示启动链路 地址域主站地址。连接建立后心跳由ParamePreset104.class的startKeepAliveTimer()触发每30秒发一次TESTFR测试帧。但真正决定连接存续的是对方是否在T1超时时间内默认15秒回复TESTFR-CON。源码用ScheduledExecutorService轮询channel.read()若连续两次读超时则主动close()并触发重连。这比单纯依赖TCP keepalive更符合104规约要求——因为规约规定“链路空闲超时即断开”而OS层keepalive可能长达2小时。2.2 ASDU构建从遥信点表到二进制帧的精确映射ASDUApplication Service Data Unit是104规约的数据载体其结构复杂度远超HTTP Header。源码用ASDU.class统一管理所有ASDU类型但实际组装逻辑分散在Telemetry104.class遥测、UnContinuousAddressBuilder.class不连续地址等类中。以遥信TypeID0x01为例其ASDU结构为类型标识(1B) 可变结构限定词(1B) 传送原因(2B) 应用服务数据单元公共地址(2B) 信息体地址列表 信息体元素每个含单比特状态品质码。// Telemetry104.java 中构造遥信ASDU片段 public byte[] buildSinglePointInfo(int[] addresses, boolean[] values) { // 计算ASDU总长固定头6B 每个点2B地址2B 状态1B 品质码1B int asduLen 6 addresses.length * 4; ByteBuffer bb ByteBuffer.allocate(10 asduLen); // APDU头10B ASDU bb.put((byte) 0x68); // 启动字符 bb.put((byte) asduLen); // APDU长度低字节在前 bb.put((byte) 0x00); bb.put((byte) 0x00); // 控制域I帧发送序号0接收序号0 bb.put((byte) 0x01); // 类型标识单点信息 bb.put((byte) (0x80 | addresses.length)); // 可变结构限定词0x80表示地址连续高位bit1 bb.putShort((short) 0x06); // 传送原因周期 bb.putShort((short) 0x0001); // 公共地址主站地址1 for (int i 0; i addresses.length; i) { bb.putInt(addresses[i]); // 信息体地址4BIEC标准要求 bb.put(values[i] ? (byte) 0x01 : (byte) 0x00); // 状态值 bb.put((byte) 0x01); // 品质码有效、无故障 } return bb.array(); }参数说明addresses是RTU侧配置的遥信点地址数组如[1001,1002,1003]values是当前采集到的布尔值。这里用putInt()写4字节地址是因为源码严格遵循IEC 60870-5-101/104标准——即使实际设备只用2字节地址也必须补零填充。品质码0x01表示“有效、无故障、非替代”若需表示“无效”则改为0x00具体位定义见标准附录A。2.3 连续/不连续地址处理为什么ContinuousAddressBuilder和UnContinuousAddressBuilder必须分离104规约允许两种地址组织方式连续地址如1001,1002,1003和不连续地址如1001,1005,1010。前者用ContinuousAddressBuilder.class后者用UnContinuousAddressBuilder.class二者生成的ASDU结构完全不同地址类型可变结构限定词信息体地址字段ASDU长度计算连续地址0x80 | nn为点数仅首地址数量固定6B头 n×2B状态品质不连续地址0x00 | nn为点数每个点独立4B地址动态6B头 n×6B地址4B状态1B品质1B源码强制分离是因为混用会导致主站解析崩溃。例如若对不连续地址误用连续模式主站会按“首地址数量”推算后续地址结果读取到错误内存位置。Analysis101Test.class里有个典型测试用例// Analysis101Test.java Test public void testUncontinuousASDU() { int[] addrs {1001, 1005, 1010}; boolean[] vals {true, false, true}; byte[] frame new UnContinuousAddressBuilder().buildSinglePoint(addrs, vals); // 验证第2个点地址是否为0x000003F51005的十六进制 assertEquals(0x00, frame[12]); // 地址字节1 assertEquals(0x00, frame[13]); // 地址字节2 assertEquals(0x03, frame[14]); // 地址字节3 assertEquals(0xF5, frame[15]); // 地址字节4 }这个测试直接校验ASDU中地址字段的字节序和位置证明源码对标准的字节级实现精度——这也是它能用于真实设备联调的关键。3. CRC校验与序列号管理104规约可靠性的双保险机制3.1 CRC-16校验为什么Util.class里的crc16()必须用0xA001多项式104规约要求对APDU不含启动字符0x68进行CRC-16校验且多项式必须为x^16 x^15 x^2 1即0xA001。Util.class的crc16(byte[] data, int offset, int len)方法正是此实现// Util.java public static int crc16(byte[] data, int offset, int len) { int crc 0xFFFF; for (int i offset; i offset len; i) { crc ^ (data[i] 0xFF); for (int j 0; j 8; j) { if ((crc 0x0001) ! 0) { crc (crc 1) ^ 0xA001; // 关键多项式0xA001 } else { crc 1; } } } return crc; }玄学提示很多开发者用错多项式如0x8005导致主站拒收帧。IEC 60870-5-104标准明确指定0xA001且校验范围是“APDU长度字段之后的所有字节直到结束符前”。源码中buildApdu()调用crc16()时传入的offset是APDU长度字段起始位置即bb.position() - 2len是长度字段后所有字节数——这与标准完全一致。3.2 发送/接收序列号滑动窗口如何防止乱序与丢包104规约用I帧的控制域携带发送序号N(S)和接收序号N(R)实现滑动窗口流量控制。源码在Telemetry104.class中维护两个原子整数private AtomicInteger sendSeq new AtomicInteger(0); private AtomicInteger recvSeq new AtomicInteger(0); // 发送I帧时 public void sendIframe(byte[] asdu) { ByteBuffer bb ByteBuffer.allocate(10 asdu.length); bb.put((byte) 0x68); bb.put((byte) (asdu.length 6)); // APDU长度 ASDU长 6B头 // 控制域0x40 | (sendSeq.get() 1) | ((recvSeq.get() 0x7FFF) 16) int ctrl 0x40 | (sendSeq.getAndIncrement() 1) | (recvSeq.get() 16); bb.put((byte) (ctrl 0xFF)); bb.put((byte) ((ctrl 8) 0xFF)); bb.put((byte) ((ctrl 16) 0xFF)); bb.put((byte) ((ctrl 24) 0xFF)); bb.put(asdu); // 计算CRC并追加 int crc Util.crc16(bb.array(), 2, bb.position() - 2); bb.putShort((short) crc); channel.write(bb); }血泪经验sendSeq和recvSeq必须用AtomicInteger因为多线程下如心跳线程和数据发送线程并发修改会导致序号错乱。源码中recvSeq只在收到S帧确认帧时更新recvSeq.set((ctrlWord 16) 0x7FFF)。当sendSeq达到0x7FFF时会自动回绕到0——这符合标准规定的15位序号空间。3.3 超时重传机制T1/T2定时器如何与ACK绑定104规约定义三个超时T1发送超时发I帧后等待ACK、T2无数据时心跳超时、T3确认超时发S帧后等待下一个I帧。源码用ScheduledExecutorService模拟// Telemetry104.java private void startT1Timer(int seq, byte[] frame) { t1Timer.schedule(() - { if (!ackReceived.contains(seq)) { // ackReceived是ConcurrentHashMapInteger, Boolean logger.warn(T1 timeout for seq {}, resending..., seq); channel.write(ByteBuffer.wrap(frame)); // 重传次数限制避免无限循环 if (resendCount.get(seq) 3) { resendCount.merge(seq, 1, Integer::sum); startT1Timer(seq, frame); } } }, 15, TimeUnit.SECONDS); // T115s }关键设计ackReceived用ConcurrentHashMap存储已确认的发送序号resendCount记录重传次数。当收到S帧时解析其N(R)字段将所有 N(R)的序号从ackReceived中移除——这实现了滑动窗口的ACK累积确认。若主站发来N(R)5则序号0~4全部视为已确认。4. 规约解析避坑指南六个让工程师凌晨三点还在抓包的真实问题4.1 现象Wireshark显示APDU长度字段为0x00但主站报“帧格式错误”原因buildApdu()中APDU长度字段未按小端序Little Endian填充。IEC 60870-5-104规定长度字段为1字节值为ASDU长度6但源码中bb.put((byte) asduLen)直接写入而asduLen可能255如批量遥信点超过250个。解决长度字段必须是ASDU长度6对256取模即bb.put((byte) (asduLen % 256))。源码Util.class的buildApdu()已修正此问题但旧版常漏掉取模。4.2 现象遥信变位能收到但时间标签总是00:00:00原因ASDU类型标识0x09带时标单点信息要求信息体包含7字节CP56Time2a时间戳但源码Telemetry104.class的buildSinglePointWithTS()方法中Util.getTimeBytes()返回的时间戳字节序错误——标准要求毫秒低位在前Little Endian而代码用了高位在前。解决修改Util.getTimeBytes()将int ms (int)(System.currentTimeMillis() % 1000)的4字节按ms 0xFF, (ms8)0xFF, (ms16)0xFF, (ms24)0xFF顺序写入。4.3 现象主站发来遥控命令RTU执行后回复确认帧但主站仍超时重发原因遥控ASDU类型标识0x2D的确认帧必须用相同ASDU类型传送原因0x0A激活确认但源码ParamePreset104.class中buildRemoteControlAck()误用了0x06周期。解决检查TransferReason.class确保遥控确认的TRANSFER_REASON_ACTIVATION_CONFIRMATION值为0x0A并在构建确认帧时显式设置。4.4 现象TCP连接正常但channel.read()始终返回0无任何数据原因SocketChannel.configureBlocking(false)后未注册SelectionKey.OP_READ事件到Selector导致NIO轮询无法触发读就绪。源码Telemetry104.class的initSelector()方法缺失channel.register(selector, SelectionKey.OP_READ)。解决在connect()成功后立即执行channel.register(selector, SelectionKey.OP_READ, this)并将selector.select()放入独立线程循环。4.5 现象同一ASDU中多个遥测点主站只解析出第一个点的值原因遥测ASDU类型标识0x09的信息体地址是2字节但源码ContinuousAddressBuilder.class中buildScaledValue()用bb.putShort((short)addr)写入当地址32767时变为负数如0x8001→-32767主站解析失败。解决改用bb.put((byte)(addr 0xFF)); bb.put((byte)((addr 8) 0xFF));手动拆分为高低字节严格遵循标准字节序。5. 实战验证用三步法确认你的104规约解析是否真正可用5.1 步骤一用Analysis101Test.class跑通核心单元测试源码自带Analysis101Test.class这是验证解析正确性的第一道防线。重点运行以下三个测试测试方法验证目标通过标准testBuildSinglePointASDU()连续地址遥信ASDU字节级正确性输出字节数组前10字节匹配0x68,0x0E,0x40,0x00,0x01,0x83,0x06,0x00,0x01,0x00testParseASDUType1()解析ASDU类型标识0x01的遥信值ASDU.getTypeId()返回1getBooleanValue(0)返回truetestCRC16()CRC校验算法准确性输入{0x01,0x02,0x03}返回0x1A91标准查表值提示若testParseASDUType1()失败先检查ASDU.class的parse()方法中buffer.get()读取位置——常见错误是跳过控制域后未重置buffer.position()导致后续读取偏移。5.2 步骤二用Wireshark捕获真实交互帧反向验证源码输出将源码编译为JAR在Windows工控机上运行连接真实主站如南瑞NS3000。打开Wireshark过滤tcp.port2404捕获一帧遥信上送0000 68 14 00 00 00 00 01 83 06 00 01 00 00 01 00 01 h............... 0010 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0020 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0030 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0040 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0050 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0060 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0070 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0080 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0090 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 00a0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 00b0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 00c0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 00d0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 00e0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 00f0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0100 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0110 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0120 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0130 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0140 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0150 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0160 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0170 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0180 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0190 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 01a0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 01b0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 01c0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 01d0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 01e0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 01f0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0200 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0210 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0220 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0230 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0240 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0250 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0260 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0270 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0280 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0290 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 02a0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 02b0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 02c0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 02d0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 02e0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 02f0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0300 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0310 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0320 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0330 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0340 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0350 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0360 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0370 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0380 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 0390 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 03a0 00 01 00 01 00 01 00 01 00 01 00 01 00 01 00 01 ................ 03b0 00 01 0 p a hrefhttps://download.csdn.net/download/weixin_42696271/25531294 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表