ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OPC UA C#开发实战:基于UA-.NETStandard的服务器与客户端搭建

OPC UA C#开发实战:基于UA-.NETStandard的服务器与客户端搭建 简介此压缩包是一份基于.NET Standard实现的OPC UA通用架构代码内含可直接运行的DEMO面向C#/.NET开发者以及工业自动化、物联网、远程监控等领域的工程师。OPC UA不仅是接口规范更包含了数据模型与安全框架这份代码演示了如何在.NET环境中搭建标准兼容的OPC UA客户端或服务器涵盖连接建立、读写数据、订阅通知、安全认证等核心环节为跨平台数据交换提供了一套可复用的工程骨架。资源包大小约10.72MB采用Git仓库式目录组织源码结构清晰便于逐模块阅读和编译调试。已有341人学习/下载无论刚接触OPC UA的新手还是需要快速搭建原型的中高级开发者都能从中获得可直接借鉴的实现思路与DEMO参考。1. UA-.NETStandard 这个仓库为什么值得当 OPC UA 架构蓝本拿到一个叫 UA-.NETStandard-master 的目录别急着双击 sln 找入口。这是 OPC Foundation 维护的 .NET Standard OPC UA 协议栈压缩包里的 DEMO 不是某个业务系统而是把 OPC UA 通用架构——传输、会话、节点管理、证书信任——串起来的最小可运行骨架。对做上位机、边缘网关或设备接入的人来说OPC UA C# 连接这件事照着这个 DEMO 拆比对着网上零散博客拼要靠谱得多。下面按「先看架构分层再搭 Server再连 Client最后处理证书配置」的顺序展开。中间所有代码都可以直接拷到 .NET 6/.NET 8 或 .NET Framework 4.6.1 工程里编译后面几章还会补上 UaExpert 验证和 Node-RED 转发 MQTT 的常见接法。2. 基于 UA-.NETStandard 的 OPC UA Server最小工程与节点建模2.1 从仓库目录看懂 OPC UA 通用架构的四个层次先花两分钟把仓库结构过一遍这决定了你把业务代码写在哪一层。Libraries/Opc.Ua.Core 是协议栈本体负责 OPC UA 二进制消息的编解码、安全通道、传输层和会话管理这一层普通应用不该碰。Libraries/Opc.Ua.Client 封装了 Session、Subscription、MonitoredItem是客户端程序的地基。Libraries/Opc.Ua.Server 提供 StandardServer、BaseNodeManager、BaseVariableState 这些基类把端点监听、证书校验、节点查询的公共逻辑收进去。最上面一层才是业务代码你的 NodeManager 子类和它管理的自定义节点DEMO 里的模拟数据就挂在这里。理解这个四层划分就理解了 OPC UA 通用架构的含义不管业务是采集 PLC、模拟温湿度还是对接 MES传输和会话代码始终只有一份业务只需要实现 NodeManager 和节点。后文所有代码都遵循这个分层否则 DEMO 越长越乱最后变成把所有逻辑塞进一个类。2.2 用 NuGet 包代替源码编译包名与用途对照有人拿到源码习惯直接编译整个仓库代价是构建时间变长、依赖版本容易被拖乱。常见做法是只引用 NuGet 上官方发布的 OPCFoundation 包源码只在排查协议栈内部问题时才整个编译。包和仓库目录的对应关系如下。NuGet 包名对应仓库目录主要类型OPCFoundation.NetStandard.Opc.Ua.CoreLibraries/Opc.Ua.Core编码解码、传输、安全通道OPCFoundation.NetStandard.Opc.Ua.ClientLibraries/Opc.Ua.ClientSession、Subscription、MonitoredItemOPCFoundation.NetStandard.Opc.Ua.ServerLibraries/Opc.Ua.ServerStandardServer、NodeManager 基类OPCFoundation.NetStandard.Opc.Ua.ConfigurationLibraries/Opc.Ua.Configuration证书、应用配置加载这些包目标框架是 .NET Standard 2.0.NET Framework 4.6.1 以上和 .NET 6/8 都能引用。这一点在工控环境下很实用老工控机可能还在跑 .NET Framework新边缘网关用 .NET 8同一套 DemoServer 代码两边都能编译不需要维护两份工程。2.3 一个最小 Server DEMO建节点、起服务、看地址空间下面是最小 Server 的骨架。先写节点管理器把业务节点挂到 OPC UA 地址空间public class DemoNodeManager : BaseNodeManager { public DemoNodeManager(IServerInternal server, ApplicationConfiguration configuration) // 第三个参数是本 NodeManager 的 NamespaceUri对应客户端看到的 ns2 : base(server, configuration, urn:demo:opcua) { } protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context) { NodeStateCollection nodes new NodeStateCollection(); BaseVariableState temperature new BaseVariableState( context, null, new NodeId(demo.temperature, NamespaceIndex)); temperature.DisplayName new LocalizedText(温度); temperature.Description new LocalizedText(模拟温度值); temperature.DataType DataTypeIds.Double; // 值类型映射到 OPC UA Double temperature.Value new Variant(25.0); temperature.AccessLevel AccessLevels.CurrentReadOrWrite; nodes.Add(temperature); return nodes; } }LoadPredefinedNodes在 Server 启动时回调这里创建的节点会出现在任何 OPC UA 客户端的地址空间里。NodeId 由两部分组成demo.temperature是字符串标识符NamespaceIndex是命名空间索引BaseNodeManager 构造函数传入的urn:demo:opcua会被注册为 ns2——ns0 和 ns1 分别被 OPC UA 规范和 UA-.NETStandard 内置节点占用自定义节点从 2 开始是惯例。AccessLevels.CurrentReadOrWrite决定这个变量能否被客户端写只读信号改成AccessLevels.CurrentRead即可否则客户端写值会收到 BadNotWritable。然后是 Server 主类和启动入口public class DemoServer : StandardServer { private DemoNodeManager _nodeManager; protected override void OnServerStarting(ApplicationConfiguration configuration) { base.OnServerStarting(configuration); _nodeManager new DemoNodeManager(ServerInternal, configuration); ServerInternal.NodeManager.Add(_nodeManager); // 注册到内部节点表 } }static async Task Main(string[] args) { // 从 XML 读取端点、安全策略、证书路径 ApplicationConfiguration config await ApplicationConfiguration.Load( server.config.xml, ServerType.Server); // 证书校验器必须在 Start 之前初始化 await config.CertificateValidator.Update(config.SecurityConfiguration); await config.ApplicationIdentity.CreateApplicationCertificate(); DemoServer server new DemoServer(); await server.Start(config); Console.WriteLine(Server 已启动: opc.tcp://localhost:48010); Console.ReadLine(); await server.Stop(); }server.config.xml里至少要声明 BaseAddresses如opc.tcp://localhost:48010、SecurityPolicies 三条策略和 UserTokenPoliciesAnonymous 与 UserName。CreateApplicationCertificate在证书缺失时自动生成自签名证书开发期可用生产环境应该走企业 CA 或者预先分发证书这块在第 4 章展开。提示第一次跑 demo 如果发现opc.tcp://localhost:48010起不来先检查端口占用和防火墙。Windows 上 48010 属于高位端口一般不会被系统保留但某些杀软会拦截监听行为。3. OPC UA C# 连接实战客户端读、写与订阅的三段核心代码3.1 客户端配置与 Session 建立先解决 Endpoint 选择客户端代码比服务端更常被复制到真实项目里因为大多数情况下你是要连别人的服务器。WinCC 做 OPC UA 服务器、Kepware 做 OPC UA 服务器客户端代码都不用改换 EndpointUrl 就行。ApplicationConfiguration config new ApplicationConfiguration { ApplicationName DemoClient, ApplicationUri urn:demo:client, ApplicationType ApplicationType.Client, SecurityConfiguration new SecurityConfiguration { ApplicationCertificate new CertificateIdentifier { StoreType Directory, StorePath pki/client/certs // 客户端自己的证书 }, TrustedPeerCertificates new CertificateTrustList { StoreType Directory, StorePath pki/client/trusted // 信任的服务器证书 }, RejectedCertificateStore new CertificateTrustList { StoreType Directory, StorePath pki/client/rejected // 未信任证书的回收目录 } }, TransportQuotas new TransportQuotas { OperationTimeout 15000 }, ClientConfiguration new ClientConfiguration { DefaultSessionTimeout 60000 } }; config.Validate(ApplicationType.Client); // 用发现端点取服务器支持的端点列表useSecurityfalse 时选最低安全等级 EndpointDescription endpoint CoreClientUtils.SelectEndpoint(config, opc.tcp://localhost:48010, useSecurity: false); Session session await Session.Create(config, endpoint, demo-client, 60000, new UserIdentity(user1, password), null);SelectEndpoint走的是 OPC UA 发现服务它会拿到服务器端点列表按安全策略过滤后返回一个可连接的端点。useSecurity: false表示能连上就行开发期最省事上线前改成 true让它优先选 Basic256Sha256。Session.Create 的第四个参数是会话超时毫秒第五个是用户身份new UserIdentity(user1, password)对应服务器 UserTokenPolicies 里的 UserName 策略如果服务器只开 Anonymous这里传new UserIdentity(new AnonymousIdentityToken())。3.2 读节点值NodeId、DataValue 与状态码连接建立后读值是最基础的动作。注意 ReadValue 返回的不是裸类型而是 DataValueNodeId nodeToRead new NodeId(demo.temperature, 2); // ns2, sdemo.temperature DataValue value await session.ReadValueAsync(nodeToRead); if (StatusCode.IsGood(value.StatusCode)) { double temperature (double)value.Value; // Variant 拆包 Console.WriteLine($温度{temperature}, 源时间戳{value.SourceTimestamp:O}); } else { Console.WriteLine($读取失败: {value.StatusCode}); }DataValue里有两个时间戳要分清SourceTimestamp 是源头设备产生该值的时间ServerTimestamp 是服务器收到并缓存的时刻。调试时发现值对但时间戳不对问题多半在服务器端的采样逻辑而不是客户端。value.Value的类型是 Variant强转前最好先判断value.Value is double因为地址空间里的 DataType 可能被服务器声明成 Float拆出来就是 float。3.3 写节点值WriteValue 的参数与返回码对照写值和读值对称但参数经常被漏OPC UA 规范要求写入时带上时间戳不带的老写法在部分严格的服务端会被拒。DataValue writeValue new DataValue { Value new Variant(26.5), ServerTimestamp DateTime.UtcNow, // 写入到达服务器的时间 SourceTimestamp DateTime.UtcNow // 值产生的时间 }; StatusCode result await session.WriteValueAsync( new NodeId(demo.temperature, 2), writeValue); Console.WriteLine(StatusCode.IsGood(result) ? 写值成功 : $写值失败: {result});写值失败时状态码能直接定位问题下面四类最常见StatusCode含义常见原因BadNodeIdUnknown节点不存在ns 索引写错或标识符字符串不匹配BadNotWritable节点不可写服务端 AccessLevel 未包含写权限BadAccessDenied鉴权失败用户名密码不正确或令牌策略不匹配BadOutOfRange值越界超过 DataType 定义范围或服务器自定义限制提示用 UaExpert 能连、自己的代码连不上时先在 UaExpert 里试一次写值。它返回的状态码比调试器直观BadNotWritable 和 BadAccessDenied 一眼就能区分。3.4 订阅值变化MonitoredItem 的四个关键参数轮询读值在点数少的时候没问题点数上千后效率会很难看。OPC UA 的推荐做法是订阅。订阅涉及两级定时器最容易混淆Subscription subscription new Subscription(session) { PublishingInterval 1000, // 服务器发布周期的间隔毫秒 LifetimeCount 30, // 连续丢心跳超过该值订阅被删除 KeepAliveCount 10, // 每 N 个发布周期发一次心跳 MaxNotificationsPerPublish 100 }; session.AddSubscription(subscription); subscription.Create(); MonitoredItem item new MonitoredItem(subscription.DefaultItem) { DisplayName 温度监视, StartNodeId new NodeId(demo.temperature, 2), AttributeId Attributes.Value, SamplingInterval 500, // 采样间隔应小于发布周期的一半 QueueSize 10, // 采样值队列深度 DiscardOldest true // 队列满时丢弃最旧值 }; item.Notification (monitoredItem, args) { if (args.NotificationValue is MonitoredItemNotification notification) Console.WriteLine($新值{notification.Value.Value}); }; subscription.AddItem(item); subscription.ApplyChanges();PublishingInterval 是服务器把变化数据打包发出的节奏SamplingInterval 是服务器读底层数据源的频率两者完全不同。典型误区是 SamplingInterval 设得比 PublishingInterval 还大结果一半周期在发旧值。QueueSize 为 0 表示由服务器决定默认深度设为 10 并配合 DiscardOldesttrue适合高速变化但客户端处理慢的场景。KeepAliveCount 乘上 PublishingInterval 就是心跳超时LifetimeCount 一般建议不小于 KeepAliveCount 的 3 倍否则网络抖动几次订阅就被服务器回收了。4. OPC UA 通用架构落地配置分离、证书与安全策略的坑4.1 把 DEMO 拆成三层避免所有逻辑堆在 NodeManagerDEMO 之所以叫通用架构是因为它的组织方式可以直接套到真实项目。我一般会在此基础上再拆一层传输层StandardServer 子类、ApplicationConfiguration 的加载和证书初始化整个项目只有一份。领域层每个设备类型一个 NodeManager 子类比如 PlcNodeManager、MeterNodeManager各自管理自己的节点集合互不引用。接入层客户端、OPC UA 转 MQTT 的桥接服务、Web API都通过 Session 访问领域层不直接碰协议栈内部对象。这个切法的直接收益是替换设备协议时不用动传输层。比如从 Modbus 换成协议转换网关只需要改领域层里数据更新的代码Client 和订阅逻辑原封不动。OPC UA 模拟 Kepware这类需求也是这样落地的用同一个 NodeManager 把点表模拟出来Kepware、WinCC 或其他客户端来连时看到的地址空间完全一致。4.2 安全策略与证书信任连接失败七成在这OPC UA 连接失败里证书问题占大头。先说安全策略DEMO 里通常会同时开放以下几档安全策略签名加密适用场景None无无开发调试、纯内网演示Basic128Rsa15有有老设备兼容1.04 后不建议新用Basic256有有过渡策略兼容性尚可Basic256Sha256有有规范推荐的最低强制策略上线首选较新的 1.5.x 版本还补了 Aes128Sha256RsaOaep 等策略但很多第三方服务器和 PLC 网关还没实现联调时优先 Basic256Sha256 最稳。策略决定后剩下的坑全在证书交换。开发期最快的流程是服务器和客户端都生成自签名证书把对方的证书从 rejected 目录移进 trusted 目录或者直接写一段开发期放行的校验逻辑config.CertificateValidator.CertificateValidation (sender, e) { if (e.Error.StatusCode StatusCodes.BadCertificateUntrusted) { // 仅限开发环境上线必须删除 e.Accept true; } };这段逻辑挂在客户端配置的 CertificateValidator 上收到未信任证书时直接放行并打印证书指纹到日志。上线前把e.Accept true删掉改回拒收然后在双方 Trusted 目录里互放对方证书。常见手误是只加了服务器信任客户端忘了把服务器证书加入客户端信任列表症状是客户端报 BadCertificateUntrusted服务器端日志却干干净净。4.3 配置外置appsettings.json 管理端点、超时与安全策略DEMO 里端点写在 XML 或硬编码在 Main 里真实项目要支持多现场部署配置必须外置。用 .NET 的 options 模式接 appsettings.json 是常见做法public class OpcUaOptions { public string EndpointUrl { get; set; } opc.tcp://localhost:48010; public string SecurityPolicy { get; set; } Basic256Sha256; public int OperationTimeoutMs { get; set; } 15000; public int SessionTimeoutMs { get; set; } 60000; public string UserName { get; set; } public string Password { get; set; } public string NodePrefix { get; set; } demo; // 节点标识符前缀 }{ OpcUa: { EndpointUrl: opc.tcp://192.168.1.50:48010, SecurityPolicy: None, OperationTimeoutMs: 15000, UserName: operator, Password: *** } }加载后把 EndpointUrl 传给 SelectEndpoint把 SecurityPolicy 映射到请求的安全策略枚举NodeId 的 ns 索引由 NamespaceUri 解析而不是硬编码——这一点第 5 章细说。配置外置的另一个好处是现场联调不用重新编译程序安全策略切 None 调试、切 Basic256Sha256 上线改配置文件即可。注意密码放 appsettings.json 只是权宜之计生产环境建议用环境变量或密钥管理服务覆盖配置文件里明文密码会被抓配置的人直接看到。5. 用 UaExpert 和 Node-RED 验证 DEMO把 OPC UA 数据送到 MQTT5.1 UaExpert 三步连上 DEMO Server服务器和客户端都能跑之后还需要一个中立视角验证地址空间。UaExpert 是 Unified Automation 出的免费 OPC UA 客户端也是工控调试的标配。连接只要三步左侧 Servers 面板右键 Add Server填opc.tcp://localhost:48010双击服务器节点建立会话在 Address Space 面板展开 MyObjects找到温度节点拖到右侧 Data Access View。此时能看到实时值和时间戳用客户端写值或修改服务器端模拟逻辑UaExpert 里应该跟着变——这能同时验证地址空间、订阅和写值三条链路。5.2 用 Node-RED 把 DEMO 的 OPC UA 数据转成 MQTTOPC UA 转 MQTT 是边缘网关的常见需求Node-RED 里用 node-red-contrib-opcua 插件就能实现完整链路是UA-.NETStandard 的 DEMO Server 当数据源Node-RED 的 OPC UA 客户端节点订阅 demo.temperature再把值交给 MQTT out 节点发布。关键参数有两个。OPC UA client 节点的 Polling interval 默认 1000 毫秒如果上位机侧需要更快的刷新改成 200 并与 Server 端的 SamplingInterval 匹配否则会白等一个周期。MQTT 节点的 QoS 在工厂内网用 0 即可跨公网或弱网环境才需要 1QoS 2 在 OPC UA 场景基本用不上。topic 建议按plant/{device}/{tag}的格式组织比如plant/ea/demo.temperature这样后续接时序数据库或消息队列都不用再改订阅端。5.3 一个必须养成的习惯用 NamespaceUris 动态取 ns 索引DEMO 里的代码大量使用new NodeId(demo.temperature, 2)数字 2 看着没问题但它依赖服务端的命名空间顺序。真实项目里服务器可能升级、点表合并ns 索引一旦漂移满屏 BadNodeIdUnknown。正确做法是按 NamespaceUri 解析索引string nsUri urn:demo:opcua; int nsIndex session.NamespaceUris.GetIndex(nsUri); if (nsIndex 0) throw new Exception($服务器未注册命名空间: {nsUri}); NodeId node new NodeId(demo.temperature, nsIndex); DataValue value await session.ReadValueAsync(node);GetIndex 会用服务器返回的命名空间表反查 URI找到再拼 NodeId。把 NodeId 的构造统一收敛到一个工厂方法里服务器端点点改名称时只有工厂方法需要动。更进一步客户端可以遍历地址空间按 BrowseName 查找节点再构建 NodeId这样即使服务器端的命名空间 URI 和标识符都变了客户端代码也能自适应——这也是为什么说通用架构的客户端应该面向地址空间编程而不是面向写死的 NodeId 编程。本文还有配套的精品资源点击获取
返回列表