
简介IEC 104规约客户端与服务端仿真工具面向电力系统自动化领域的开发者和测试工程师用于模拟SCADA主站与RTU/保护装置之间的通信过程可在无真实设备环境下验证遥测、遥信、遥控、遥调等数据传输链路。压缩包共137个文件包含24个sbr、24个obj、17个h、14个cpp、4个dll、4个exe等既提供可运行的程序与动态库也包含VC工程源码、资源与调试文件便于二次修改和深入分析规约实现。包体大小14.55MB轻量实用。压缩包内含IEC104Master与IEC104Slave两套工程分别对应客户端与服务端仿真可模拟断线重连、错误处理、流量控制等场景并支持通过日志或界面监控数据交换适合进行功能测试、压力测试以及规约细节学习。该资源已有710人学习是一份兼顾源码研读与直接部署的实用工具。1. 变电站深夜调试的痛IEC 104 规约客户端、服务端仿真工具到底在仿什么凌晨两点的变电站调试现场后台监控画面上遥信点纹丝不动调度端反复说总召唤已经发出你对着抓包窗口翻了好几屏也没看出哪一帧有问题。这时候最缺的不是规约手册而是一台能扮演“对端”的仿真工具。IEC 104 规约客户端、服务端仿真工具解决的就是这个问题把调度主站和测控装置之间的黑匣子拆开用一台笔记本充当任意一侧让每一帧报文的交互过程都看得见、改得动、能重放。客户端仿真扮演调度端服务端仿真扮演测控装置两者配合起来设备没进场也能把链路验收工作往前赶。这篇笔记覆盖从零搭这套仿真环境的步骤、关键参数对照和 5 个实践中高频踩坑的细节适合调度自动化、变电检修、继保调试方向的工程师也适合想快速上手的在校生照着复现。2. 先看协议再看工具IEC 104 的帧结构、时序与仿真选型2.1 APCI 帧和 TCP 端口 2404链路能建立不代表通信正常IEC 60870-5-104 是电力远动最常用的规约之一传输层固定走 TCP 2404 端口。我最初有个误解以为 TCP 三次握手完成就能直接发遥测数据实际抓包才发现连接建立后客户端必须先发 U 帧 STARTDT_ACT十六进制 68 04 07 00 00 00等服务端回 STARTDT_CON68 04 0B 00 00 00双方才被允许交换 I 帧。这个状态机在从零写 socket 层时非常容易漏因为很多协议栈会自动处理握手一旦自己实现就跳过这一步——后果是服务端一直接收但永不回应。在 TCP 字节流上每帧 APDU 的结构是1 字节起始符 0x681 字节长度字段长度值等于 APDU 减去起始符和长度字段本身也就是 4 字节 APCI 加 ASDU 部分再往后是 4 字节 APCI 控制域。抓包看原始字节流会感觉全是乱码但只要先按长度字段切帧再按控制域低 2 位识别帧类型整个交互过程就能一行一行读出来。控制域低 2 位决定帧类型00 是 I 帧携带 ASDU并且带发送序号和接收序号01 是 S 帧只带接收序号用于纯确认不携带数据11 是 U 帧带命令位用于 STARTDT、STOPDT、TESTFR 这类链路控制。I 帧的序号在仿真里必须按协议自增服务端收到序号不对的 I 帧时可以选择丢弃不处理这会让主站侧看到超时。TCP 是流协议一次 recv 收到两帧半属于常态。仿真工具的接收循环必须做字节缓冲先攒数据解析长度字段取够一帧再处理剩下的留在缓冲区等下一次 recv。我见过不止一个自研仿真脚本在一次性收到多个 APDU 时按“一个包一帧”处理导致帧被切错后面的 ASDU 全部错位。这不是协议问题是流协议处理的基本功但它是仿真工具里最常见的翻车点之一。连接建立后的行为还受五个时序参数控制仿真时不一定全要调整但必须知道每个参数在管什么参数默认值作用仿真里最容易踩的地方t030 秒TCP 连接建立超时客户端在非直连网络里连不上t0 内没建连直接判失败t115 秒发送报文后等确认的超时总召唤发出后服务端不回t1 超时后客户端重发或断开t210 秒接收侧确认的最大间隔大量遥信灌进来接收侧超过 t2 不确认会被对方判超时t320 秒空闲链路测试帧周期仿真挂机超过 20 秒不发测试帧对端直接踢你下线k/w12/10发送/确认窗口批量上送超过 k 帧未确认时发送侧停下来等确认这里面跟仿真关系最大的是 t1 和 t3。t1 影响你调一个命令后多久能判断“没收到确认”t3 影响空闲挂机时链路能否保持。t0 在跨网段调仿真时才会用到k 和 w 多数情况下不必动但你在做压力测试一次上送几千个遥信点时k 值决定了发送侧在等待确认前最多能突发的帧数。2.2 ASDU 与信息对象遥测、遥信、遥控在仿真里怎么表达TCP 层之上IEC 104 沿用了 IEC 101 的 ASDU 结构。仿真工具要重点搞清楚三件事应答什么、什么时候应答、用什么传输原因应答。ASDU 头部依次是类型标识、可变结构限定词、传输原因、公共地址之后跟随信息对象。类型标识决定这个报文是遥信、遥测还是遥控命令传输原因决定这个报文是主动上报、激活确认还是总召唤响应。一组最常用的类型标识和传输原因的对应关系实际调仿真时可以直接照这个表配类型ID缩写语义典型传输原因1M_SP_NA_1单点遥信3突发/ 20响应总召唤9M_ME_NA_1归一化遥测3突发/ 20响应总召唤45C_SC_NA_1单点遥控6激活/ 7激活确认100C_IC_NA_1总召唤6激活/ 10激活终止103C_CS_NA_1时钟同步6激活/ 7激活确认信息对象地址IOA是每个点位的编码。仿真工具里通常用一个数组或者哈希表维护所有点位总召唤时把表里的点逐个按 IOA 下发每个点位包含状态值、品质位和时标。IOA 占用 3 字节超过 65535 的站内点位编码在仿真中非常常见写代码时不要用两个字节去存。可变结构限定词VSQ低 7 位表示这一帧里连续装载的点个数最高位表示地址是否连续。地址连续时可以把多个点塞进一帧地址不连续时每个点单独占一帧。仿真中如果图省事把不连续的点拼进同一帧接收方解析出来的点位就是错位的。2.3 仿真工具三条路开源库封装、自研脚本、商业软件怎么选选仿真工具的第一原则是你的掌控要到字节级而不是只挑最省事的那个。三条路线我都试过各自的边界很清楚。开源库封装是上手最快的方式。GitHub 上有不少基于 C/C、Java、.NET 或 Python 的 104 协议栈实现多数自带服务端和客户端两种角色示例编译后直接跑通。适合做链路前置验证、给上位机做配套工具。风险在于库对非标 ASDU厂商私有扩展的支持参差不齐遇到工程私有定义的类型会卡住而且出了问题你未必看得懂库里封装好的逻辑。自研脚本是掌控最强、也最能练基本功的路线。用 Python 的 socket 加上缓冲和状态机两百行代码就能覆盖大部分调试场景。前面说的 APCI 拆帧、ASDU 编解码全在你自己代码里现场遇到一致性争议随时加日志打点。代价是开发周期长TCP 异常处理要自己维护。商业测试软件包括厂家的规约测试仪在稳定性和测试报告方面最好但字节级分析反而不够灵活。一些现场问题需要单独拆出一帧来看在商业软件里反而拿不到原始字节。三者的取舍可以用一张表概括维度开源库封装自研脚本商业测试软件上手速度快慢最快协议透明中高低二次开发中高低典型用途工程前置验证报文级疑难排查出厂/验收测试项目里的实际做法工程前期用开源库快速验证链路遇到规约不一致的疑难 case 才用自研脚本逐字节分析商用软件配合正式验收时使用。三者不是互斥关系但如果你只想投入一种我建议从自研脚本开始——那个过程能让你对 IEC 104 的理解发生质变。3. 用自研脚本搭一套最小可复现的仿真环境客户端与服务端都能跑3.1 服务端仿真绑定 2404、U 帧应答与 I 帧序号管理先写服务端从 TCP 层开始。下面的代码是一个最小可运行的服务端监听 2404、正确应答 U 帧、解析 I 帧并打印 ASDU 字节完整功能在此基础上扩展。# iec104_server.py import socket import threading # U帧命令字 STARTDT_ACT 0x07 # 客户端请求启动 STARTDT_CON 0x0B # 服务端确认启动 TESTFR_ACT 0x43 # 测试帧请求 TESTFR_CON 0x83 # 测试帧确认 def u_frame(cmd): 构造U帧68 04 cmd 00 00 00 return bytes([0x68, 0x04, cmd, 0x00, 0x00, 0x00]) def i_seq(frame): 从I帧APCI解析发送序号 return (frame[3] 7) | (frame[2] 1) def handle_conn(conn, addr): buf b while True: try: data conn.recv(1024) except ConnectionResetError: break if not data: break buf data # 按长度字段循环切帧 while len(buf) 2 and buf[0] 0x68: apdu_len buf[1] 2 # 起始符长度字段本身也要算进去 if len(buf) apdu_len: break # 帧没凑齐等下一次recv frame buf[:apdu_len] buf buf[apdu_len:] c frame[2] if c 0x03 0x03: # U帧处理 if c STARTDT_ACT: conn.send(u_frame(STARTDT_CON)) elif c TESTFR_ACT: conn.send(u_frame(TESTFR_CON)) # STOPDT_ACT(0x13)收到后回STOPDT_CON(0x23) elif c 0x13: conn.send(u_frame(0x23)) elif c 0x03 0x01: # S帧只确认无需回复 pass else: # I帧解析序号ASDU从第6字节开始 seq i_seq(frame) asdu frame[6:] print(f[I] seq{seq} asdu{asdu.hex()}) # 在这里根据asdu[0]类型ID做业务应答 conn.close() def main(): srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((0.0.0.0, 2404)) srv.listen(5) print([server] listening on 2404) while True: conn, addr srv.accept() threading.Thread(targethandle_conn, args(conn, addr), daemonTrue).start() if __name__ __main__: main()这段代码的关键在于recv 返回的数据先追加到 buf再用内层 while 按 buf[1] 长度字段切分。只有凑齐了一帧的长度才处理避免 TCP 粘包/半包导致的切帧错位。handle_conn 里对 U 帧的分支处理就是 2.1 节说的链路状态机只要实现了 STARTDT_ACT 应答客户端就能顺利进入数据传输阶段。I 帧的 seq 解析用来做发送序号的连续性校验实际工程里遇到不连续序号时应该直接丢弃并等待重发不要继续处理后面的 ASDU。线程模型用的是每连接一线程支持多客户端同时连入。这在仿真场景里是合理做法一个服务端仿真可以同时被多个客户端调度主站、厂站后台、调试笔记本连接每个连接独立维护自己的链路状态和序号互不干扰。3.2 客户端仿真连接握手、总召唤与遥信接收客户端仿真负责扮演主站核心动作是建立连接、启动传输、发总召唤、周期发送测试帧。最小实现如下# iec104_client.py import socket STARTDT_ACT 0x07 def u_frame(cmd): return bytes([0x68, 0x04, cmd, 0x00, 0x00, 0x00]) def i_frame(seq, recv_seq, asdu): 组装I帧seq和recv_seq都按实际序号填入 body bytes([0x68, len(asdu) 4, (seq 1) 0xFF, (seq 7) 0xFF, (recv_seq 1) 0xFF, (recv_seq 7) 0xFF]) return body asdu def asdu_c_ic_na(common_addr1, io_addr0): 总召唤ASDU类型100传输原因6召唤限定词0x14 return bytes([ 0x64, 0x01, 0x06, common_addr 0xFF, (common_addr 8) 0xFF, io_addr 0xFF, (io_addr 8) 0xFF, (io_addr 16) 0xFF, 0x14 ]) s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((127.0.0.1, 2404)) s.send(u_frame(STARTDT_ACT)) resp s.recv(6) print([client] startdt resp:, resp.hex()) send_seq 0 recv_seq 0 # 发送总召唤I帧序号从0开始 asdu asdu_c_ic_na(1, 0) s.send(i_frame(send_seq, recv_seq, asdu)) send_seq 1 buf b while True: try: data s.recv(1024) except socket.timeout: continue if not data: break buf data while len(buf) 2 and buf[0] 0x68: total buf[1] 2 if len(buf) total: break frame buf[:total] buf buf[total:] c frame[2] if c 0x03 0x03: # 收到U帧处理器主要看TESTFR_ACT pass elif c 0x03 0x01: # S帧确认 recv_seq (frame[4] 7) | (frame[3] 1) else: # I帧携带ASDU seq (frame[3] 7) | (frame[2] 1) asdu_body frame[6:] type_id asdu_body[0] cot asdu_body[2] print(f[rx] seq{seq} type{type_id} cot{cot} ioas{asdu_body[4:7].hex()})客户端代码大概 60 行覆盖了最常用的交互路径。asdu_c_ic_na 函数里几个字节值要说明一下0x64 是总召唤类型 ID0x01 是可变结构限定词1 个对象0x06 是传输原因激活公共地址按 2 字节小端填组包时注意字节序IOA 按 3 字节小端填0x14 是召唤限定词固定为总召唤。帧头不动只改 ASDU是调整仿真行为最快的路径。想调点位时把 asdu_body 换成遥信或遥测的组包函数比如类型 1 的 M_SP_NA_1数据区填 IOA 加状态值和品质位。客户端和服务端互相配合就能模拟出完整的遥信上送链路。3.3 参数对照把 t0/t1/t2/t3、k/w 按场景调对仿真工具要调参数时最容易改错的是 t1 和 t3 的配合。t1 是发送后等待确认的时间t3 是空闲时发送测试帧的周期。如果 t1 小于链路往返时间客户端会在服务端确认到达之前就超时重发导致序号错乱如果 t3 设置过长而 t1 又偏短链路空闲时服务端不会主动清理半开连接等待时间超过服务端容忍上限就会掉线。一个稳妥的起始参数组合是t0 30st1 15st2 10st3 20sk 12w 8。大多数局域网场景都不用改需要跨公网或无线链路调试时把 t1 放大到 30s 以上t3 保持 20s 以下即可。在代码里没有实现完整参数表的场景下也建议至少把测试帧收发做成一个独立线程每 t3 秒发一次 TESTFR_ACT收到 TESTFR_CON 才算链路保持。仿真挂机时最容易出的问题就是测试帧没人理20 秒后对端强制断链。4. IEC 104 仿真高频踩坑从链路半开到品质位5 个真实案例细节4.1 链路连接正常总召唤发出后却迟迟无响应现象客户端 TCP 连上、U 帧握手也对总召唤 I 帧发出去后服务端就是没有回包。抓包看服务端确实收到了数据但应用层完全没反应。原因链路状态机没有真正进入数据传输阶段。很多从零写的服务端只实现了 TCP accept没有在收到 STARTDT_ACT 后回 STARTDT_CON客户端在发出总召唤前实际上还处于未启动状态I 帧被服务端当成无效帧丢弃。另一种常见原因是公共地址不匹配客户端总召唤 ASDU 里填的公共地址是 2服务端点位表里公共地址配的是 1地址对不上服务端按规约逻辑直接丢弃。解决先确认服务端收到 STARTDT_ACT 并回了 STARTDT_CON再查总召唤 ASDU 的公共地址是否与从站一致。给服务端打印收到的 ASDU 字节比较公共地址字段有问题直接改成一致。调试时我习惯把公共地址做成配置项不写死在代码里。4.2 遥信点翻转了后台监控画面一动不动现象仿真服务端按周期上送遥信客户端确实收到了 ASDU但画面里的点位不翻转日志里看不到任何状态变化。原因IOA 越界或者 VSQ 组装错误。点位实际编号是 1000 开头仿真代码里却用两字节存 IOA超过 65535 的高位被截掉后台自然找不到这个点位。另一种是 VSQ 的地址连续位用错把地址不连续的点位连续填充在同一帧里后台解析时点位号全乱了。解决核对点位表的 IOA 编码确认 3 字节完整写入VSQ 最高位按实际情况置 0 或 1不连续点位每个对象单独成帧。经验是先用单点一帧的方式排除 VSQ 问题再逐步合并成连续帧做效率测试。4.3 时钟同步反复失败时标对不上现象客户端发 C_CS_NA 时钟同步命令服务端回了确认但后台始终报时标异常或者同步成功后几分钟又跳回旧值。原因时标字节序和 CP56Time2a 格式处理错误。CP56Time2a 的 7 字节里毫秒占前 2 字节且按小端存储仿真代码转成整数后毫秒数颠倒还有的是把“星期几”等保留位当数据代码解析。解决先把时标按字节拆开打印确认低 2 字节毫秒的字节序。CP56Time2a 的标准布局是毫秒 2 字节、分钟、小时、日、月/星期位、年加修正位。仿真中只实现到“精确到秒”的级别时间字段按顺序写入即可但字节序必须跟现场主站侧保持一致。4.4 仿真挂一晚上第二天发现链路已经掉线现象TCP 连接显示还在但没有任何数据活动重启客户端才能恢复对端日志显示早已断链。原因t3 测试帧机制没有正常工作。仿真客户端在空闲时没有按 t3 周期发送 TESTFR_ACT或服务端没有回 TESTFR_CON另一种更隐蔽的场景是网络链路断开了比如笔记本休眠TCP 在纯空闲时探测不到断链等应用层发数据才发现。解决客户端实现独立的测试帧线程按 t3 间隔发送同时启用 TCP KeepAlivesetsockopt 的 SO_KEEPALIVE间隔设为小于 t3。服务端同样在空闲状态接到 TESTFR_ACT 要立即回 TESTFR_CON。挂机掉线这类问题多半是这两边的机制缺了其中一个。4.5 同一帧报文仿真工具和现场装置解读不一致现象仿真工具抓下来看着正常的报文接上真实测控装置后后台点位或状态显示不合理两边对同一帧的解释对不上。原因品质位和传输原因没有按现场规约配置去填。很多仿真代码默认把品质位置 0好品质但有些站需要按实际情况把品质位标记为“无效”“溢出”等特别是初始化阶段上送的点位品质位必须按初始状态处理。传输原因也一样总召唤响应用 20响应总召唤如果用 3突发上了这一帧主站和时间点对不上数据处理就会错位。解决在仿真里把品质位改成可配置初始化时按点位表初始状态生成品质传输原因严格按协议定义填。遇到现场不一致先导出双方报文做字节级比对不要凭直觉猜点位。5. 把仿真工具推进自动化回归录波回放、场景脚本与交叉验证5.1 把现场抓包变成回归用例仿真工具用顺了以后最值得投入的是把现场抓包文件转成回归用例。常见做法是现场出问题时用抓包工具导出一份完整交互事后用仿真客户端回放同一组报文观察服务端行为是否和现场一致。回放时把报文里的时间戳替换成当前时间序号字段按帧重建然后逐帧对比服务端的响应。这样既能复现现场场景修完代码后再次回放也能验证修复没有把其它路径弄坏。5.2 场景脚本化用状态机验证遥控-遥信联动遥控联动是调度自动化里最容易出回归的场景。例如遥控合闸后期望测控装置在 1 秒内返回合位遥信。我在仿真客户端里用状态机写这种场景发 C_SC_NA类型 45传输原因 6→ 等激活确认类型 45传输原因 7→ 等遥信变位类型 1传输原因 3→ 判断超时。用同一套状态机把点位表参数化可以跑完一个间隔的所有开关。这比手工点点位可靠得多也适合接进服务端接口测试的自动化体系。5.3 双仿真交叉验证客户端和服务端互相对打最后说一个我坚持了很久的习惯把服务端仿真和客户端仿真同时启动两者互为镜像、互相对打是验证协议实现最彻底的方式。服务端发出的每一帧客户端都能按规约解析客户端发出的每一个命令服务端都能按状态机应答。如果两端对同一帧的解析结果不一致问题一定出在组帧或解析的边界条件上。我会在这种交叉验证里故意构造边界IOA 取最大值、时标填 0xFF、ASDU 长度填满确保实现没有溢出和静默丢弃。仿真工具的边界就是你对规约理解的边界。我在这个方向上最大的收获是把 5 分钟能跑通的链路验证变成了一套可反复执行的回归资产最大的教训是永远不要在仿真里跳过 U 帧握手和 t3 测试帧它们在真实现场迟早会变成掉线事故。希望这篇笔记里列的参数和踩坑记录能帮你少走一段弯路也祝你把 IEC 104 仿真做成真正可靠、随时可用的调试底座。本文还有配套的精品资源点击获取