
简介面向工业控制系统与电力自动化领域的工程师、安全测试人员及协议研究者这份PDF文档围绕IEC60870-5-104协议系统讲解工控协议测试床的架构设计与模拟实战方法。内容覆盖协议数据单元APDU、应用服务数据单元ASDU等核心概念以及测试床软硬件选型、网络拓扑配置、报文模拟与解析、测试用例设计、故障注入与异常处理、性能优化与安全防护等完整链路既能辅助入门者理解协议机制也能支持中高级技术人员在系统部署前开展功能、兼容性与安全性验证。资源为单个PDF文件压缩包大小约4.37MB排版清晰、目录可跳转图表与代码显示正常已有73人学习使用。借助该文档读者可快速搭建模拟环境掌握报文构造、解析流程及故障注入的实操方法为电力自动化系统的稳定运行与信息安全防护提供参考。1. 为什么需要一张IEC60870-5-104测试床从一次变电站联调事故说起IEC60870-5-104测试床不是一套买回来就能用的盒子而是一套把协议模拟、故障注入、性能验证串起来的工控协议模拟实战环境。我做过电力自动化项目遇到过不止一次现场联调翻车主站侧用厂家自带模拟器发遥测从站解析出来全是乱码双方各自拿着抓包截图吵了三天最后发现是控制域的序号处理方式不一致。这种问题在真实变电站里排查成本极高。于是我搭了一套IEC104测试床把主站、从站、故障注入点全收进实验室让协议行为先在测试床上完整跑一遍再去现场。这套思路适合三类人要验证协议栈实现是否规范的开发人员要确认RTU或IED能扛住异常报文的测试工程师以及想研究协议攻击面与防御策略的安全研究人员。2. IEC60870-5-104协议核心APDU字段与传输规则的底层逻辑2.1 分层结构与应用层职责在动手搭测试床之前得先把协议的分层搞清楚因为后面所有模拟器和解析脚本都是围绕应用层APDU展开的。IEC60870-5-104把IEC60870-5-101的应用层服务映射到了TCP/IP之上底层走标准以太网和TCP协议。TCP面向连接、可靠传输的特性解决了101协议在串口链路上丢包重传的麻烦但同时也引入了序号、确认和重传这些需要应用层配合处理的机制。物理层就是把比特流放到网线上这个不用我们操心选好网线和交换机即可。数据链路层处理以太网帧的封装和差错检测通常也由网卡和交换机自动完成。真正需要关注的是传输层和应用层传输层用TCP承载默认端口2404连接建立走三次握手断开走四次挥手应用层定义了一组ASDU应用服务数据单元对应遥测、遥信、遥控、遥调这些电力自动化里的基本动作。理解这条链路就能明白测试床为什么要把服务器和工控机放在同一个二层网络里也更容易理解后面的防火墙配置为什么不能只放开一个端口。2.2 APDU字段拆解启动字符、长度域、控制域与ASDUAPDU是IEC104应用层的基本数据单元所有报文都是APDU的组装与拆解。一个完整的APDU由四段构成启动字符固定为0x68用来在字节流里标记APDU的起点抓包时第一眼看它不是0x68基本可以断定报文错位或不是IEC104长度域占一个字节表示从长度域自身到报文末尾的字节数注意它不包含启动字符0x68但包含长度域自己这个细节容易踩坑。控制域在我的测试床实现里占四个字节承载发送序号和接收序号序号机制是为了配合TCP做传输确认防止报文丢失或重复。IEC104对序号有严格的处理规则发送方每发一帧数据报文序号加2不是加1因为序号的最低位被用作其他控制标志很多联调问题就出在序号跳跃规则上。ASDU部分是业务核心由类型标识、可变结构限定词VSQ、传送原因COT、公共地址和信息对象组成。类型标识决定报文是遥测、遥信还是遥控VSQ的低7位表示信息对象个数COT则告诉接收方这是周期刷新、突发上报还是激活命令。这张表可以直接拿去核对报文格式字段名长度字节含义启动字符1固定0x68长度域1从长度域自身到报文末尾的字节数控制域4序号、确认与控制标志ASDU类型标识1如0x09遥测、0x01遥信可变结构限定词1信息对象个数与排列方式传送原因1周期、突发、激活等公共地址2ASDU归属标识信息对象可变具体业务数据2.3 解析APDU头部一段可复用的Python代码理解字段后先给出一个解析APDU头部的Python脚本它可以在后续所有测试用例里作为公共函数复用。这个脚本只负责把头部信息提取出来不涉及具体信息对象的解码信息对象解码放到第4章统一处理。import struct def parse_apdu_header(apdu: bytes) - dict: # 校验启动字符IEC104固定以0x68开头 if len(apdu) 6: raise ValueError(APDU长度不足无法解析头部) if apdu[0] ! 0x68: raise ValueError(f无效启动字符: 0x{apdu[0]:02x}) # 长度域表示从长度域自身到报文末尾的字节数不含启动字符 length apdu[1] if len(apdu) ! length 2: raise ValueError(f长度域为{length}实际总长度{len(apdu)}报文不完整) # 控制域四字节这里按简化方式取前两字节为发送序号后两字节为接收序号 control_field apdu[2:6] send_seq struct.unpack(H, control_field[:2])[0] recv_seq struct.unpack(H, control_field[2:])[0] # ASDU部分从第6字节开始 asdu apdu[6:] if len(asdu) 5: raise ValueError(ASDU长度不足) type_id asdu[0] vsq asdu[1] cot asdu[2] asdu_address struct.unpack(H, asdu[3:5])[0] return { length: length, send_seq: send_seq, recv_seq: recv_seq, type_id: type_id, vsq: vsq, cot: cot, asdu_address: asdu_address, info_object_start: 6 5, } apdu_example bytes([ 0x68, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x09, 0x01, 0x03, 0x00, 0x01, 0x00, 0x01, 0x41, 0xF4, 0x00, 0x00 ]) result parse_apdu_header(apdu_example) print(result)这段代码有三个关键点。第一struct.unpack(H, ...)里的表示大端序IEC104的序号和公共地址都是高位在前如果按本机默认的小端序解析序号会完全颠倒。第二长度域的校验逻辑是len(apdu) ! length 2因为启动字符0x68占一个字节不计入length而length本身又包含了长度域这字节所以整个APDU的大小是length加2。第三返回值里的info_object_start记录了信息对象在原始报文中的起始下标后续解析具体测量值或开关状态时直接从该位置切片即可。参数apdu是原始字节串调用方需要确保传入的是完整报文TCP收包时要按长度循环拼接。3. 测试床架构设计角色划分、网络拓扑与软件栈落地3.1 总体架构硬件层、软件层与网络层的分工测试床的架构我倾向于做成三层硬件层提供计算和仿真载体软件层承载协议模拟、测试管理和数据分析网络层负责把设备连起来并制造各种网络条件。这三层不是各自独立软件层跑在硬件层之上网络层的变化会直接影响软件层观察到的行为。硬件层里服务器负责跑主站模拟器和测试管理模块工控机扮演RTU或IED这类从站设备。我建议不要用一台机器同时跑主站和从站因为那样会让网络层失去意义也就没法验证跨设备的时序和异常。服务器选普通的x86机器即可工控机则要稳定可靠毕竟它要长期开在那里跑模拟任务研华IPC-610L这类带扩展槽的箱子比较合适。软件层是测试床的心脏协议模拟器负责生成和解析IEC104报文测试管理模块负责用例编排与执行数据分析模块负责把抓到的报文和测试结果做统计展示。这个三层架构里还有一个容易被忽略的点测试管理模块最好独立部署不要和协议模拟器跑在同一个进程里。因为跑压力测试时模拟器会拼命发报文管理模块如果挤在同一进程数据库写入和用例调度会互相争抢CPU导致测试结果失真。3.2 网络拓扑选择星型为什么是测试床的首选设计网络拓扑时常见选项有星型、总线型和环型。总线型布线简单成本低但出了故障不好隔离总线上一个节点异常可能拖累整张网环型可靠性高但扩展困难环上插入新设备要中断链路星型以一台交换机为核心每个设备一根线上连故障隔离非常干净拔掉某个模拟节点的网线不影响其他节点这对测试床特别重要因为我们要经常单独切断某个从站来观察主站的重连行为。我最终选了星型拓扑核心是一台24口千兆交换机服务器和工控机都直连到交换机上。这种结构在模拟多主站、多从站并发场景时非常灵活需要新增节点时只需把新设备插到交换机空余端口再在测试管理模块里登记IP即可。如果测试床规模再扩大可以叠一台交换机做级联或者用VLAN把管理流量和数据流量分开。另外如果测试床需要模拟跨区通信我建议在网络层加一台路由器把数据子网分成两个网段一段放主站、一段放从站这样模拟跨区调度比单交换机拓扑更贴近真实部署。3.3 软件栈落地lib60870编译部署与MySQL初始化软件侧我以lib60870作为协议模拟的核心它是开源的C语言库同时有Python绑定我用它快速构造主站和从站。部署步骤按下面代码走环境是Ubuntu Server。# 更新软件源并安装编译依赖 sudo apt update sudo apt install -y cmake gcc make libssl-dev # 克隆lib60870仓库并进入目录 git clone https://github.com/mz-automation/lib60870.git cd lib60870 # 编译安装 mkdir build cd build cmake .. make sudo make install # 编译Python绑定在仓库的python子目录下执行 cd ../python python setup.py build_ext --inplace这里有个容易被忽略的细节libssl-dev一定不能省lib60870的部分示例依赖OpenSSL做TLS通道模拟缺了这个依赖cmake ..阶段会报找不到SSL头文件的错误。Python绑定编译时build_ext --inplace会把扩展模块生成到当前目录import时必须保证工作目录在lib60870/python下否则解释器找不到lib60870的so文件。数据库我用MySQL来存测试记录和用例配置。初始化脚本如下sudo apt install -y mysql-server sudo mysql_secure_installation sudo mysql EOF CREATE DATABASE iec60870_test CHARACTER SET utf8mb4; CREATE USER test_userlocalhost IDENTIFIED BY password; GRANT ALL PRIVILEGES ON iec60870_test.* TO test_userlocalhost; FLUSH PRIVILEGES; EOF这段代码里的utf8mb4是刻意设置的因为测试记录里可能包含中文备注和特殊符号默认的utf8字符集会丢数据。用户名和密码建议单独建不要用root跑业务库。后面测试管理模块连接数据库时权限收窄到iec60870_test一个库可以避免误删其他库。3.4 网络环境配置IP规划、子网划分与防火墙IP规划直接影响抓包分析时的心情。我习惯给网络分三个子网管理网段、数据网段和外接网段。管理网段给服务器和工控机的带外管理口跑SSH和数据库访问数据网段给IEC104协议通信也就是2404端口所在链路外接网段留给以后接入真实设备或第三方模拟器。一个具体可用的规划是这样管理子网192.168.1.0/24服务器192.168.1.100工控机192.168.1.101到192.168.1.110数据子网192.168.2.0/24主站模拟器固定192.168.2.10从站模拟器从192.168.2.21开始递增分配。为什么要分开因为抓包时如果管理流量和IEC104流量混在同一网段Wireshark里全是SSH和数据库连接过滤麻烦不说跑负载测试时管理流量还会抢占带宽污染性能数据。防火墙配置用UFW重点保证两件事一是SSH管理通道不被断开二是IEC104的2404端口只对数据网段开放。命令如下# 允许管理网段的SSH和MySQL访问 sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp sudo ufw allow from 192.168.1.0/24 to any port 3306 proto tcp # 允许数据网段的IEC104协议端口2404 sudo ufw allow from 192.168.2.0/24 to any port 2404 proto tcp # 默认拒绝其他入站流量 sudo ufw default deny incoming sudo ufw enable sudo ufw status verbose强调一下这里的边界我故意没有对外部网段放行任何端口模拟器之间的通信全部限制在数据子网内。这样即使某台工控机被攻破攻击者也无法从数据网段直接跳到管理网段这是工控测试环境最基本的安全隔离。在后续安全测试里这个隔离边界就是渗透测试要验证的第一道防线。4. 协议报文模拟与解析实战遥测、遥信、遥控三类报文的构造4.1 模拟工具选型lib60870与OpenMUC怎么选IEC104报文的模拟工具开源领域最常见的是lib60870和OpenMUC。lib60870是C语言库性能好适合自己写脚本精确控制每一个字段也适合做故障注入因为你能直接篡改报文内容OpenMUC是Java框架带Web界面配置简单上手快但遇到需要构造畸形报文的场景就不如lib60870灵活。我的建议是日常功能测试用OpenMUC省时间协议兼容性测试和安全测试必须落到lib60870上。原因很简单安全测试需要精确构造非标准报文比如故意把长度域写错、把序号跳成奇数这类需求对工具的可控性要求非常高。下面我用lib60870的Python绑定演示三类典型报文的生成和解析。4.2 生成三类典型报文遥测、遥信、遥控先看一个完整的主站模拟器脚本它初始化一个Server对象然后循环发送遥测、遥信、遥控三类报文。import lib60870 import time server lib60870.Server() server.start() def send_telemetry(): # 遥测类型标识0x09带浮点测量值 asdu lib60870.ASDU(lib60870.COT_PERIODIC, 1, 1) io lib60870.IEC60870_5_104.IO_Type.M_ME_NC_1 value 123.45 info_object lib60870.InfoObject(1, io, value) asdu.add_info_object(info_object) server.send_asdu(asdu) print(遥测报文已发送值:, value) def send_teleindication(): # 遥信类型标识0x01单点布尔开关状态 asdu lib60870.ASDU(lib60870.COT_SPONTANEOUS, 1, 1) io lib60870.IEC60870_5_104.IO_Type.M_SP_NA_1 value True info_object lib60870.InfoObject(1, io, value) asdu.add_info_object(info_object) server.send_asdu(asdu) print(遥信报文已发送状态:, value) def send_remote_control(): # 遥控类型标识0x2D单点命令 asdu lib60870.ASDU(lib60870.COT_ACTIVATION, 1, 1) io lib60870.IEC60870_5_104.IO_Type.C_SC_NA_1 value True info_object lib60870.InfoObject(1, io, value) asdu.add_info_object(info_object) server.send_asdu(asdu) print(遥控报文已发送命令:, value) while True: send_telemetry() time.sleep(5) send_teleindication() time.sleep(5) send_remote_control() time.sleep(5)这段脚本里要解释的参数比较多。lib60870.ASDU(cot, ca, cot_len)的第一个参数是传送原因COT_PERIODIC表示周期上报COT_SPONTANEOUS表示突发变位COT_ACTIVATION表示激活命令三种COT会影响从站对报文的处理优先级第二个参数1是公共地址模拟环境里一台主站对多台从站时每个从站要有独立地址这里先统一用1第三个参数是COT的长度IEC104里传送原因默认占一个字节。InfoObject的第一个参数是信息对象地址模拟开关柜里不同的测点编号IO_Type选型决定了报文里信息对象的结构比如M_ME_NC_1对应浮点测量值传输时按四字节浮点编码M_SP_NA_1对应单点遥信只占一个字节。实际项目中IO地址要和现场点表一一对应测试床阶段可以随便编但建议保持一个递增规则方便后面写断言。4.3 解析流程与代码实现报文解析我按四步走先收齐TCP流里的完整APDU再校验启动字符和长度域然后提取控制域序号最后按类型标识分发到具体的信息对象解析器。第三步和第四步之间还要做一项工作——更新接收序号这个序号必须回给发送方否则对方会认为链路异常。import struct INFO_OBJECT_LEN { 0x01: 1, # 单点遥信 0x02: 1, # 双点遥信 0x09: 4, # 遥测浮点值 0x0D: 5, # 遥测浮点值品质描述词 0x2D: 1, # 单点遥控 } def parse_info_objects(type_id: int, data: bytes): if type_id in (0x09, 0x0D): value struct.unpack(f, data[:4])[0] return {type: telemetry, value: round(value, 2)} elif type_id in (0x01, 0x02): return {type: teleindication, value: bool(data[0] 0x01)} elif type_id 0x2D: return {type: remote_control, command: bool(data[0] 0x01)} else: return {type: unknown, raw: data.hex()} def parse_apdu(apdu: bytes) - dict: if apdu[0] ! 0x68: raise ValueError(f启动字符错误: 0x{apdu[0]:02x}) length apdu[1] if len(apdu) ! length 2: raise ValueError(f长度域{length}与实际报文长度不符) if length 8: raise ValueError(APDU过短无有效ASDU) # 控制域按简化方式解析序号 send_seq struct.unpack(H, apdu[2:4])[0] recv_seq struct.unpack(H, apdu[4:6])[0] asdu_start 6 type_id apdu[asdu_start] vsq apdu[asdu_start 1] cot apdu[asdu_start 2] common_addr struct.unpack(H, apdu[asdu_start 3: asdu_start 5])[0] # 信息对象数量取VSQ低7位 object_count vsq 0x7F obj_start asdu_start 5 results [] count 0 while count object_count and obj_start 2 len(apdu): io_addr struct.unpack(H, apdu[obj_start: obj_start 2])[0] data_len INFO_OBJECT_LEN.get(type_id, 1) data_start obj_start 2 results.append({ io_addr: io_addr, **parse_info_objects(type_id, apdu[data_start: data_start data_len]) }) obj_start data_start data_len count 1 return { send_seq: send_seq, recv_seq: recv_seq, type_id: type_id, vsq: vsq, cot: cot, common_addr: common_addr, info_objects: results, } apdu_example bytes([ 0x68, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x09, 0x01, 0x03, 0x00, 0x01, 0x00, 0x01, 0x41, 0xF4, 0x00, 0x00 ]) print(parse_apdu(apdu_example))这段解析代码里有几个重要的取舍。第一前缀表示大端序这是IEC104的硬性规定公共地址和信息对象地址都是高字节在前写成小端会导致地址错乱。第二object_count vsq 0x7F取可变结构限定词的低7位最高位表示后续信息对象地址是否连续排列测试床阶段我全部按不连续排列处理每个信息对象都带完整地址。第三INFO_OBJECT_LEN这个字典必须按类型标识维护好不同类型的信息对象长度不同比如单点遥信占1字节遥测浮点值占4字节带品质描述词则占5字节查错表会导致解析错位。报文解析做扎实之后功能测试、兼容性测试、安全测试就都有了数据基础。功能测试拿模拟报文验证主站能否正确采集兼容性测试用不同厂家的报文格式对比解析结果看接收方是否严格遵循标准安全测试则伪造异常报文观察系统的容错表现。这一步是整个测试床数据链路的终点也是后续所有测试用例的输入来源。5. 避坑指南IEC104测试床搭建中的常见问题与排查5.1 启动字符0x68校验通过长度域却对不上现象抓包看到报文以0x68开头解析脚本却报长度不匹配有时还连带报结构体解包失败。 原因长度域表示的是从长度域自身到APDU末尾的字节数不包含起始字符0x68。很多人下意识认为长度域是整个APDU的总长度会把校验写成len(apdu) length结果永远差一个字节另一种情况是TCP粘包一次recv收到多条APDU解析脚本把第一条的长度域误当作整段数据的长度。 解决校验条件改为len(apdu) length 2同时接收逻辑按循环处理每解析完一条APDU就从缓冲区切掉对应字节数直到缓冲区剩余不足一条。我在测试床里专门写了一个缓冲区拆包类来处理粘包先读前两个字节判断长度再决定是否等待更多数据。5.2 发送序号与接收序号不匹配连接被对端强行关闭现象主站和从站握手正常但发送几条报文后对端直接发来ACK复位命令连接被关闭日志里出现序号异常。 原因IEC104的序号规则是每发送一帧加2且序号只在0到32767之间循环。常见错误有两个一是初始序号没有按规范从0开始二是把U帧比如STARTDT激活帧之间的计数也写进了I帧序列导致序号跳变异常。另一个隐蔽场景对端在收到报文后期望的是发送方上一次报文的序号加2如果发送方同时跑着多个发送线程且没有加锁序号就会竞争错乱。 解决所有发送操作收敛到单一发送队列由同一个线程负责累加序号启动时先发送STARTDT激活确认等对端确认后才开始发送I帧避免链路未激活就发数据。测试管理模块里我把序号计数器单独抽成了一个类所有模拟器实例共享同一套序号分配逻辑从源头杜绝多线程竞争。5.3 防火墙规则放行不完整TCP 2404端口始终不可达现象模拟器启动正常服务器上ss -lnt能看到2404端口监听但从工控机telnet 2404却超时Wireshark里只能看到SYN包没有SYN-ACK回复。 原因这是典型的防火墙规则遗漏。很多人只放行了TCP 2404入站但忘了IEC104连接建立后数据方向是双向的从站向主站发送的报文是源端口2404、目的端口为临时高端口的流量如果默认入站策略是拒绝这些回程数据会被丢弃。 解决数据子网内的设备之间互相放行管理网段只保留SSH和MySQL端口。配置命令参考第3章但要在后端补一行sudo ufw allow from 192.168.2.0/24 to any这样数据网段内任意端口互通2404回程流量自然不会被拦。提示UFW规则顺序很关键default deny incoming如果出现在放行规则之前合法流量会被先匹配拒绝。配置完用sudo ufw status verbose逐条核对规则顺序有问题时把放行规则插到deny规则之前再重载。5.4 大端字节序搞混浮点遥测值解析成天文数字现象同样的遥测报文用Wireshark解析是123.45用自己写的脚本解析出来的浮点数接近1.7e38或者变成一个很大的负数。 原因IEC104所有多字节字段都按大端序传输包括浮点数。Python的struct.unpack如果不指定字节序默认按本机原生序在x86机器上恰好是小端直接解浮点会把四个字节的字节顺序搞反得到完全不同的值。 解决所有解包都显式写前缀比如struct.unpack(f, data)、struct.unpack(H, data)。这个坑在写解析脚本时特别容易犯因为脚本不报错结果就是一连串诡异数字排查起来比直接报错更费时间。我的习惯是解析器里强制统一用f、H、I不写不带前缀的struct调用测试用例里也专门加了一条“用Wireshark对比解析结果”的回归校验。5.5 测试用例之间互相依赖故障定位越查越乱现象单独执行某个遥控测试用例时通过但按用例集顺序执行时这个用例偶发失败重跑却又是好的。 原因测试用例没有做到独立。最常见的是前一个用例修改了模拟器的公共地址或信息对象地址后一个用例沿用旧配置或者某个用例里没有清理TCP连接连接对象泄漏导致下一个用例的握手流程被干扰。 解决每个用例执行前强制恢复默认配置重建TCP连接用例结束无论通过还是失败都主动关闭连接并释放资源。测试管理模块里写一个清理钩子每次用例跑完统一重置模拟器状态和连接池。另外用例断言里最好加上前置状态检查比如遥控用例先读一次遥信确认当前状态和预期一致再发遥控命令把用例间的隐性耦合显式化。最后建议把报文收发日志默认打开每条报文关联用例ID出问题时按用例ID过滤日志比逐个抓包分析快得多。6. 故障注入与性能验证让测试床真正暴露问题6.1 故障注入的三种操作路径测试床搭好之后真正的价值在于故障注入。我常用的注入路径有三条网络层用tc netem模拟延迟和丢包设备层直接停掉某个模拟器进程来模拟RTU离线协议层用lib60870构造畸形报文比如把长度域改成超大值、把传送原因改成非法枚举值。下面是网络层注入的典型命令# 在数据网卡上增加120ms延迟和5%丢包 sudo tc qdisc add dev eth1 root netem delay 120ms loss 5% # 恢复默认状态清掉所有队列规则 sudo tc qdisc del dev eth1 root这三条路径要组合用单独注一种故障往往暴露不出真实问题。我见过最典型的情况只模拟网络延迟主站的重传定时器没触发性能指标一切正常但把延迟和报文篡改叠加在一起重传和序号确认的交互问题立刻暴露。协议层注入畸形报文时记得用lib60870底层API直接构造ASDU不要走高层的send_asdu封装否则库会替你纠正字段注入效果就没了。6.2 性能验证指标与边界验证时我重点看四个指标响应时间、吞吐量、并发连接数、资源利用率。响应时间取P95而非平均值因为平均值会被极端值拉偏吞吐量用每秒处理的ASDU数量衡量而不是字节数因为不同报文类型的有效载荷差异很大并发连接数用多线程脚本逐步加压观察连接建立率和失败率资源利用率关注CPU和内存如果模拟器CPU先跑到100%而不是网络先达到瓶颈说明协议栈实现有性能问题。这几项验证里最容易出问题的是并发连接数。IEC104主站通常一个连接对应一台从站模拟上千台从站时服务器文件描述符上限要提前调大ulimit -n默认值往往不够。我记得有次压测到八百连接时进程直接崩溃排查半天是文件描述符耗尽改配置重测就稳定了。从那以后我每次搭测试床第一件事就是把ulimit -n和内核的net.core.somaxconn调好再跑任何压测脚本这个习惯帮我省掉了无数次无意义的排查。希望这些踩坑记录能帮你在搭IEC60870-5-104测试床时少走几步弯路。本文还有配套的精品资源点击获取