ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OPC UA 1.03 客户端 C#/.NET Core 实战:连接、订阅与避坑

OPC UA 1.03 客户端 C#/.NET Core 实战:连接、订阅与避坑 简介OPC UAOPC统一架构是工业自动化与物联网领域通用的安全通信标准。这份C# OPC UA资源包面向使用.NET Core进行跨平台开发的工程师与学习者覆盖OPC UA 1.03版规范包含SDK以及服务端、客户端示例程序可解决从协议理解到实际数据交换读写变量、订阅变化、调用方法、报警与历史数据访问的落地问题。压缩包共195个文件以dll运行库与依赖组件为主另有exe示例程序、config配置文件、ico图标及xml文档等整体约15.06MB结构清晰适合直接部署或对照学习。示例涵盖锅炉系统模拟服务和标准参考服务端并配有多个客户端样本便于观察OPC UA通信的不同交互模式。资源已获557人学习下载适合需要快速搭建OPC UA测试环境、理解1.03版信息模型与安全模型或基于.NET Core研发工业互联应用的C#开发者参考使用。1. 从OPC UA 1.03说起为什么C# .NET Core 是 opcua客户端最顺手的技术栈做上位机或工业数据采集的人一抬头看到“OPC UA”这个协议第一反应就是“用C#写个 opcua客户端是不是最稳”。实际项目里也确实是C# 有官方维护的 OPC UA 库.NET Core 又能把采集服务直接部署到 Linux 工控机或容器里一套代码不用改就能连西门子、倍福、罗克韦尔这些老牌 PLC 的 OPC UA 服务器。更现实的是很多工厂里的服务器固件还停留在 OPC UA 1.03 规范版本比 1.04/1.05 少了 PubSub 和部分安全模式但基础的数据读写、订阅、历史访问全都够用。这个标题里的“覆盖1.03版”其实就是提醒你不要只对着新版规范写代码你要处理的是存量服务器。这篇笔记就把我从零搭一个 .NET Core OPC UA 客户端的步骤、参数和翻车记录一次说清新手能照抄熟手能直接看边界。2. 搭一个能跑的 OPC UA 客户端从 NuGet 选型到连上西门子 PLC 的三步2.1 选对库OPCFoundation.NetStandard.Opc.Ua 与其它库的差别常见做法是直接用基金会官方移植的OPCFoundation.NetStandard.Opc.Ua它在 NuGet 上能搜到支持 .NET Standard 2.0所以 .NET Core 3.1、.NET 5/6/8 都能引用。也有轻量级的开源库如OPCFoundation.NetStandard.Opc.Ua.Sample或第三方封装但官方库虽然 API 啰嗦一点协议覆盖最全遇到 1.03 服务器时兼容性最好。我一般会同时装上两个包OPCFoundation.NetStandard.Opc.Ua和OPCFoundation.NetStandard.Opc.Ua.Client。前者管协议栈和编码后者管 Session、Subscription 这些客户端抽象。别为了省事只引 Core 包否则你会发现自己手写 ReadRequest 和编解码那是踩坑的无底洞。ItemGroup PackageReference IncludeOPCFoundation.NetStandard.Opc.Ua Version2.0.0 / PackageReference IncludeOPCFoundation.NetStandard.Opc.Ua.Client Version2.0.0 / /ItemGroup版本号我故意写成 2.0.0实际你按当前 NuGet 最新稳定版用就行。这个库的命名空间是Opc.Ua和Opc.Ua.Client代码里 usings 别写错。引入后第一步是初始化ApplicationConfiguration这个配置在官方库里的地位相当于全局黑匣子证书、超时、传输配额全在它身上。using Opc.Ua; using Opc.Ua.Configuration; var config new ApplicationConfiguration { ApplicationName MyCollectionService, ApplicationUri urn:MyCollectionService, ApplicationType ApplicationType.Client, SecurityConfiguration new SecurityConfiguration { AutoAcceptUntrustedCertificates true, // 测试时先打开上线必须设为 false AddAppCertToTrustedStore true, ApplicationCertificate new CertificateIdentifier { StoreType Directory } }, TransportQuotas new TransportQuotas { OperationTimeout 10000, // 单次请求超时 10 秒 MaxBufferSize 65536, MaxMessageSize 4194304 }, ClientConfiguration new ClientConfiguration { DefaultSessionTimeout 60000 // 会话超时 60 秒 } }; await config.Validate(ApplicationType.Client);这段代码的坑在SecurityConfiguration。AutoAcceptUntrustedCertificates true只能放在测试环境生产环境你必须在证书校验事件里自己写信任名单。TransportQuotas里的OperationTimeout很多人不设结果服务器响应稍慢客户端就报 BadTimeout。后面排错会再展开。2.2 最小连接代码Endpoint、Session 和握手配置好了接下来要选 Endpoint。OPC UA 服务器一般暴露多个 Endpoint比如opc.tcp://192.168.1.10:4840后面可能带安全策略参数。你用CoreClientUtils.SelectEndpoint传一个 URL它会尝试拉取服务器 Endpoint 列表然后按安全策略从高到低排序返回一个合适的。using Opc.Ua.Client; var endpointUrl opc.tcp://192.168.1.10:4840; // 第二参数 useSecurity:false 表示优先选无加密策略 var endpointDescription CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity: false); var endpointConfig EndpointConfiguration.Create(config); var endpoint new ConfiguredEndpoint(endpointDescription, endpointConfig); var session await Session.Create( config, endpoint, updateBeforeConnect: false, sessionName: my-session, sessionTimeout: 60000, identity: null, // null 表示匿名身份 preferredLocales: null );这里identity: null是匿名连接很多 1.03 服务器默认允许匿名但西门子 PLC 的 OPC UA 服务器通常要用户名密码。这时你要改成new UserIdentity(user, password)。updateBeforeConnect建议设为 false否则每次重连它都重新拉 Endpoint 列表调试时会拖慢重连。连接成功后你拿到的session就可以发请求了。先做一个最简单的读点看看当前服务器时间// 读取服务器时间节点 var serverTimeNode new NodeId(Objects.Server_ServerTime); var result await session.ReadValueAsync(serverTimeNode); Console.WriteLine($Server time: {result.Value});Objects.Server_ServerTime是规范里固定节点 ID1.03 和 1.04 都一样。如果你连不上先不急着看业务代码把 Endpoint 拉出来看endpointDescription.EndpointUrl和SecurityPolicyUri多半是安全策略不匹配。2.3 读点、写点和订阅采集逻辑的三种基本姿势读点是ReadValueAsync写点是WriteValueAsync这俩在 Session 上直接有异步方法。但实际采集场景里轮询读 100 个点位用逐个ReadValueAsync是低效的。要用ReadValuesAsync或者构建一个ReadRequest批量读。1.03 服务器对单个请求的节点数有限制默认MaxNodesPerRead可能是 1000但稳妥点每批 500 个。var nodeIds new ListNodeId(); for (int i 0; i 500; i) { nodeIds.Add(new NodeId($ns2;sDB100.D{i:D4}, 2)); } var values await session.ReadValuesAsync(nodeIds); for (int i 0; i values.Count; i) { Console.WriteLine(${nodeIds[i]} - {values[i].Value}); }注意ReadValuesAsync返回的是DataValueCollection它不会因为某个节点失败就把整批丢掉单点失败时那个DataValue的状态码会标识错误。你做数据采集时一定要过滤StatusCode.IsBad否则会把错误状态当成真实值入库。订阅是 OPC UA 里最有价值的部分也是 1.03 版就支持的核心功能。它由服务器按周期间隔主动推送不用你轮询。下面是最小订阅代码var subscription new Subscription { PublishingInterval 500, // 服务器发布周期单位毫秒 KeepAliveCount 10, // 心跳包数量超过未收到就判断断线 LifetimeCount 60, // 订阅过期周期 }; session.AddSubscription(subscription); subscription.Create(); var monitorItem new MonitoredItem { StartNodeId new NodeId(ns2;sPLC1.Temperature), SamplingInterval 1000, // 采样周期只针对该节点 QueueSize 10, // 采样队列大小爆队列会丢数据 AttributeId Attributes.Value }; monitorItem.Notification (sender, e) { foreach (var notif in e.NotificationValue) { if (notif is MonitoredItemNotification min) { Console.WriteLine(${min.Value}); } } }; subscription.AddItem(monitorItem); subscription.CreateItems();这里的PublishingInterval是整个订阅的发布节奏SamplingInterval是单个节点的采样频率两者互相影响。你设 100ms 采样、500ms 发布实际客户端每秒最多收到 2 次最新值中间采样点会丢。这是很多人误以为“OPC UA 实时性差”的原因。2.4 参数速查表第一次配置别乱调参数推荐值说明OperationTimeout10000单请求超时长任务要加大SessionTimeout60000Session 空闲保护时间PublishingInterval500订阅发布周期实时性要求高设 100SamplingInterval1000节点采样别小于发布周期的一半QueueSize10队列深度突发数据量大时加到 50KeepAliveCount10重连判断网络抖动大就增大ConnectTimeout5000建连超时跨网段要加AutoAcceptCertificatesfalse生产环境必须关闭这些参数不是越大越好。KeepAliveCount太大会导致断线后重连变慢SamplingInterval小于PublishingInterval只会浪费 CPU。我的经验是先按表格设跑 30 分钟观察服务端日志再根据实际采集延迟微调。3. 覆盖 1.03 版的协议要点会话、节点、服务与数据模型3.1 1.03 版规范里我们真正要处理的三块差异1.03 版本对于客户端来说最明显的差异在三个方面安全策略的枚举值、UA Binary 编解码的命名空间版本、以及部分服务返回错误码的语义。比如SecurityPolicy里的Basic256Sha256在 1.03 的服务器里可能不支持你通过SelectEndpoint选了一个带加密策略的 Endpoint握手时服务器直接报BadSecurityPolicyRejected。另外1.03 的用户令牌类型只支持Anonymous和UserName证书令牌虽然规范写了但很多德国厂商的 PLC 实现得并不完整所以入门阶段别想着上证书认证。还有个容易踩的雷是NamespaceUri。OPC UA 标准类型节点的命名空间是http://opcfoundation.org/UA/但 1.03 和 1.04 在某些扩展对象类型上有细微差别。你如果在 Read 时拿到BadNodeIdUnknown先检查自己拼的 NodeId 格式特别是不带命名空间索引的ns0节点是不是写错了。3.2 从服务器读节点树遍历 Address Space 而不是猜节点很多新手连上服务器后第一件事就是问“西门子 PLC 的温度点在哪”。实际上 OPC UA 服务器会暴露自己的地址空间Address Space你可以通过Browse服务遍历出完整的节点树根本不用问厂家要节点表。下面这段代码能列出根节点的所有子节点var browseResult await session.BrowseAsync( new BrowseDescription { NodeId ObjectIds.ObjectsFolder, BrowseDirection BrowseDirection.Forward, ReferenceTypeId ReferenceTypeIds.Organizes, IncludeSubtypes true, NodeClassMask (uint)(NodeClass.Object | NodeClass.Variable), ResultMask (uint)BrowseResultMask.All }, null, null); foreach (var reference in browseResult.References) { Console.WriteLine(${reference.BrowseName} - {reference.NodeId}); }这里ObjectIds.ObjectsFolder是地址空间的根BrowseDirection.Forward表示往下找子节点ReferenceTypeId设为Organizes是因为服务器的对象组织关系都是从根节点用这个引用串起来的。你用这个命令跑一遍就能把 PLC 里的数据块、电机、温度表全列出来。注意Browse是支持分页的。服务器返回的BrowseResult里有一个ContinuationPoint字段如果你的节点数超过单次返回上限必须用BrowseNextAsync继续拉。1.03 服务器的单次限制常常是 1000但有些小型设备的实现比较抠只能返回 100。我做项目时先测试References.Count发现等于限制值就无脑接着调BrowseNext。3.3 数据订阅的内部逻辑 MonitoredItem 与 Publish 循环你在客户端创建订阅后实际本地的 Session 会维护一个 Publish 循环。这个循环周期性地向服务器发 PublishRequest服务器把有变化的 MonitoredItem 数据放在 PublishResponse 里回给客户端。1.03 服务器要求客户端必须按一定顺序发送 PublishRequest如果你长时间不发服务器会把订阅里的数据丢弃甚至关闭订阅。这就是为什么我前面提到KeepAliveCount和LifetimeCount不能乱调。当服务器等了很久没收到新的 PublishRequest它会先发一个 KeepAlive 响应如果再等超过LifetimeCount个周期就会删除订阅。客户端程序如果卡在某个同步 IO 上没跑消息循环订阅就断了。所以你的订阅回调里千万别放慢操作比如写数据库、调用 Web API。正确的姿势是把数据推到内存队列由后台消费者落库。我踩过好几次坑回调里直接同步写 SQLite结果写库耗时 20msPublish 循环被阻塞订阅差点断掉后来改成Channel缓冲才稳定。4. 参数配置与调优安全策略、超时重连与批量请求4.1 安全策略选型None、Basic256Sha256 与证书的边界开发初期用useSecurity: false连接到服务器不带加密的 Endpoint这是最快的跑通方式。但工业现场特别是 PLC 直连场景很多德国的安全审核要求强制启用加密。这时你就要把SelectEndpoint的第二个参数改成true让库自动选择服务器支持的最高级安全策略。var endpointDescription CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity: true);用加密后证书校验就成了最大门槛。1.03 服务器通常用自己的自签名证书客户端第一次连接时会收到服务器的证书你必须在CertificateValidation事件里决定是否信任。官方库默认是拒绝的如果你直接调用Create会抛ServiceResultException错误码是BadCertificateUntrusted。调试阶段简单处理config.CertificateValidator.CertificateValidation (sender, evt) { evt.Accept true; // 生产环境不要这么写 };正式环境你应该evt.Accept evt.Certificate.Subject.ToString().Contains(PLC-01)或者把证书指纹白名单写进配置。只开放自己服务器的证书别全信任。4.2 超时与重连服务器重启后客户端怎么自救OPC UA 客户端最容易翻车的地方就是重连。服务器重启、网络闪断、防火墙断开长时间空闲连接都会让 Session 死掉。官方库的Session对象实现了断线自动重连但默认参数偏保守我的经验是手动接管。session.KeepAlive Session_KeepAlive; session.ReconnectComplete Session_ReconnectComplete; private static void Session_KeepAlive(Session session, KeepAliveEventArgs e) { if (e.Status ! null ServiceResult.IsBad(e.Status)) { Console.WriteLine($KeepAlive failed: {e.Status}); // 这里触发手动重连逻辑 } }KeepAlive事件会在连接异常时触发但不会帮你重连。如果你发现e.Status是BadConnectionClosed可以尝试调用session.Reconnect()试试。有些服务器的 TCP 链路还活着直接 Reconnect 能恢复有些已经彻底断开你只能重新Session.Create。我的做法是维护一个CancellationTokenSource一旦收到 Bad 状态创建一个新 Session然后重新订阅所有 MonitoredItem。注意新 Session 不会携带旧订阅必须重建。这里有 3 个坑一是节点集不变但订阅的句柄变了必须重新AddItem后再CreateItems二是旧 Session 要 Dispose三是重连期间采集数据会丢你要在业务侧做数据连续性补偿。4.3 批量数据请求的三种有效姿势读取几百个点位时ReadValuesAsync是最直接的批量读。但更高效的方式是ReadRequest 属性读取比如同时读 Value、Timestamp、StatusCode。官方库提供了ReadValueAsync的单点快速通道但批量时一定要手动构造请求。var readValueCollection new ReadValueIdCollection(); foreach (var nodeId in nodeIds) { readValueCollection.Add(new ReadValueId { NodeId nodeId, AttributeId Attributes.Value, DataEncoding null }); } var readRequest new ReadRequest { RequestHeader new RequestHeader { Timestamp DateTime.UtcNow }, NodesToRead readValueCollection }; var readResponse await session.ReadAsync(readRequest);这个请求会一次性返回所有节点的DataValueCollection。请求头里的Timestamp用来记录客户端发起时间方便服务器做超时判断。如果返回的Results数量和你请求的数量不一致或者某个节点状态码是BadWaitingForInitialData说明这个节点在服务器端还没准备好比如 PLC 里该数据块未启动。订阅上的批量请求同样存在。MonitoredItem可以一次添加多个节点但建议按采集频率分组高速变化量放一个订阅低速状态量放另一个订阅这样调参数时不互相牵制。5. 避坑清单覆盖 1.03 服务器时最常见的 4 个翻车现场5.1 现象连接到西门子 PLC 时报 BadSecurityPolicyRejected原因客户端通过SelectEndpoint选择了一个加密策略但 PLC 的 OPC UA 服务器不支持该策略或者客户端和服务器都支持但证书协商阶段失败。解决先降低标准用SelectEndpoint(endpointUrl, useSecurity: false)强制选无加密 Endpoint。确认能跑通后再升级到 Basic256Sha256。如果服务器端列表里有多个 Endpoint你可以在代码里过滤出指定安全策略的 Endpoint手动构造ConfiguredEndpoint不要把所有选择权都交给SelectEndpoint。5.2 现象连接成功但 Read 返回 BadNodeIdUnknown原因你拼的 NodeId 命名空间ns写错了或者服务器使用的命名空间索引不是 2。1.03 服务器的命名空间表顺序可能与新版服务器不同。解决用 Browse 遍历节点树打印每个节点的NodeId.ToString()和BrowseName。在代码里不要硬编码ns2;sPLC1.Temperature改成从配置读节点名连接后先把服务器命名空间表拉取出来var namespaceTable session.NamespaceUris; Console.WriteLine(string.Join(\n, namespaceTable));这样你能看到命名空间索引对应的 URI比如http://siemens.com/simatic/plc/1500在索引 1 而不是 2。拼 NodeId 时就写new NodeId(sPLC1.Temperature, (ushort)nsIndex)。5.3 现象订阅创建成功但 Notification 永远不触发原因SamplingInterval设置为 0很多服务器会按最快速度采样并发大量数据然后你的 QueueSize 又设 1数据还没回调就被覆盖了。更常见的是PublishingInterval设太大比如 10000ms你等 10 秒自然觉得没反应。解决先确认订阅和 MonitoredItem 的句柄状态。打印subscription.CurrentPublishingInterval和monitorItem.SamplingInterval看有没有被服务器强行调整。有些服务器会忽略小于最小周期的值并把实际值改到最小允许值。把采样间隔设为 1000发布间隔设为 500回调里先打印不落库看通不通。另外事件签名注意MonitoredItemNotification要强转有些示例代码用的是NotificationMessage那是订阅整体通知接口不是单点通知。用我第 2 章里写的monitorItem.Notification就不会漏。5.4 现象.NET Core 在 Linux 上找不到证书存储原因官方库默认把证书放在 Windows 证书存储位置Linux 上CertificateIdentifier的StoreTypeDirectory时默认路径可能是空的或权限不足。你在 Windows 上跑得好好的部署到 Ubuntu 容器里一启动就报证书无法加载。解决显式指定证书路径并确保目录存在var certPath Path.Combine(AppContext.BaseDirectory, pki); Directory.CreateDirectory(Path.Combine(certPath, own)); Directory.CreateDirectory(Path.Combine(certPath, trusted)); SecurityConfiguration new SecurityConfiguration { ApplicationCertificate new CertificateIdentifier { StoreType Directory, StorePath certPath, SubjectName CNMyOpcUaClient } }然后给应用容器挂载该目录的写权限。如果用 Docker加chmod或volume。这个坑没有捷径关键是要让客户端能保存自己生成的证书才能与服务器做信任交换。6. 进阶调试技巧用 UAExpert 对照报文把乱掉的节点和数据对齐6.1 用 UAExpert 和抓包做客户端行为校准UAExpert 是免费的 OPC UA 客户端调试工具Windows 和 Linux 都有安装包。它本身也是一个 OPC UA 客户端你可以用它连你的目标服务器看看服务器返回的正确节点树、安全策略、命名空间索引。然后回到你的 C# 代码里把 UAExpert 显示的ns2;sPLC1.Temperature直接复制到代码里排除自己拼节点拼错的低级问题。更高级的用法是开 Wireshark 抓opc.tcp端口的报文。UAExpert 连接时发的GetEndpoints、OpenSecureChannel、CreateSession报文和你代码里发的可以对照。如果你代码里某个请求返回BadServiceUnsupported抓包能看出是不是请求头里的 RequestTypeID 不对。6.2 自定义 DataType 的处理把结构体字节数组解出来1.03 服务器的自定义结构体在客户端读出来的通常是一个ExtensionObject或byte[]。很多设备把模拟量、状态字打包在一个结构体里一次性发布。你要做的是读取它的Body属性按设备的二进制协议自己解析。我没有背书只能给你一个常见做法先拿到ExtensionObject的TypeId再去服务器的地址空间里 Browse 这个类型节点看它的字段定义。if (value.Value is ExtensionObject extObj) { var body extObj.Body; if (body is byte[] bytes) { // 按设备文档解析字节 var temperature BitConverter.ToSingle(bytes, 0); var status bytes[4]; } }6.3 从 1.03 平滑升级的路线别等服务器升级先在客户端做兼容最后说一个实际项目里的教训现场设备服务器可能只有 1.03但新设备是 1.05。你的客户端不要绑定单一版本而是每次都通过Endpoint.Description.ProtocolVersion检查服务器版本然后按版本选择不同的安全策略和订阅参数。例如 1.03 服务器对Publish的请求频率敏感就把PublishingInterval调大到 10001.05 服务器支持 PubSub你就可以考虑大数据量走 MQTT 而不走订阅。我的习惯是维护一个枚举enum OpcUaVersion { Legacy103, V104, V105 }连接后根据服务器返回的协议版本决定行为。Session.Create的 response 里有ServerProtocolVersion字段取出来判断即可。这样即使工厂里新旧服务器混杂你也不必急着要求 PLC 厂家升级固件。希望这篇踩坑记录能帮你在 OPC UA 客户端这条路上少走几趟弯路至少我下次再遇到 1.03 的服务器时拿到手里的就是现成的调试路径。本文还有配套的精品资源点击获取
返回列表