简介这是一份面向.NET开发者与工业自动化工程师的OPC UA通信实战Demo围绕连接、断开、读写、订阅与心跳监听五大核心操作展开帮助读者快速掌握在.NET环境中与PLC及自动化设备进行数据交互的方法。资源包共2000个文件以1218个xml配置与元数据、252个dll程序集、112个txt说明、28个nupkg依赖包及10个cs源码文件为主另含sln解决方案与csproj工程文件压缩包约87.75MB结构完整便于直接编译调试。已有674人学习下载。通过该Demo读者可参考客户端实例创建、服务器URL与安全证书配置、节点读写调用、订阅与监视项推送、心跳回调处理等关键代码片段理解OPC UA协议从基础连接到高级订阅的完整链路为工业物联网项目落地提供可复用的排错思路与工程模板。1. 从一次产线联调说起这个 .NET OPC UA Demo 到底能省多少事去年帮一个做自动化集成的朋友排查问题现场有台设备用的是 OPC UA 协议对外暴露数据上位机那边用 .NET 写的客户端死活连不上日志里只有一句干巴巴的BadTimeout。他手头没有可用的参考代码只能对着协议文档一行行猜光握手阶段就耗了两天。后来我把自己压箱底的一个 .NET OPC UA 通信 Demo 丢给他连接、断开、读写、订阅、心跳监听五个动作全都有现成实现改个 IP 和端口就能跑半小时定位到是安全策略选错了。这个 Demo 解决的就是这类问题它把 OPC UA 客户端最常用的五类操作封装成可直接调用的方法基于 .NET 生态里成熟的 OPC UA 库不依赖任何商业组件。适合两类人——刚接触工业通信、需要一份能跑通的参考代码快速上手的开发者以及手头有项目、想找个稳定骨架直接往里填业务逻辑的熟手。下面我按实际拆解的顺序把这份资源从结构到跑通再到踩坑讲一遍。2. 拆开这份 Demo五个核心动作的代码骨架与选型逻辑2.1 为什么是这五个动作而不是别的工业现场对 OPC UA 客户端的需求九成集中在五件事上建立会话、释放会话、读点位、写点位、订阅变化。心跳监听严格说属于订阅的一种特殊用法——订阅一个周期性变化的节点用它的更新时间戳判断链路是否还活着。这份 Demo 把这五个动作拆成独立方法好处是每个方法职责单一你调试的时候能精确定位到是哪一步出的问题而不是面对一个几百行的Main函数猜。选型上.NET 这边做 OPC UA 客户端常见做法是用Opc.Ua.Client这个库它是 OPC Foundation 官方 .NET 标准栈的一部分NuGet 上直接能拉到。相比自己基于协议规范手写二进制解析用现成库能把握手、加密、序列化这些脏活挡在外面。代价是库本身有一定学习曲线配置项多默认值不一定适合你的现场——这正是 Demo 存在的意义它把常用配置固化下来你照着改就行。2.2 连接与断开会话建立的参数怎么填连接的核心是构造Session对象。下面这段是 Demo 里连接部分的简化写法我加了注释说明每个参数的作用。// 创建应用配置ApplicationUri 建议用唯一标识避免多客户端冲突 var config new ApplicationConfiguration { ApplicationName OpcUaDemoClient, ApplicationUri $urn:{System.Net.Dns.GetHostName()}:OpcUaDemoClient, ApplicationType ApplicationType.Client, SecurityConfiguration new SecurityConfiguration { // 自动接受服务端证书生产环境建议改为手动校验 AutoAcceptUntrustedCertificates true, // 不强制使用证书吊销列表内网环境常见做法 RejectSHA1SignedCertificates false }, TransportConfigurations new TransportConfigurationCollection(), TransportQuotas new TransportQuotas { OperationTimeout 15000 }, ClientConfiguration new ClientConfiguration { DefaultSessionTimeout 60000 } }; await config.Validate(ApplicationType.Client); // 选择端点SecurityPolicy 和 MessageSecurityMode 必须与服务端匹配 var endpointDescription CoreClientUtils.SelectEndpoint( opc.tcp://192.168.1.100:4840, useSecurity: false); var endpointConfiguration EndpointConfiguration.Create(config); var endpoint new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); // 建立会话sessionName 会显示在服务端的会话列表里方便排查 var session await Session.Create( config, endpoint, false, OpcUaDemoSession, 60000, new UserIdentity(new AnonymousIdentityToken()), null);逻辑上分三步先构造应用配置再选端点最后建会话。参数里最容易翻车的是SecurityPolicy和MessageSecurityMode——服务端如果开了Basic256Sha256加SignAndEncrypt你客户端用None去连必然被拒。OperationTimeout默认值偏短现场网络抖动时容易误报超时我一般会调到 15000 毫秒以上。断开就是调session.Close()再session.Dispose()注意顺序先关会话再释放资源反过来可能抛异常。2.3 读写操作节点 ID 的坑与批量读的写法读写的前提是拿到正确的节点 ID。OPC UA 的节点 ID 格式有好几种常见的是ns2;sDevice1.Tag1这种字符串形式ns是命名空间索引s表示字符串标识符。命名空间索引不是固定的不同服务端可能不一样写死之前先用session.NamespaceUris确认一下。// 读单个节点 var readValue session.ReadValue(ns2;sDevice1.Temperature); Console.WriteLine($值: {readValue.Value}, 状态: {readValue.StatusCode}); // 批量读减少往返次数现场点位多时必用 var nodesToRead new ReadValueIdCollection { new ReadValueId { NodeId ns2;sDevice1.Temperature, AttributeId Attributes.Value }, new ReadValueId { NodeId ns2;sDevice1.Pressure, AttributeId Attributes.Value } }; session.Read(null, 0, TimestampsToReturn.Both, nodesToRead, out var results, out var errors); // 写单个节点注意值类型要和节点定义匹配 var writeValue new WriteValue { NodeId ns2;sDevice1.SetPoint, AttributeId Attributes.Value, Value new DataValue(new Variant(25.5)) }; session.Write(null, new WriteValueCollection { writeValue }, out var writeResults, out var writeErrors);批量读的maxAge参数填 0 表示强制从设备取最新值填大于 0 的数会走服务端缓存对实时性要求高的场景别用缓存。写操作返回的writeErrors一定要检查很多时候写失败不抛异常只在状态码里体现不检查就以为写成功了。2.4 订阅与心跳监听把变化推给你而不是你轮询订阅是 OPC UA 相比传统轮询最有价值的地方。你告诉服务端“我关心这几个节点变化超过阈值就通知我”服务端主动推。心跳监听就是订阅一个周期性变化的节点比如服务端自己维护的秒计数器超过预期时间没收到通知就判定链路异常。// 创建订阅PublishingInterval 是服务端推送周期单位毫秒 var subscription new Subscription(session.DefaultSubscription) { PublishingInterval 1000, KeepAliveCount 10, LifetimeCount 30 }; session.AddSubscription(subscription); subscription.Create(); // 添加监控项SamplingInterval 是服务端采样周期 var monitoredItem new MonitoredItem(subscription.DefaultItem) { StartNodeId ns2;sDevice1.Heartbeat, AttributeId Attributes.Value, SamplingInterval 500, QueueSize 10, DiscardOldest true }; monitoredItem.Notification (item, e) { // 每次收到通知刷新时间戳用于心跳判断 Console.WriteLine($心跳: {e.NotificationValue}); }; subscription.AddItem(monitoredItem); subscription.ApplyChanges();PublishingInterval和SamplingInterval的关系要理清采样是服务端读设备值的频率发布是服务端往客户端推的频率。采样比发布快中间的变化会被缓存QueueSize决定缓存多少个。心跳场景下KeepAliveCount设成 10 意味着服务端每 10 个发布周期没数据也会发一个空通知保活客户端据此判断链路。3. 从零跑通环境准备、连接测试与读写验证3.1 环境准备与依赖安装跑这个 Demo 需要 .NET 环境版本建议 6.0 或以上低版本对异步支持不完整容易在await上出玄学问题。依赖就一个核心包用命令行装# 创建控制台项目 dotnet new console -n OpcUaDemoClient cd OpcUaDemoClient # 安装 OPC UA 客户端库 dotnet add package Opc.Ua.Client # 如果要用到配置文件的复杂结构再加这个 dotnet add package Opc.Ua.Configuration装完检查一下csproj里的版本号不同大版本之间 API 有变动比如Session.Create的重载参数在 3.x 和 4.x 里就不一样。我一般会锁定一个稳定版本避免自动升级带来意外。3.2 连接测试先确认能握手再谈读写环境好了之后第一步不是急着读写而是确认能连上。找一个可用的 OPC UA 服务端地址本地没有的话可以用公开的测试服务端或者自己搭一个模拟服务端。连接测试的代码就是 2.2 节那段跑起来后看控制台有没有输出会话 ID。如果连不上按这个顺序排查先ping确认网络通再用telnet或Test-NetConnection确认端口开放然后检查安全策略是否匹配最后看服务端有没有把客户端 IP 加入白名单。现场最常见的翻车是安全策略不匹配服务端日志里会有明确的拒绝原因别只盯着客户端看。3.3 读写验证用已知点位做闭环测试连上之后找一个确定存在的节点做读测试。读到的值如果和预期一致说明读链路通了。写测试要谨慎别拿生产点位试找一个可写的测试点位写一个安全的值然后再读回来确认写进去了。// 写后读回验证写入是否生效 var testNodeId ns2;sDevice1.TestTag; session.Write(null, new WriteValueCollection { new WriteValue { NodeId testNodeId, AttributeId Attributes.Value, Value new DataValue(new Variant(123)) } }, out var wr, out var we); // 稍等片刻再读给服务端处理时间 await Task.Delay(200); var check session.ReadValue(testNodeId); Console.WriteLine($写回验证: {check.Value});写后读回这个习惯能帮你排除“写请求发出去了但服务端没接受”的情况。有些服务端对写入有权限控制返回的状态码是BadUserAccessDenied不读回来看不出来。3.4 订阅验证观察通知频率是否符合预期订阅跑起来后观察控制台输出通知的频率。如果PublishingInterval设的 1000 毫秒但通知来得稀稀拉拉可能是SamplingInterval设太大了或者服务端负载高。反过来通知来得太频繁检查是不是QueueSize太小导致频繁触发。心跳监听单独验证把服务端停掉看客户端多久能感知到。正常情况下KeepAliveCount乘以PublishingInterval就是理论上的感知时间实际会略长。如果停掉服务端后客户端毫无反应检查KeepAliveCount是不是设太大了或者通知回调里没做超时判断。4. 避坑与排查五个我踩过的真实坑4.1 坑一连接超时但网络是通的现象Session.Create抛BadTimeout但ping服务端是通的端口也开着。原因OperationTimeout默认值太短或者服务端在握手阶段做了证书校验校验耗时超过了超时时间。解决把TransportQuotas.OperationTimeout调到 15000 以上同时检查服务端证书是否被客户端信任。如果是自签名证书要么把证书导入客户端信任列表要么临时开AutoAcceptUntrustedCertificates但生产环境别这么干。4.2 坑二读到的值一直是旧值现象设备明明变了读回来的值还是几分钟前的。原因ReadValue默认走服务端缓存maxAge参数没设对。解决读的时候把maxAge设为 0强制服务端从设备取最新值。批量读的Read方法里那个maxAge参数同理。如果对实时性要求高干脆用订阅代替轮询读。4.3 坑三订阅通知收不到现象订阅创建成功但回调一直不触发。原因StartNodeId写错了或者SamplingInterval设得比PublishingInterval还大导致数据还没采样就发布了。解决先用读操作确认节点 ID 正确再把SamplingInterval设成小于等于PublishingInterval。另外检查subscription.ApplyChanges()有没有调用漏了这步监控项不会生效。4.4 坑四心跳监听误报断线现象链路正常但心跳偶尔报超时。原因KeepAliveCount设太小服务端正常抖动就触发了超时判断。解决把KeepAliveCount调大比如从 5 调到 10 或 15给链路留出抖动余量。同时心跳判断逻辑里加一个重试计数连续多次超时才判定断线单次超时忽略。4.5 坑五断开连接时抛异常现象调session.Close()时抛NullReferenceException或通信异常。原因会话已经因为网络问题断开了再调Close就会出问题。解决关闭前先判断session.Connected状态或者用try-catch包住异常吞掉即可。顺序上先Close再Dispose反过来可能资源没释放干净。5. 进阶用法把心跳监听做成可复用的健康检查模块跑通基础功能之后我习惯把心跳监听单独抽成一个健康检查模块而不是散落在业务代码里。思路是订阅一个服务端必然存在的周期性节点客户端维护一个最后收到通知的时间戳另起一个定时器检查这个时间戳是否超时。public class HeartbeatMonitor { private DateTime _lastNotification DateTime.UtcNow; private readonly TimeSpan _timeout; private readonly Timer _checkTimer; public HeartbeatMonitor(TimeSpan timeout) { _timeout timeout; // 每 2 秒检查一次检查频率应小于超时时间 _checkTimer new Timer(Check, null, 0, 2000); } // 订阅回调里调这个方法刷新时间戳 public void OnHeartbeatReceived() { _lastNotification DateTime.UtcNow; } private void Check(object state) { var elapsed DateTime.UtcNow - _lastNotification; if (elapsed _timeout) { // 触发断线处理比如重连或告警 Console.WriteLine($心跳超时已 {elapsed.TotalSeconds:F1} 秒未收到通知); } } }这个模块的关键参数是_timeout我一般设成KeepAliveCount乘以PublishingInterval再乘以 2留一倍余量。检查频率设成超时时间的一半左右太频繁浪费资源太稀疏反应慢。验证这个模块是否可靠有个简单办法正常跑一段时间看有没有误报然后手动把服务端停掉看多久能报出来。如果停掉后一直不报检查订阅是不是已经断了但没触发重连。实际项目里我还会加一个自动重连逻辑检测到超时后先尝试重建会话连续失败再告警。从那以后我每次接 OPC UA 项目都强制先把心跳模块跑通再写业务逻辑链路健康是一切读写的前提。希望这份 Demo 和上面的踩坑记录能帮你少走点弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?