在工业上位机开发这个圈子里C#要跟PLC通信OPC几乎是绕不开的一条路。我做产线数据采集和MES对接也有几年了早期也写过西门子S7协议的直连驱动、三菱MC协议的串口帧后面真正稳定跑起来的基本都收敛到了OPC这套方案上。这篇文章不堆概念直接把我一套能用的C#与PLC通信的OPC连接程序源码的整体设计思路、通用性处理方式和关键代码拆开讲顺便把DCOM那点破事、采集性能瓶颈、标签映射的坑一次性说清楚。不管你是刚接第一个上位机项目的新手还是被PLC品牌协议折腾得想换方案的老手这篇都能给你一个可以直接改了就用的参照物。1. 为什么C#与PLC通信要选OPC1.1 直连PLC协议的致命问题先说说我早期踩过的坑。当时一个项目要同时采集西门子S7-1200和一台三菱FX5U的数据我分别用了S7comm和MC协议各写了一套驱动。听起来没什么但后面现场加了一台欧姆龙CP1H我只能再写一套FINS帧解析再后来设备升级S7-1200换成了S7-1500点位表全变了我的解析代码也伤筋动骨。这个模式的问题在于每换一种PLC品牌、每升级一个固件版本你都要重新研究协议文档、重写底层帧而产线设备根本不可能只用一种PLC。更难受的是很多PLC的私有协议根本没有公开文档你只能抓包逆向或者找厂家要SDK。就算要到了DLL还分32位和64位跟上位机进程的位数对不上又是一堆麻烦。所以现场做数据采集最怕的就是驱动地狱。1.2 OPC就是那个翻译官OPCOLE for Process Control解决的就是这个生态碎片化问题。它定义了一套统一的接口规范PLC厂家或者第三方厂商把各自设备的协议封装成OPC服务器你上位机里的OPC客户端只跟这个服务器对话不需要关心对端是西门子、三菱还是施耐德。打个比方直连方案就像你出门必须随身带各种充电线OPC方案则是大家统一用USB-C口中间那个转接头由设备厂商负责。这套机制带来的好处非常直接一是点位管理可以脱离代码PLC里的DB块、寄存器地址全部映射成OPC的标签Tag加一个点通常只需要在OPC服务器里配置一下不用重新编译上位机二是设备可以热切换西门子的OPC服务器坏了换个支持同一协议族的服务器软件客户端代码基本不用动三是数据带质量戳读到的每个值都附带质量、时间戳上位机可以判断数据是否可信不会把PLC停机时的脏数据当成正常值采进数据库。1.3 什么场景最适合用OPC不是所有C#和PLC通信都要上OPC。我的判断标准很简单如果只是单台PLC、几个点位写个串口或TCP直连完全够用别自己给自己找DCOM的麻烦但只要是产线级项目多台不同品牌设备、几十上百个点位、要跟MES或SCADA对接直接上OPC。尤其是汽车零部件行业的拧紧机、压装机这类设备厂家自带的OPC服务器基本都是标配比如现场常见的Atlas Copco Power Focus 6000拧紧控制器扭矩和角度结果就是通过OPC通道交给上位机做追溯的。这类场景用直连协议去读累死还读不全。2. OPC体系架构与选型思路2.1 OPC DA与OPC UA到底差在哪现在聊OPC必须先把DA和UA这两代分清楚。OPC DA是基于COM/DCOM的老标准最大的特点是Windows绑定、局域网为主、安全配置极其反人类。OPC UA则是完全重写的第二代协议传输层走TCP默认端口4840支持加密和证书认证跨平台而且自描述能力很强客户端可以浏览服务器的地址空间不用预先知道点位结构。我用一张表把核心差异列出来方便你按项目选型对比项OPC DAOPC UA底层技术COM/DCOMTCP/HTTPS二进制或JSON操作系统Windows全平台防火墙友好度差动态端口DCOM好固定端口4840安全性基本靠DCOM权限很弱证书加密用户认证地址空间扁平 Tag 集合对象化、可浏览的结构跨网段通信很难轻松老设备兼容大量老PLC/仪表只支持DA新型设备逐步原生UA选型建议就一句话新项目优先UA老设备网段只能走DA就认命用DA但最好在代码层把这两种客户端抽象成同一个接口给将来切换留后路。我后文给的源码就是这么设计的。2.2 常见的OPC服务器软件怎么挑C#客户端不生产数据数据都在OPC服务器那边。所以项目能不能跑通一半取决于服务器软件选得对不对。主流的选择大致分三类第一类是各PLC厂家的原生服务器比如西门子的SIMATIC NET、三菱的MX OPC Server、欧姆龙的FINS OPC Server。它们的优势是跟自家PLC匹配度最高点位类型、数据块映射都做得最细但通常要单独购买授权而且一个服务器只认自家设备。第二类是第三方多协议网关最有名的就是Kepware的KEPServerEX现在叫PTC Kepware。它一个软件能同时接几十种PLC、仪表、机器人控制器然后在同一套命名空间里暴露OPC DA和UA两种服务。我的产线项目有一半用的它省心是最大的优点缺点是授权价格不低点位数量也按档位卖。第三类是用来开发和测试的仿真服务器。身边很多朋友问免费的OPC服务器有哪些如果你只是想练手C#客户端完全不用买商业授权。Prosys的OPC UA Simulation Server免费版就能生成随机变化的模拟数据OPC Foundation官方也有UA .NET Sample ServerKeppware甚至有模拟设备驱动。我建议新手第一步就跑这种仿真服务器把客户端代码验证完再去接真实PLC。2.3 一次选型失误让我学会的事这里插一个真实翻车案例。之前有个项目客户指定要用某品牌工控机自带的OPC服务器我图省事就没仔细核对协议版本结果到现场发现那台设备的OPC服务器只支持DCOM而我写的客户端是UA。DCOM跨网段访问需要开135端口加一堆动态端口客户的防火墙策略根本不放行最后只能让IT特批网段又折腾了两天。从那以后我所有项目的选型评审都多了一条铁律先确认OPC服务器的协议版本和访问方式再决定客户端技术栈如果可能让服务器同时启用DA和UA两个端点客户端优先UA。3. 源码整体设计与通用性实现3.1 把客户端抽象成接口别跟具体协议绑死我见过很多写死在OPC DA上的项目代码类名都叫OpcDaHelper里面全是COM互操作的代码后来要切UA只能推倒重写。这就是典型的没有做抽象。我的做法是先定义一个通信服务接口把客户端该有的能力定死public interface IPlcOpcClient : IDisposable { bool IsConnected { get; } void Connect(); void Disconnect(); object ReadTag(string tagPath); void WriteTag(string tagPath, object value); void Subscribe(string[] tagPaths, ActionOpcTagValue[] callback, int samplingIntervalMs); Dictionarystring, object ReadGroup(string[] tagPaths); event EventHandlerbool ConnectionStateChanged; }这个接口是整个源码的地基。Connect只管建立会话ReadTag按标签路径读值Subscribe做订阅推送ReadGroup做大批量读取。每个方法的参数都用string和设备无关的值对象不暴露OPC DA的ItemIdentifyer也不暴露OPC UA的NodeId。这样上层业务代码只认识标签路径和数值协议细节全部被关在实现类里。接口定完我再定义两个数据对象一个叫OpcTagValue承载值、质量、时间戳一个叫OpcTagConfig承载标签路径、数据类型、读写权限等配置项。3.2 配置驱动是通用性的核心真正让我在不同项目间复用代码的不是接口而是配置驱动的设计。标签定义、服务器地址、订阅频率全部外置到JSON配置文件程序启动时加载并构建标签映射表。这样一来换一个项目只需要改配置文件核心DLL一行不动。我习惯的配置结构长这样{ opcType: ua, endpoint: opc.tcp://192.168.1.20:4840, useSecurity: false, tags: [ { path: PLC1.DB10.RealValue, dataType: Real, direction: Read }, { path: PLC1.MW100, dataType: Int16, direction: ReadWrite } ], subscription: { enabled: true, samplingIntervalMs: 100, publishIntervalMs: 500 } }有人可能觉得标签路径直接写在代码里不是更省事吗等现场点位表变了你就知道配置文件的好处了。设备厂商给的点位表往往几百行你不可能在代码里硬编码也不可能每次点位调整都让开发重新发版。配置文件的另一个好处是能用配置文件生成器自动导入从Excel点位表直接转JSON这个我在后面实操部分会讲。3.3 通用性设计里的三处关键处理第一标签路径的归一化。OPC DA和UA的路径分隔符不一致DA常用点号UA常用斜杠。我的做法是在配置层统一用点号进入具体实现类时再转换成协议要求的格式这样配置文件始终长一个样。第二数据类型的自动转换。OPLC里面是Int16、Int32、Real、Bool这些但C#侧往往需要转成decimal、string或者直接丢给数据库。我的实现类里内置了一个类型转换器按配置文件里的dataType字段做强制转换避免上层到处出现Convert.ToInt32这种散弹枪代码。第三重连机制的统一封装。设备断电、OPC服务器重启、网络抖动都会让连接断开。我的接口里定义了ConnectionStateChanged事件实现类内部维护一个心跳线程掉线时按指数退避策略自动重连重连成功后自动恢复订阅关系。这个机制是通用性的重中之重没有它你的程序在无人值守车间里活不过一个夜班。4. 核心代码模块解构与实现细节4.1 OPC DA客户端的连接与读写实现OPC DA的C#实现业界用得比较多的是开源的OpcDaNet库底层封装了COM互操作代码比直接引用OpcRcw写COM调用来得干净。连接一个DA服务器核心代码大概是这样using Opc; using Opc.Da; public class OpcDaClient : IPlcOpcClient { private Opc.Da.Server _server; private Opc.Da.Subscription _subscription; public void Connect(string host, string progId) { var factory new OpcCom.Factory(); var url new URL($opcda://{host}/{progId}); _server new Opc.Da.Server(factory, url); _server.Connect(); } public object ReadTag(string tagPath) { var item new Opc.Da.Item { ItemName tagPath }; var result _server.Read(new[] { item })[0]; if (result.Quality Quality.Good) return result.Value; throw new Exception($标签质量异常: {result.Quality}); } public void WriteTag(string tagPath, object value) { var item new Opc.Da.Item { ItemName tagPath }; var result _server.Write(new[] { new ItemValue(item) { Value value } }); if (result[0].ResultID.Failed()) throw new Exception($写入失败: {result[0].ResultID}); } }注意几个细节。URL里的progId是OPC服务器在Windows注册表里的COM标识比如Kepware是Kepware.KEPServerEX.V6Matrikon是Matrikon.OPC.Simulation。这个字符串错了Connnect直接抛服务器运行失败或类未注册。另外DA的读分为设备读和缓存读Read方法默认走设备速度慢要快就调用Read时传DataSource.Cache。真正高频采集场景我一般不主动读而是用Subscription订阅让服务器按采样周期推数据这个后面讲。4.2 OPC UA客户端的连接与读写实现OPC UA的C#客户端最主流的库是OPC基金会官方维护的Opc.Ua.Core和Opc.Ua.ClientNuGet包名就是OPCFoundation.NetStandard.Opc.Ua和OPCFoundation.NetStandard.Opc.Ua.Client。连接UA服务器比DA省心得多不要DCOM一个endpoint地址加证书配置就够using Opc.Ua; using Opc.Ua.Client; public class OpcUaClient : IPlcOpcClient { private Session _session; public void Connect(string endpointUrl, bool useSecurity false) { var config new ApplicationConfiguration { ApplicationName CSharp OPC Client, ApplicationUri urn:CSharpOpcClient, ApplicationType ApplicationType.Client, SecurityConfiguration new SecurityConfiguration { ApplicationCertificate new CertificateIdentifier { StoreType Directory, StorePath %CommonApplicationData%\OPC Foundation\pki } }, TransportConfigurations new TransportConfigurationCollection(), TransportQuotas new TransportQuotas { OperationTimeout 5000 }, ClientConfiguration new ClientConfiguration { DefaultSessionTimeout 60000 } }; var endpoint CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity, 5000); _session Session.Create(config, endpoint, false, CSharpSession, 60000, null, null); } public object ReadTag(string tagPath) { var nodeId new NodeId(tagPath); var value _session.ReadValue(nodeId); return value.Value; } public void WriteTag(string tagPath, object value) { var nodeId new NodeId(tagPath); var status _session.WriteValue(nodeId, value); if (StatusCodes.IsBad(status)) throw new Exception($写入失败: {status}); } }这里有个小坑Session.Create需要ApplicationConfiguration初始化时带上证书存储路径否则客户端首次连接时找不到本机证书服务器可能直接拒绝。开发阶段图省事可以把useSecurity设为false走SecurityPolicy.None生产环境再开加密和证书。还有一个新手容易忽略的点UA服务器的地址空间是分层的NodeId不一定是简单的标签字符串。很多UA服务器支持按BrowseName寻址但更稳的做法是先用Session.Browse浏览一遍服务器地址空间确认节点ID的真实格式。我在开发环境写了个小工具把服务器地址空间树全部导出来再跟设备厂商的点位表对照比自己瞎猜NodeId靠谱多了。4.3 订阅与批量读取性能和实时性的关键被动读始终有延迟和性能天花板。OPC真正的优势在订阅机制客户端向服务器注册一批感兴趣的标签服务器按采样周期检查这些标签发生变化或按固定周期推送给客户端。DA里面叫Subscription和ItemUA里面叫Subscription和MonitoredItem概念基本一一对应。DA订阅的简化实现var groupState new SubscriptionState { Name DataGroup, Active true, KeepAlive 1000, UpdateRate 200, Deadband 0 }; _subscription new Opc.Da.Subscription(groupState, null, null, _server); _subscription.DataChanged (sender, args) { foreach (var itemValue in args.Values) { Console.WriteLine(${itemValue.ItemName} {itemValue.Value}, 质量: {itemValue.Quality}); } }; _subscription.SetResultFilters(0x0001); _subscription.AddItems(new[] { new Opc.Da.Item { ItemName Channel1.Device1.Tag1 } });UA订阅的简化实现var subscription new Subscription(_session.DefaultSubscription) { PublishingInterval 200, PublishingEnabled true }; var monitor new MonitoredItem { StartNodeId new NodeId(ns2;sChannel1.Device1.Tag1), AttributeId Attributes.Value, SamplingInterval 100, QueueSize 10 }; monitor.Notification (item, e) { foreach (var value in e.NotificationValue) { Console.WriteLine(${item.StartNodeId} {value.Value}); } }; subscription.AddItem(monitor); _session.AddSubscription(subscription); subscription.ApplyChanges();有两个参数必须理解到位SamplingInterval是服务器采样底层数据的周期PublishingInterval是服务器把变化包推给客户端的周期。前者比后者小数据才不会被漏采后者决定了你的上位机看到新数据的最长等待时间。产线上常见的配置是采样100毫秒、发布200毫秒实时性足够也不会把网络打爆。抖动处理同样重要。UA的MonitoredItem支持DeadbandDAQ里叫死区。比如某个温度在50.2和50.3之间反复跳不设死区的话每秒推几十条消息数据库要被写爆。设了1%的死区后波动在0.5度以内不推送网络和数据库压力立刻降下来。4.4 数据质量戳上位机最容易忽略的东西OPC每个值都带着一个质量戳DA里是Quality枚举UA里是StatusCode。质量戳的含义就三种好、不确定、坏。我刚做上位机那会儿直接把读到的数值往数据库里怼后来发现PLC停机时从某些寄存器读出的是0这0被我当成真实产量记了。从那以后所有数值入库前都强制查一遍质量戳。质量戳的另一个用途是诊断。服务器报BadOutOfService说明标签服务被暂停了BadWaitingForInitialData说明PLC还没把数据准备好UncertainLastUsableValue说明当前值不可信但给了最近一个可用值。我把这些状态映射成程序里的采集状态枚举状态异常时在上位机界面上标红并且不写入正常业务库。这套逻辑看起来多写了一百行但能挡住大量脏数据。5. 实操过程从零搭一个PLC数据采集服务5.1 环境准备与服务器选择这一节带你完整跑一遍。先准备环境Visual Studio 2022一个.NET控制台项目我推荐直接用.NET 6以上跨平台、性能好、NuGet依赖也好拉OPC服务器我用Kepware的模拟驱动或者Prosys的UA Simulation Server。没有商业授权就用后者下载免费版默认起一个模拟设备里面带了不少随机数标签用来测试客户端足够了。NuGet需要装这几个包走UAOPCFoundation.NetStandard.Opc.Ua.Client走DAOPCDaNet也可以直接用官方COM包装器OPCFoundation.NetStandard.Opc.Ua不含DADA就用OpcDaNet5.2 写一个最小可用的采集程序完整源码我拆成一个控制台程序加两个核心类。Program里演示了如何把接口、DA客户端、UA客户端串起来class Program { static async Task Main(string[] args) { var config LoadConfig(config.json); IPlcOpcClient client config.OpcType ua ? new OpcUaClient() : new OpcDaClient(); client.ConnectionStateChanged (_, connected) Console.WriteLine($连接状态: {(connected ? 在线 : 掉线)}); client.Connect(); Console.WriteLine(OPC连接成功); client.Subscribe(config.SubscribeTags.ToArray(), values { foreach (var v in values) Console.WriteLine($[{DateTime.Now:HH:mm:ss.fff}] {v.Tag} {v.Value}, 质量{v.Quality}); }, config.Subscription.SamplingIntervalMs); await Task.Delay(Timeout.Infinite); } }配置加载子程序我用System.Text.Json反序列化public static OpcConfig LoadConfig(string path) { var json File.ReadAllText(path); return JsonSerializer.DeserializeOpcConfig(json); }接入模拟服务器后正常现象是控制台每秒按发布周期滚出一批标签值。如果标签路径配错了UA客户端会抛BadNodeIdUnknownDA客户端会返回ItemResult失败错误码很直观。5.3 用Excel点位表批量生成配置现场点位表通常是一张Excel表格几百行手工维护JSON根本不现实。我的做法是写了一个小的导入工具读取Excel的几列——路径、数据类型、读写方向——直接生成config.json。这里有个Excel里常见的坑点位路径里有时带空格和特殊字符导入时要统一清洗否则OPC服务器根本不认。导入工具的核心数据流就三步用NPOI或EPPlus读Excel逐行解析成OpcTagConfig对象。校验路径非空、类型合法重复路径去重查出格式错误的行单独输出警告。序列化成JSON跟服务器里的标签列表做一次核对输出配置了但服务器不存在的标签清单。这一步做完几百个点位的上线配置从半天压缩到几分钟。后面再有点位增删改Excel重导一遍重启服务就能生效。5.4 采集性能到底能跑到多少很多人关心C#读PLC频率的上限。实测下来用订阅方式单客户端订阅500个标签发布周期200毫秒CPU占用很低基本可以忽略用主动轮询方式单线程每秒能执行几十次批量读但并发一多就明显吃紧。所以我的结论是高频采集一律用订阅主动读只适合点对点的低频查询。还有几个影响性能的关键点。一是订阅组不要建太多一个客户端建2到3个订阅组足够每个组里放几百个item比建几十个小组稳定得多二是Session的OperationTimeout不要设太短否则服务器稍慢一点客户端就报超时重连反而拖垮吞吐三是采集线程和写库线程要分离回调里只做值转换和入队数据库写入由独立消费者批量执行这个模式能撑住很高的点位数。6. 常见问题与调试心得6.1 DCOM配置引起的灾难性故障OPC DA时代90%的故障都出在DCOM上。客户端连着服务器一读就报0x8000FFFF灾难性故障或者0x80070005拒绝访问。排查顺序我总结成一套流程第一先确认客户端进程位数跟OPC服务器的COM注册位数一致。32位的OPC服务器绝对不能被64位进程调用反之亦然。很多连不上是编译平台没改导致的。第二确认Windows防火墙放开了135端口和DCOM动态端口范围。在管理工具里打开组件服务找到DCOM配置里的OPC服务器条目把标识改为交互式用户或指定管理员账户权限里给Everyone读和启动权限。记得改完重启服务器进程。第三两台机器跨域访问时尽量让两边登录用户一致或者在组件服务里配置一致的匿名访问权限。我的经验是直接给OPC服务器程序所在机器开一个固定的服务账号比每次部署都要调整DCOM权限省心。6.2 OPC服务器枚举不到客户端找不到ProgID用OpcCom.Factory枚举本机OPC服务器时有时列表是空的。这个一般不是代码问题而是OPC服务器的COM组件没有被正确注册常见于服务器软件是绿色版或者手动拷贝的。解决办法是到安装目录找Register.bat或者用regsvr32手动注册核心DLL。另外别忘了OPC Core Components红istributable是否安装很多OPC服务器依赖它。6.3 采集程序跑一晚上就假死这种问题我排查过很多次绝大多数不是OPC库的锅而是回调线程里的异常没被捕获。订阅回调是在后台线程池里触发的万一某个标签的值转换抛了个异常整个回调链可能中断表现为程序不报错、数据也不更新了。我的处理方式是在回调入口统一包一层try-catch所有异常记录日志不让它冒泡出去。另一个假死原因是连接没有心跳。OPC UA的Session自身有超时机制但如果网络闪断后没有触发异常Session可能一直挂在半开状态。我的重连线程每隔几秒检查一次会话状态如果距离上次收到数据超过设定时间主动Dispose旧会话重新Connect。这个自愈机制上线后采集服务的连续运行时间从几天直接拉长到几个月。6.4 读出来的数值类型老是不对OPC服务器返回的数值类型跟C#里的类型不是一对一。比如UA里读一个Int16可能返回的是short也可能返回ushort读一个Real可能在数据源里其实是Double。程序里如果不做类型兜底遇到InvalidCastException整个批次读取就失败了。我的类型转换器里对所有数值类型做了统一处理能隐式转换的用Convert.ChangeType不能转换的按配置里的dataType来强转并且在转换失败时记录原始类型方便排查点位表的问题。6.5 PLC模拟器启动不了之类的环境问题这两年不少人问S7-PLCSIM Advanced为什么启动不了、还没报错。我虽然不常用PLCSIM但遇到这种无报错启动失败的问题常规排查路径是先看虚拟网卡和PLCSIM实例的IP配置是否匹配再看虚拟机/物理机是否放开了相关服务。这类问题跟OPC源代码没直接关系但如果你打算在纯软件环境里练习OPC通信我需要提醒一句OPC客户端调试不一定非要真实PLC用OPC仿真服务器就足够验证协议和代码逻辑了别在PLC模拟器上死磕。7. 配套学习资料与进阶路线7.1 必看的官方文档与规范学OPC最权威的资料永远是官方规范。OPC Foundation官网的UA规范Part 1到Part 14不用全看重点读Part 1概念、Part 4服务、Part 6映射这三份读完就对UA的架构、节点模型、通信流程有了框架性认识。DA的老规范虽然过时了但很多老设备还在用建议只理解数据访问模型别深抠COM细节。OPC UA .NET标准库的GitHub仓库本身就是最好的学习材料源码里自带SampleClient和SampleServer能编译能运行比任何二手教程都管用。看源码时重点看两个文件Session的调用流程和Subscription的发布机制看完你会对订阅为什么比主动读高效有深刻理解。7.2 学习路径怎么规划如果你是从零开始我建议按这个顺序走第一步用UA Simulation Server建一个模拟服务跑通上面的最小客户端实现连接、读、写、订阅四个功能。这一步建立手感明白OPC客户端的基本操作。第二步深入理解地址空间。用官方客户端里的Browse功能像逛文件系统一样逛一遍服务器的节点树搞清楚NodeClass、NodeId、BrowseName这些概念的实际形态。很多功能卡壳都是因为节点树不熟。第三步切换到真实设备或Kepware这类商业服务器把DCOM、证书、跨网段这些工程问题过一遍。这一步才是从会写代码到能交付的关键也是简历上能写独立完成OPC数据采集模块的底气。第四步试着给接口加功能比如历史数据读取、报警事件订阅、批量写入优化。这些进阶功能在实际项目里需求很多提前练过不吃亏。7.3 源码里值得借鉴的细节最后分享几个我源码里很实用的小设计你可以直接抄走。第一个是标签缓存订阅回调拿到的值先放一份到内存字典界面或报表要取任意标签最新值时直接从字典拿不用发同步读请求。第二个是值变化记录器按标签维度记录每次变化的时间点和数值专门帮现场排查这个值什么时候跳变的。第三个是指数退避重连连不上时先等1秒然后2秒、4秒、8秒最多30秒防止服务器恢复期间客户端反复握手把它压垮。我个人在实际项目里最大的体会是OPC这套东西代码量其实不大真正耗时间的是对现场设备的理解和对通信细节的把控。同样的标签在仿真服务器里读得好好的到现场就可能因为数据类型映射、点位偏移、服务器缓存策略不同而翻车。所以写这类程序永远要留好日志和诊断接口把每一次读写的往返时间、质量状态、异常码都记录下来。这些日志平时没人看但出问题的时候它们就是救命的线索。