YAOTU INSIGHTS

基于 .NET 的 OPC UA 通信 Demo 实战:连接、读写、订阅与心跳

基于 .NET 的 OPC UA 通信 Demo 实战:连接、读写、订阅与心跳
简介这份 .NET OPC UA 通信 Demo 面向工业自动化、物联网方向的 .NET 开发者以及需要与 PLC 等设备进行数据交互的工程师围绕连接、断开、读写、订阅与心跳监听五类核心操作提供可运行示例。资源包共 2000 个文件以 1218 个 xml 配置与说明、252 个 dll 依赖库、112 个 txt 文档、28 个 nupkg 包及 10 个 cs 源码文件为主另有少量 sln、csproj、config 等工程文件压缩包约 87.75MB结构完整便于直接编译调试。已有 674 人学习下载。读者可从中掌握客户端实例创建、服务器 URL 与证书身份验证配置、节点读写方法调用、订阅与监视项推送机制以及心跳间隔设置与回调处理等关键环节并借助示例代码理解断连资源释放与异常排查思路适合作为实际项目对接 OPC UA 服务器的参考模板。1. 从一次产线联调说起为什么 OPC UA 的 Demo 值得你亲手搭一遍很多做上位机或工控软件的同行第一次接触 OPC UA 都是在项目现场被逼出来的。设备厂商丢过来一个端点地址说“支持 OPC UA你们连一下”然后你打开 Visual Studio发现 NuGet 上光 OPC UA 相关的包就有好几个文档全是英文示例代码跑起来不是证书报错就是会话超时。更麻烦的是连接只是第一步后面还有读写、订阅、断线重连、心跳保活每一个环节都有坑。这篇笔记要讲的就是用 .NET 搭一个能跑通的 OPC UA 通信 Demo覆盖连接、断开、读写、订阅、监听心跳这五件事。它适合两类人一类是刚接手 OPC UA 对接任务、需要一份能直接复现的最小可用代码的开发者另一类是用过 OPC UA 但总是在证书、订阅回调、重连逻辑上翻车的熟手。我不会只贴代码每个环节的选型理由、参数含义、失败时该看哪里都会讲清楚。整套方案基于 .NET 生态里最常用的 OPC UA 客户端库不依赖任何商业组件本地起一个模拟服务器就能验证全流程。2. 选型与环境准备用哪个库、起什么服务器、证书怎么放2.1 客户端库的选择OPCFoundation.NetStandard.Opc.Ua 的取舍.NET 平台上做 OPC UA 客户端目前主流选择是 OPC Foundation 官方维护的OPCFoundation.NetStandard.Opc.Ua系列包。它支持 .NET Standard 2.0 及以上意味着 .NET Framework 4.6.2、.NET Core、.NET 5/6/7/8 都能用。相比一些第三方封装库官方库的优势在于协议实现完整、订阅和心跳机制有明确 API、社区问题可检索。代价是 API 偏底层配置项多新手容易在证书和安全策略上卡住。我一般会装这几个包OPCFoundation.NetStandard.Opc.Ua.Client客户端核心、OPCFoundation.NetStandard.Opc.Ua.Configuration证书和配置管理、OPCFoundation.NetStandard.Opc.Ua.Core基础类型。用dotnet add package或 NuGet 管理器安装即可版本选当前稳定版不要混用不同大版本。# 在项目目录下执行安装 OPC UA 客户端相关包 dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client dotnet add package OPCFoundation.NetStandard.Opc.Ua.Configuration dotnet add package OPCFoundation.NetStandard.Opc.Ua.Core这里的关键点是Client包负责会话、订阅、读写Configuration包负责应用证书的生成和加载。很多人只装 Client结果连不上就是因为缺少应用实例证书配置。2.2 本地模拟服务器用官方示例服务器验证不碰真实设备在没接真实设备之前必须有一个可控的服务器来验证客户端逻辑。OPC Foundation 官方提供了一个参考服务器示例通常随 SDK 或 GitHub 仓库提供。你可以直接运行它默认端点一般是opc.tcp://localhost:62541或类似地址。如果找不到现成的也可以用开源的 OPC UA 模拟服务器工具起一个带基本变量和事件的端点。我习惯在本地起服务器后先用一个通用的 OPC UA 客户端工具连一下确认端点地址、安全策略、匿名访问是否开放。这一步能排除掉一半“连不上”的问题。模拟服务器上一般会有ServerStatus、CurrentTime这类标准节点还有可读写的自定义变量节点正好用来测读写和订阅。2.3 证书与安全策略开发阶段怎么配最省事OPC UA 的安全模型基于证书这是新手最大的拦路虎。客户端和服务器需要互相信任对方的应用证书。开发阶段最省事的做法是客户端配置为自动接受不受信任的服务器证书服务器端也设置为允许匿名和自动信任。生产环境当然不能这样但 Demo 阶段先把链路跑通。在客户端配置里需要设置AutoAcceptUntrustedCertificates true并指定一个证书存储目录。应用证书会在首次运行时自动生成。安全策略方面如果服务器支持None可以先用SecurityPolicy.None和MessageSecurityMode.None跑通再逐步升级到Basic256Sha256SignAndEncrypt。// 应用配置设置证书自动接受和存储路径 var appConfig new ApplicationConfiguration() { ApplicationName OpcUaDemoClient, ApplicationType ApplicationType.Client, SecurityConfiguration new SecurityConfiguration { // 开发阶段自动接受不受信任的证书生产环境务必关闭 AutoAcceptUntrustedCertificates true, // 证书存储目录首次运行会自动生成应用证书 ApplicationCertificate new CertificateIdentifier { StoreType CertificateStoreType.Directory, StorePath pki/own }, TrustedIssuerCertificates new CertificateTrustList { StoreType CertificateStoreType.Directory, StorePath pki/issuer }, TrustedPeerCertificates new CertificateTrustList { StoreType CertificateStoreType.Directory, StorePath pki/trusted }, RejectedCertificateStore new CertificateTrustList { StoreType CertificateStoreType.Directory, StorePath pki/rejected } }, // 传输配额Demo 阶段用默认值即可 TransportConfigurations new TransportConfigurationCollection(), TransportQuotas new TransportQuotas { OperationTimeout 15000 }, ClientConfiguration new ClientConfiguration { DefaultSessionTimeout 60000 } }; await appConfig.Validate(ApplicationType.Client);这段配置里OperationTimeout是单次操作超时DefaultSessionTimeout是会话超时。开发阶段把AutoAcceptUntrustedCertificates打开能省去大量证书信任操作但上线前一定要关掉并正确配置信任列表。3. 连接与断开会话建立、保活和优雅关闭的完整写法3.1 建立会话EndpointDescription 与 Session 的创建流程连接 OPC UA 服务器的核心是两步先发现端点再创建会话。发现端点用DiscoveryClient或直接指定端点 URL。创建会话需要EndpointDescription、ApplicationConfiguration和用户身份。匿名访问传null或new UserIdentity()即可。// 发现端点并创建会话 var endpointUrl opc.tcp://localhost:62541; var endpointDescription CoreClientUtils.SelectEndpoint(appConfig, endpointUrl, useSecurity: false); var endpointConfiguration EndpointConfiguration.Create(appConfig); var endpoint new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); // 创建会话匿名身份会话名自定义 var session await Session.Create( appConfig, endpoint, updateBeforeConnect: false, sessionName: OpcUaDemoSession, sessionTimeout: 60000, identity: new UserIdentity(), preferredLocales: null );SelectEndpoint的useSecurity参数决定是否选择安全端点。Demo 阶段传false选无安全策略端点如果服务器只开了安全端点这里会抛异常需要改成true并确保证书互信。Session.Create的sessionTimeout单位是毫秒会话超时后服务器会清理会话客户端需要重建。3.2 会话保活KeepAlive 与重连的触发条件会话建立后客户端需要定期发送保活请求否则服务器会在超时后关闭会话。官方库的Session对象内部有保活机制但重连逻辑需要自己处理。我一般会订阅Session.KeepAlive事件在事件里检查会话状态如果发现SessionState不是Activated就触发重连。// 订阅保活事件处理会话状态变化 session.KeepAlive (sender, e) { if (e.Session.State ! SessionState.Activated) { // 会话异常记录日志并触发重连 Console.WriteLine($会话状态异常: {e.Session.State}); // 实际项目中这里应调用重连方法 } else { // 正常保活可记录心跳时间 Console.WriteLine($保活正常: {DateTime.Now:HH:mm:ss}); } };保活事件的触发频率由服务器的RevisedSessionTimeout决定通常是会话超时的三分之一到二分之一。如果保活事件长时间不触发说明网络断了或服务器挂了需要主动重连。3.3 优雅断开Close 与 Dispose 的顺序不能反断开连接时正确顺序是先关闭会话再释放配置资源。直接Dispose会话可能导致服务器端会话残留直到超时才清理。// 优雅关闭先关闭会话再释放资源 if (session ! null) { await session.CloseAsync(); session.Dispose(); session null; }CloseAsync会向服务器发送关闭会话请求服务器立即清理资源。如果直接Dispose服务器可能仍认为会话活跃占用连接数。在频繁重连的场景下这个顺序不对会导致服务器端会话数暴涨。4. 读写与订阅节点操作、订阅回调、心跳监听的落地细节4.1 同步读写与异步读写ReadValue / WriteValue 的参数与返回码读写节点用Session.ReadValue和Session.Write。读操作返回DataValue包含值、状态码、时间戳。写操作需要构造WriteValue集合指定节点 ID、属性、值。// 读取节点值 var nodeId new NodeId(ns2;sDemo.Variable); DataValue value session.ReadValue(nodeId); Console.WriteLine($值: {value.Value}, 状态: {value.StatusCode}, 时间: {value.SourceTimestamp}); // 写入节点值 var writeValue new WriteValue { NodeId nodeId, AttributeId Attributes.Value, Value new DataValue(new Variant(123.45)) }; var writeCollection new WriteValueCollection { writeValue }; StatusCollection results session.Write(null, writeCollection, out DiagnosticInfoCollection diagnostics); foreach (var status in results) { Console.WriteLine($写入状态: {status}); }ReadValue是同步阻塞调用高频读取建议用异步版本ReadValueAsync。写入时AttributeId固定为Attributes.ValueVariant包装实际值。返回的StatusCollection里每个状态码对应一个写入项Good表示成功BadNodeIdUnknown表示节点不存在。4.2 订阅的创建MonitoredItem、Subscription 与回调线程订阅是 OPC UA 最实用的功能服务器在数据变化时主动推送。创建订阅需要三步建Subscription、加MonitoredItem、绑定通知回调。// 创建订阅发布间隔 1000ms var subscription new Subscription(session.DefaultSubscription) { PublishingInterval 1000, KeepAliveCount 10, LifetimeCount 20, MaxNotificationsPerPublish 1000, PublishingEnabled true }; session.AddSubscription(subscription); await subscription.CreateAsync(); // 添加监控项 var monitoredItem new MonitoredItem(subscription.DefaultItem) { DisplayName DemoVariable, StartNodeId new NodeId(ns2;sDemo.Variable), AttributeId Attributes.Value, SamplingInterval 500, QueueSize 10, DiscardOldest true }; monitoredItem.Notification OnMonitoredItemNotification; subscription.AddItem(monitoredItem); await subscription.ApplyChangesAsync();PublishingInterval是服务器推送间隔SamplingInterval是服务器采样间隔。QueueSize是客户端队列长度DiscardOldest决定队列满时丢弃最旧还是最新数据。回调OnMonitoredItemNotification在后台线程执行更新 UI 需要切回主线程。4.3 心跳监听的两种实现订阅 ServerStatus 与自定义心跳节点心跳监听有两种常见做法。一种是订阅服务器标准节点ServerStatus下的CurrentTime或State服务器状态变化时会推送。另一种是在服务器上定义一个自定义心跳变量服务器周期性更新它客户端订阅该节点。// 订阅 ServerStatus 的 CurrentTime 作为心跳 var heartbeatItem new MonitoredItem(subscription.DefaultItem) { DisplayName ServerHeartbeat, StartNodeId new NodeId(ns0;i2258), // ServerStatus_CurrentTime AttributeId Attributes.Value, SamplingInterval 1000, QueueSize 1 }; heartbeatItem.Notification (item, e) { foreach (var value in e.NotificationValue.NotificationValues) { Console.WriteLine($心跳时间: {value.Value}); } }; subscription.AddItem(heartbeatItem); await subscription.ApplyChangesAsync();ns0;i2258是标准节点ServerStatus_CurrentTime的 NodeId。如果服务器不支持该节点可以换成自定义心跳节点。心跳超时判断逻辑记录最后一次收到心跳的时间如果超过阈值比如 3 倍采样间隔没收到就认为连接异常。5. 避坑与排查证书、订阅回调、重连里的血泪经验5.1 证书报错BadCertificateUntrusted 的三种触发场景现象连接时抛BadCertificateUntrusted或BadSecurityChecksFailed。原因通常有三种客户端证书未被服务器信任、服务器证书未被客户端信任、证书链不完整。解决方法是把客户端证书导出到服务器的信任目录把服务器证书导入客户端的信任目录。开发阶段可以临时开启AutoAcceptUntrustedCertificates但生产环境必须走正规信任流程。5.2 订阅回调不触发SamplingInterval 与 PublishingInterval 的关系现象订阅创建成功但回调一直不执行。原因往往是SamplingInterval大于PublishingInterval或者节点值没有变化。OPC UA 的订阅是变化推送值不变时不推送。如果节点是静态的可以设置SamplingInterval小于PublishingInterval并确认服务器支持该节点的采样。另外QueueSize为 0 时表示只保留最新值也可能导致回调行为异常。5.3 重连后订阅丢失Session 重建与 Subscription 的恢复现象断线重连后读写正常但订阅不再推送。原因是Session重建后原来的Subscription对象绑在旧会话上不会自动恢复。解决方法是重连后重新创建订阅和监控项或者使用Session.Recreate方法尝试恢复。我一般会在重连逻辑里把订阅创建封装成独立方法重连成功后调用一次。5.4 心跳误判网络抖动与 KeepAlive 事件的区分现象心跳偶尔超时但实际连接正常。原因是网络抖动导致保活事件延迟或者服务器负载高时推送变慢。解决方法是把心跳超时阈值设得宽松一些比如 3 到 5 倍采样间隔并在超时后先做一次主动读取验证读取成功则说明连接仍在只是心跳延迟。5.5 内存泄漏MonitoredItem 与 Subscription 的释放现象长时间运行后内存持续增长。原因是订阅和监控项没有正确释放或者回调事件没有解绑。解决方法是在断开连接前先删除所有监控项再删除订阅最后关闭会话。MonitoredItem.Notification事件在删除监控项后会自动解绑但手动置空引用更稳妥。6. 进阶技巧把 Demo 改造成可复用的通信组件6.1 封装 OpcUaClient 类连接、读写、订阅的统一入口把前面散落的代码封装成一个OpcUaClient类对外暴露ConnectAsync、DisconnectAsync、ReadAsync、WriteAsync、SubscribeAsync等方法。内部管理Session、Subscription和重连逻辑。这样在多个项目里复用不用每次重写证书配置和保活处理。public class OpcUaClient : IDisposable { private Session _session; private ApplicationConfiguration _appConfig; private readonly string _endpointUrl; public OpcUaClient(string endpointUrl) { _endpointUrl endpointUrl; } public async Task ConnectAsync() { // 加载配置、发现端点、创建会话 _appConfig await BuildAppConfigAsync(); var endpointDescription CoreClientUtils.SelectEndpoint(_appConfig, _endpointUrl, useSecurity: false); var endpointConfiguration EndpointConfiguration.Create(_appConfig); var endpoint new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); _session await Session.Create(_appConfig, endpoint, false, OpcUaClient, 60000, new UserIdentity(), null); _session.KeepAlive OnKeepAlive; } private void OnKeepAlive(object sender, KeepAliveEventArgs e) { // 保活处理与重连触发 } public async TaskDataValue ReadAsync(NodeId nodeId) { return await _session.ReadValueAsync(nodeId); } public void Dispose() { _session?.CloseAsync().Wait(3000); _session?.Dispose(); } }这个类里BuildAppConfigAsync负责证书和传输配置OnKeepAlive负责状态监控。对外只暴露业务方法调用方不用关心 OPC UA 底层细节。6.2 用配置文件管理端点与节点appsettings.json 的映射把端点 URL、安全策略、节点 ID 放到appsettings.json避免硬编码。用Microsoft.Extensions.Configuration读取映射成配置类。这样换设备时只改配置不改代码。{ OpcUa: { EndpointUrl: opc.tcp://localhost:62541, UseSecurity: false, SessionTimeout: 60000, Nodes: { DemoVariable: ns2;sDemo.Variable, Heartbeat: ns0;i2258 } } }读取时用configuration.GetSection(OpcUa).GetOpcUaOptions()把Nodes字典传给客户端类。节点 ID 的字符串格式要符合 OPC UA 规范ns2;sxxx表示命名空间 2 下的字符串标识符。6.3 验证方法用模拟服务器做断线重连与心跳超时测试验证组件是否可靠不能只测正常流程。我一般会做三个测试一是手动停掉模拟服务器观察客户端是否触发重连二是恢复服务器确认订阅是否自动恢复三是模拟网络延迟确认心跳超时阈值是否合理。测试时打开日志记录每次状态变化的时间戳对照保活事件和心跳事件的时间差。最后一个习惯每次改完连接或订阅逻辑先在本机模拟服务器上跑 30 分钟观察内存和会话数是否稳定。这个习惯帮我省掉了很多现场调试的麻烦。希望帮到你。本文还有配套的精品资源点击获取