
简介南瑞继保网络103规约是面向电力系统自动化从业者、远动调试人员及二次开发工程师的技术文档围绕IEC 60870-5-103标准在南瑞继保设备上的实现展开重点解决RTU与调度中心、集控站之间遥测、遥信、遥控、遥调等四遥数据交换的协议理解与工程应用问题。资源包共1个doc文件约3.38MB内容涵盖规约的兼容性扩展、功能码与报文格式、安全验证与数据加密机制、通信性能优化、故障诊断恢复策略以及自定义服务接口等关键知识点并对网络拓扑适应性和数据编解码方式进行了说明。目前已有1973人学习下载适合从事变电站自动化、调度通信调试及系统集成的技术人员参考。通过阅读可系统掌握南瑞继保103规约的帧结构、命令帧与应答帧交互逻辑、错误处理流程为现场调试、协议对接和二次开发提供可落地的排错思路与实现依据。1. 南瑞继保网络103规约从报文黑匣子到能抓能解的实战路径如果你在变电站自动化现场干过调试大概率遇到过这样的场景后台监控画面上某个间隔的遥测值突然不刷新了或者遥控命令下发后装置毫无反应而屏柜后面的保护装置面板显示一切正常。这时候老师傅通常会拎着笔记本往站控层交换机上一插打开抓包工具盯着满屏的十六进制报文说一句——“看103规约的链路断了”。南瑞继保网络103规约就是这套排查动作背后的核心通信协议。它脱胎于IEC 60870-5-103规约原本是串口上的主从问答式协议后来被映射到TCP/IP之上形成了“网络103”。在国内变电站尤其是南瑞继保的保护测控装置与后台监控系统之间这套规约至今仍是主力。它不像IEC 61850那样模型自描述、配置复杂也不像Modbus那样简单粗暴而是卡在中间——有固定帧格式、有链路层确认机制、有应用层信息元素但文档散落、抓包工具默认不解析、不同厂家实现还有细微差异。这篇文章面向的是需要现场调试、故障排查、或者自己写工具解析网络103报文的工程师。我会从协议帧结构讲起给出可复现的解析代码拆解链路建立、总召唤、遥控这些关键流程最后落到几个只有踩过坑才知道的排查技巧上。不堆术语直接上能跑的东西。2. 网络103规约的帧结构与链路层为什么抓到的报文总对不上2.1 从串口103到网络103封装方式决定了你抓包的位置IEC 60870-5-103的原始形态是串行链路上的异步通信采用FT1.2帧格式有固定帧长和可变帧长两种。固定帧用于链路确认、请求链路状态、复位等长度固定为5字节不含校验和起始符可变帧用于传输数据长度随信息元素变化。当它被搬到TCP/IP上最常见的做法是保留FT1.2的帧结构直接作为TCP payload传输。南瑞继保的装置通常监听TCP 2404端口这个端口号在IEC 60870-5-104里是标准端口但103 over TCP并没有强制规定只是现场约定俗成用2404的居多。这里第一个容易翻车的点就来了你在交换机上做端口镜像抓包Wireshark默认把2404端口识别为104规约然后按104的APCI格式去解析结果满屏红字“Malformed Packet”。实际上你抓到的payload是完整的FT1.2帧只是被错误地套用了104的解析器。解决办法很简单在Wireshark里右键报文选“Decode As”把TCP 2404指定为“Data”或者直接看Raw十六进制。我一般会先看TCP payload的前几个字节如果以0x68开头那是104的启动字符如果以0x10或0x68开头但后面跟的是103的链路控制域就需要手动区分。南瑞继保网络103的固定帧启动字符是0x10可变帧启动字符是0x68这一点和104的0x68容易混淆但固定帧的0x10是103独有的。2.2 FT1.2帧格式拆解固定帧与可变帧的字节级差异固定帧长5字节结构如下启动字符0x101字节、控制域1字节、链路地址1字节、校验和1字节、结束字符0x161字节。控制域里包含了链路功能码比如请求链路状态、复位链路、确认等。可变帧结构稍复杂启动字符0x681字节、长度域L1字节表示后续用户数据长度、长度域L重复1字节、启动字符0x68重复1字节、控制域1字节、链路地址1字节、ASDU应用服务数据单元长度可变、校验和1字节、结束字符0x161字节。注意长度域L的计算范围是从控制域开始到校验和之前的所有字节数不包括启动字符、长度域本身和结束字符。这个定义如果搞错解析出来的ASDU长度就会偏移导致后续所有字段错位。下面这段Python代码演示了如何从TCP payload中提取并解析一个完整的可变帧。代码假设你已经从抓包文件或socket中拿到了原始bytes。import struct def parse_ft12_frame(data: bytes): 解析FT1.2帧返回帧类型和字段字典 data: 从TCP payload中提取的完整帧字节 if len(data) 5: return None, {error: frame too short} start data[0] if start 0x10: # 固定帧 if len(data) ! 5: return None, {error: fixed frame length mismatch} ctrl data[1] link_addr data[2] checksum data[3] end data[4] if end ! 0x16: return None, {error: invalid end byte} return fixed, { control: ctrl, link_addr: link_addr, checksum: checksum } elif start 0x68: # 可变帧 if len(data) 8: return None, {error: variable frame too short} L data[1] L_rep data[2] start_rep data[3] if L ! L_rep or start_rep ! 0x68: return None, {error: length or start repeat mismatch} # 计算帧总长启动(1) L(1) L重复(1) 启动重复(1) L字节用户数据 校验(1) 结束(1) total_len 4 L 2 if len(data) total_len: return None, {error: incomplete frame} ctrl data[4] link_addr data[5] asdu data[6:6L-2] # L包含控制域和链路地址所以ASDU长度是L-2 checksum data[6L-2] end data[6L-1] if end ! 0x16: return None, {error: invalid end byte} return variable, { control: ctrl, link_addr: link_addr, asdu: asdu, checksum: checksum } else: return None, {error: funknown start byte 0x{start:02x}}这段代码的关键逻辑在于长度域L的解读。L的值等于控制域、链路地址、ASDU三部分的总字节数。所以ASDU的实际长度是L减去2控制域1字节链路地址1字节。校验和的计算范围是从控制域到ASDU最后一个字节采用算术和取反加一的方式但很多网络103实现并不严格校验抓包时可以先跳过校验直接看内容。参数方面链路地址在103规约里通常对应装置的物理地址南瑞继保的装置一般从1开始编号后台通过链路地址区分不同间隔。如果你在解析时发现链路地址和预期不符先检查抓包位置是否经过了规约转换器或网关有些网关会修改链路地址。2.3 控制域功能码链路握手为什么总在“请求链路状态”控制域是链路层的心脏。对于固定帧控制域的低4位是功能码高4位保留或用于其他标志。103规约定义的功能码包括0x0 复位链路、0x1 请求链路状态、0x2 请求链路状态并期待确认、0x3 确认、0x4 用户数据确认、0x5 链路忙、0x6 链路空闲、0x7 否定确认等。实际抓包时你会看到后台和装置之间反复出现“请求链路状态”和“确认”的配对。如果装置一直不回确认或者回“链路忙”后台就会不断重试最终报通信中断。这里有个血泪经验南瑞继保的部分装置在链路层有“链路状态保持”机制如果后台超过一定时间没有发送任何帧装置会主动断开TCP连接。这个超时时间不同版本不一样有的30秒有的60秒。所以你在写测试工具时不能只建立连接就不管了要定期发链路状态请求或者总召唤来维持链路。另外控制域里的FCB位帧计数位和FCV位帧计数有效位用于防止帧重复和丢失在可变帧传输时发送方会翻转FCB接收方通过比较FCB来判断是否是新帧。如果你自己实现主站忘记翻转FCB装置会认为你在重发旧帧直接丢弃。3. 应用层ASDU解析总召唤、遥测、遥信和遥控的报文长什么样3.1 ASDU类型标识与信息元素从类型号反查数据含义ASDU是应用层的数据单元结构为类型标识1字节、可变结构限定词1字节、传送原因1字节、公共地址1字节、信息对象地址2字节或3字节103规约通常用2字节、信息元素集。类型标识决定了这个ASDU携带的是什么数据。网络103常用的类型标识包括0x01 带时标的报文、0x02 带相对时间的报文、0x03 被测值、0x04 带时标的被测值、0x05 标识、0x06 带时标的标识、0x07 总召唤启动、0x08 总召唤结束、0x09 带时标的被测值II型、0x0A 通用分类数据、0x0B 通用分类标识、0x0C 通用分类命令、0x0D 通用分类命令确认、0x0E 遥控命令、0x0F 遥控命令确认、0x10 遥控命令终止等。注意不同厂家可能对类型标识有扩展南瑞继保在通用分类服务里定义了自己的数据集用于传输保护定值、故障报告等。如果你抓到一个类型标识为0x0A的ASDU那大概率是通用分类数据需要结合装置的点表来解析具体含义。可变结构限定词的最高位表示信息元素是否按顺序排列低7位表示信息元素的数量。传送原因占1字节常见值0x03 突发、0x05 请求或被请求、0x06 激活、0x07 激活确认、0x08 停止激活、0x09 停止激活确认、0x0A 激活终止、0x14 响应站召唤、0x15 响应第1组召唤等。公共地址在103里通常等于链路地址但有些实现会固定为0xFF或0x00。信息对象地址是2字节低字节在前高字节在后对应装置内部的信号点号。3.2 总召唤流程从后台发起请求到装置上送全数据总召唤是后台获取装置全部遥测遥信的标准流程。后台先发一个类型标识0x07的ASDU总召唤启动传送原因为0x06激活装置回一个0x07且传送原因为0x07激活确认的ASDU然后装置开始逐个上送类型标识0x01、0x02、0x03、0x04等的数据ASDU传送原因为0x14响应站召唤。全部数据送完后装置发一个类型标识0x08的ASDU总召唤结束传送原因为0x0A激活终止。后台收到后总召唤流程结束。这里容易踩的坑是总召唤启动ASDU里的信息对象地址通常为0但有些装置要求填特定的组号。南瑞继保的装置一般支持全局总召唤和分组总召唤全局总召唤的信息对象地址为0分组总召唤则填组号。如果你发全局总召唤装置没反应试试分组召唤。另外总召唤过程中如果后台不及时发链路层确认装置可能会因为链路层超时而中断上送。下面这段代码演示了如何构造一个总召唤启动帧并解析装置返回的遥测ASDU。def build_general_interrogation(link_addr: int, common_addr: int 0xFF): 构造总召唤启动可变帧 link_addr: 链路地址 common_addr: 公共地址南瑞继保常用0xFF或0x01 # ASDU: 类型标识0x07, 可变结构限定词0x81(顺序1个元素), 传送原因0x06, 公共地址, 信息对象地址0x0000 asdu bytes([0x07, 0x81, 0x06, common_addr, 0x00, 0x00]) L len(asdu) 2 # 控制域1 链路地址1 ASDU # 控制域主站发送PRM1, FCB0, FCV0, 功能码0x03(用户数据确认)实际总召唤用可变帧控制域功能码为0x03 # 103规约中可变帧的控制域功能码通常为0x03用户数据 ctrl 0x03 # 简化处理实际需根据FCB/FCV翻转 frame bytes([0x68, L, L, 0x68, ctrl, link_addr]) asdu # 计算校验和从控制域到ASDU末尾 checksum sum(frame[4:]) 0xFF checksum (~checksum 1) 0xFF frame bytes([checksum, 0x16]) return frame def parse_measure_asdu(asdu: bytes): 解析遥测ASDU类型标识0x03或0x04 返回信息对象列表 if len(asdu) 6: return [] type_id asdu[0] vsq asdu[1] cot asdu[2] common_addr asdu[3] num vsq 0x7F results [] offset 4 for i in range(num): if offset 2 len(asdu): break ioa asdu[offset] | (asdu[offset1] 8) offset 2 if type_id 0x03: # 被测值通常2字节归一化值或标度化值 if offset 2 len(asdu): break raw asdu[offset] | (asdu[offset1] 8) offset 2 # 归一化值-1到1之间公式 raw/32768 value raw / 32768.0 results.append({ioa: ioa, raw: raw, value: value}) elif type_id 0x04: # 带时标的被测值7字节时标 if offset 2 7 len(asdu): break raw asdu[offset] | (asdu[offset1] 8) offset 2 timestamp asdu[offset:offset7] offset 7 value raw / 32768.0 results.append({ioa: ioa, raw: raw, value: value, ts: timestamp.hex()}) return results构造总召唤帧时控制域的值需要根据链路状态机的当前状态来设置。上面的代码简化了FCB/FCV的处理实际实现中每次发送新帧要翻转FCB位。解析遥测ASDU时注意类型标识0x03和0x04的区别0x03不带时标0x04带7字节时标。时标格式通常是毫秒、秒、分、时、日、月、年但有些厂家会调整顺序。归一化值的转换公式是raw/32768如果装置上送的是标度化值则需要根据点表里的系数和偏移量换算。南瑞继保的装置在总召唤时上送的遥测值可能是归一化值也可能是标度化值取决于装置配置。如果你发现解析出来的值明显偏大或偏小先检查点表里的量程定义。3.3 遥控命令的报文交互选择、执行、确认与超时遥控是网络103里最需要小心处理的部分因为它直接操作一次设备。遥控流程通常分为“选择”和“执行”两个阶段。后台先发遥控选择命令类型标识0x0E传送原因0x06激活装置回遥控选择确认类型标识0x0F传送原因0x07激活确认然后后台发遥控执行命令类型标识0x0E传送原因0x06激活但信息元素里的状态不同装置回执行确认类型标识0x0F传送原因0x07激活确认最后装置可能发一个遥控终止类型标识0x10传送原因0x0A激活终止。这里的关键参数是遥控命令ASDU里的信息元素通常包括遥控命令类型分/合、输出状态、时间参数等。南瑞继保的遥控命令ASDU格式为类型标识0x0E、可变结构限定词0x81、传送原因、公共地址、信息对象地址2字节、遥控命令1字节0x01分闸、0x02合闸、输出状态1字节、时间参数1字节通常为0。如果你发的遥控命令装置不回确认先检查信息对象地址是否正确对应遥控点号再检查公共地址是否匹配。另外遥控选择和执行之间的超时时间一般设为30秒到60秒超过这个时间装置会自动终止遥控流程需要重新选择。我遇到过因为后台在遥控选择后等了太久才发执行装置已经超时终止但后台没收到终止报文导致执行命令发出去后装置直接丢弃后台却显示“遥控成功”的假象。所以调试遥控时一定要在装置侧同时观察报文确认每一步都有对应的确认帧。4. 现场调试避坑网络103规约排查的五个血泪教训4.1 抓包工具默认解析错误导致误判链路断开现象Wireshark抓包显示大量“Malformed Packet”或“TCP Retransmission”但装置面板通信灯正常闪烁。原因Wireshark将TCP 2404端口默认识别为IEC 104规约按104的APCI格式解析103的FT1.2帧自然报错。解决在Wireshark中右键任意2404端口报文选择“Decode As” - “TCP Port” - “2404” - “Data”或“Raw”然后手动查看十六进制。或者直接用“Follow TCP Stream”看原始字节流按0x10和0x68起始符人工分段。我习惯在抓包前先过滤“tcp.port2404 tcp.len0”然后导出为纯文本十六进制用脚本解析避免Wireshark的自动解析干扰。4.2 链路地址不匹配导致总召唤无响应现象后台发总召唤装置无任何回应但TCP连接已建立。原因后台配置的链路地址与装置实际地址不一致。南瑞继保的装置链路地址通常在装置参数里设置默认可能是1但有些工程会改成其他值。另外如果中间有规约转换器转换器可能会修改链路地址。解决先用固定帧“请求链路状态”逐个链路地址试探看哪个地址能收到确认。或者直接抓取后台与装置之间已有的正常通信报文看链路地址字段的值。注意有些装置在TCP连接建立后如果收到的第一个帧的链路地址不对会直接断开TCP连接而不是回否定确认。4.3 校验和计算错误导致帧被静默丢弃现象自己写的测试工具发送的帧装置完全不回复但用抓包工具看帧已经发出。原因FT1.2的校验和计算错误。校验和是从控制域到ASDU最后一个字节的算术和取反加一但有些实现要求校验和包含启动字符和长度域有些则不包含。南瑞继保的装置通常按标准FT1.2计算即从控制域开始。解决先用一个已知正确的报文比如从正常通信中抓到的验证你的校验和函数。如果校验和错误装置链路层会直接丢弃帧不会返回任何错误信息这就是“静默丢弃”非常难排查。我一般会在代码里加一个校验和自检用抓到的正常帧反推校验和算法。4.4 总召唤过程中遥测值跳变或缺失现象总召唤上送的遥测值在后台显示为0或满量程或者部分点号缺失。原因ASDU长度域L计算错误导致解析偏移或者信息对象地址与点表不对应。另外如果装置上送的是带时标的被测值类型标识0x04而你的解析代码按不带时标0x03处理就会把时标字节当成遥测值解析导致数值异常。解决先确认装置实际使用的类型标识可以在抓包中看ASDU第一个字节。然后核对点表里的信息对象地址和量程系数。南瑞继保的装置在总召唤时可能只上送变化的数据而不是全部数据这取决于装置的总召唤配置。如果发现缺失检查装置是否配置了“总召唤上送全部”还是“只上送变化”。4.5 遥控执行后装置无动作但后台显示成功现象后台遥控操作返回成功但一次设备没有动作。原因遥控选择和执行之间的超时或者遥控命令的信息元素格式不对。有些后台在收到选择确认后如果执行命令发送延迟超过装置的超时时间装置已经终止遥控流程但后台可能没有正确处理终止报文仍然显示成功。解决在装置侧同时抓包确认选择确认、执行确认、终止报文是否完整。另外检查遥控命令里的输出状态字节有些装置要求该字节为0x00有些要求为0x01不同版本有差异。最可靠的办法是拿一个已知能正常遥控的点抓取正常操作的报文逐字节对比。5. 用Python写一个网络103解析器从抓包文件到点表映射5.1 用Scapy或原始socket提取TCP payload如果你已经有抓包文件pcap/pcapng可以用Scapy提取TCP payload。Scapy的rdpcap函数可以读取pcap文件然后遍历TCP流按端口过滤。下面代码演示了如何从pcap中提取所有2404端口的TCP payload并按FT1.2帧起始符分段。from scapy.all import rdpcap, TCP import struct def extract_103_frames(pcap_path: str, port: int 2404): 从pcap文件中提取网络103帧 返回帧列表每个元素为bytes packets rdpcap(pcap_path) frames [] for pkt in packets: if TCP in pkt and (pkt[TCP].sport port or pkt[TCP].dport port): payload bytes(pkt[TCP].payload) if not payload: continue # 按起始符分段简单处理假设每个TCP payload包含完整帧或帧的一部分 # 实际需要处理TCP流重组这里简化演示 offset 0 while offset len(payload): if payload[offset] 0x10: if offset 5 len(payload): frames.append(payload[offset:offset5]) offset 5 else: break elif payload[offset] 0x68: if offset 2 len(payload): L payload[offset1] total 4 L 2 if offset total len(payload): frames.append(payload[offset:offsettotal]) offset total else: break else: break else: offset 1 return frames这段代码的局限性在于没有处理TCP流重组。实际网络中一个FT1.2帧可能被拆到多个TCP segment里也可能多个帧合在一个segment里。生产环境建议用Scapy的TCPSession或者自己维护缓冲区。参数port默认2404如果你的现场用了其他端口需要修改。提取出的frames列表可以直接传给前面的parse_ft12_frame函数解析。5.2 将解析结果映射到点表信息对象地址与信号名的对应解析出ASDU后你需要一张点表来把信息对象地址IOA翻译成人类可读的信号名。点表通常由装置厂家提供格式可能是Excel或CSV。南瑞继保的装置点表里遥测、遥信、遥控、电度等分别有不同的IOA范围。比如遥测从0x4000开始遥信从0x0001开始遥控从0x6000开始。下面是一个简单的点表映射示例用字典存储。# 示例点表IOA - (信号名, 类型, 系数) point_table { 0x0001: (断路器位置, 遥信, 1), 0x0002: (隔离开关位置, 遥信, 1), 0x4001: (A相电压, 遥测, 0.1), 0x4002: (B相电压, 遥测, 0.1), 0x4003: (C相电压, 遥测, 0.1), 0x6001: (断路器遥控, 遥控, 1), } def map_to_point(ioa: int, raw_value, type_id: int): 将IOA和原始值映射到点表 if ioa not in point_table: return {ioa: hex(ioa), name: 未知点, value: raw_value} name, ptype, factor point_table[ioa] if ptype 遥测: value raw_value * factor elif ptype 遥信: value 合 if raw_value else 分 else: value raw_value return {ioa: hex(ioa), name: name, type: ptype, value: value}点表映射的关键是确认IOA的字节序。103规约里IOA是低字节在前高字节在后所以解析时用asdu[offset] | (asdu[offset1] 8)。如果你从厂家拿到的点表里IOA是十六进制字符串注意转换。另外遥测的系数可能不是简单的乘法有些装置上送的是归一化值需要先转成浮点数再乘量程。南瑞继保的装置在通用分类数据里还会用数据集索引来传输保护定值那套映射更复杂需要单独处理。5.3 实时监听与自动应答用socket实现一个简易主站如果你想实时监听装置上送的数据或者模拟主站发总召唤可以用Python的socket库直接连接装置。下面代码演示了建立TCP连接、发送总召唤、接收并解析响应的最小实现。import socket import time def simple_master(ip: str, port: int 2404, link_addr: int 1): 简易网络103主站连接装置发送总召唤打印解析结果 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) try: sock.connect((ip, port)) print(f已连接 {ip}:{port}) # 发送总召唤 frame build_general_interrogation(link_addr) sock.send(frame) print(f发送总召唤: {frame.hex()}) # 接收响应 buffer b start_time time.time() while time.time() - start_time 10: try: data sock.recv(4096) if not data: break buffer data # 按帧解析 while len(buffer) 5: if buffer[0] 0x10: if len(buffer) 5: ftype, fields parse_ft12_frame(buffer[:5]) print(f固定帧: {fields}) buffer buffer[5:] else: break elif buffer[0] 0x68: if len(buffer) 2: L buffer[1] total 4 L 2 if len(buffer) total: ftype, fields parse_ft12_frame(buffer[:total]) if ftype variable: asdu fields[asdu] type_id asdu[0] if type_id in (0x03, 0x04): points parse_measure_asdu(asdu) for p in points: mapped map_to_point(p[ioa], p[value], type_id) print(f遥测: {mapped}) elif type_id 0x08: print(总召唤结束) buffer buffer[total:] else: break else: break else: buffer buffer[1:] except socket.timeout: continue finally: sock.close() # 使用示例请替换为实际IP # simple_master(192.168.1.100, 2404, 1)这个简易主站没有处理链路层确认和FCB翻转只适合在实验室环境快速验证。实际主站需要维护链路状态机收到固定帧请求链路状态时要回确认收到可变帧时要回用户数据确认。另外总召唤结束后要发链路层确认。如果你用这个代码连现场装置可能会因为缺少确认而导致装置断开连接。建议先用它来解析抓包文件确认解析逻辑正确后再逐步完善链路层。5.4 解析结果验证用已知报文做单元测试写完解析器后一定要用已知正确的报文做测试。你可以从现场抓一个总召唤的完整交互把每个帧的十六进制保存下来然后写单元测试验证解析结果。比如下面这个测试用例用一个模拟的遥测ASDU验证解析函数。def test_parse_measure_asdu(): # 模拟一个类型标识0x03的遥测ASDU类型0x03, VSQ 0x81, COT 0x14, 公共地址0xFF, IOA 0x4001, 值0x1234 asdu bytes([0x03, 0x81, 0x14, 0xFF, 0x01, 0x40, 0x34, 0x12]) points parse_measure_asdu(asdu) assert len(points) 1 assert points[0][ioa] 0x4001 assert points[0][raw] 0x1234 # 归一化值 0x1234 / 32768 0.142... assert abs(points[0][value] - 0x1234/32768.0) 1e-6 print(测试通过) test_parse_measure_asdu()这个测试用例覆盖了IOA字节序和归一化值转换。实际项目中我会把现场抓到的典型报文都做成测试用例每次修改解析代码后跑一遍确保不会引入回归错误。南瑞继保的装置在不同版本间可能有细微差异比如时标格式、公共地址默认值等用测试用例固化下来能省很多排查时间。5.5 性能与稳定性长时间运行时的内存和超时处理如果你要把解析器做成长期运行的服务需要注意内存管理和超时处理。TCP流重组时缓冲区不能无限增长要设置最大长度超过就丢弃旧数据。socket接收要设置超时避免阻塞。另外链路层要定期发送链路状态请求维持连接。我一般会用一个独立的线程做链路保持每20秒发一次固定帧请求链路状态收到确认后继续。如果连续3次没收到确认就断开重连。解析线程和链路保持线程之间用队列传递数据避免锁竞争。还有一点南瑞继保的装置在TCP连接断开后可能需要几秒钟才能重新接受连接所以重连逻辑里要加退避不要立即重试。6. 从能解析到能诊断用报文时间序列定位通信中断的根因当你已经能稳定解析网络103报文后下一步就是用它来诊断那些“时好时坏”的通信故障。这类故障最让人头疼因为现场复现困难后台日志往往只记录“通信中断”没有细节。我的做法是在后台和装置之间的交换机上做端口镜像用tcpdump或Wireshark长时间抓包然后把抓包文件丢给解析器生成一份带时间戳的报文流水。流水里记录每个帧的发送方、帧类型、控制域功能码、ASDU类型、传送原因、IOA和值。然后按时间排序看通信中断前最后几个帧是什么。常见的模式有三种第一种后台发总召唤后装置回了几个遥测帧就没了既没有总召唤结束也没有链路层确认。这通常是装置CPU负载过高或者某个遥测点采集阻塞导致上送卡死。第二种后台和装置之间反复出现“请求链路状态”和“链路忙”说明装置链路层缓冲区满了可能是后台发送频率太高或者装置处理不过来。第三种TCP连接被RST复位抓包能看到RST标志。这通常是中间网络设备比如防火墙或网关超时断开了空闲连接。针对第一种可以在装置侧查看CPU负载和任务状态第二种降低后台的总召唤频率或增加链路层确认的及时性第三种在后台和装置上启用TCP Keepalive或者让后台定期发链路状态请求维持连接。我自己的习惯是每次现场调试完把抓包文件和解析出的流水存档标注日期和装置版本。下次再遇到类似问题先翻历史记录往往能快速定位。这个方案值不值得做如果你负责的变电站有几十台南瑞继保装置每次通信故障都靠猜和试那花两天写个解析器绝对划算。如果你只是偶尔调试一台装置用Wireshark手动看也够。希望帮到你。本文还有配套的精品资源点击获取