简介面向需要在 Visual Studio 2012 中使用 VB.NET 开发 OPC UA 客户端的开发者与工业自动化初学者示例项目演示了从配置服务器地址、设置安全策略到建立连接、创建会话、浏览节点、读取及写入设备数据、异常处理和释放资源的完整流程能帮助读者快速了解 OPC UA 的通信机制与 SDK 调用方式。压缩包共 55 个文件大小 2.67MB除可直接运行的 4 个 exe 和 9 个 dll 运行库外还包含 6 个 VB 源码含 Form1.vb 主窗体逻辑、4 个文本说明、4 个 XML 配置、3 个 config 配置以及 sln/suo/vbproj/resources/pdb 等工程辅助文件解压后可直接用 VS2012 打开与定位关键模块。源码中预留了安全策略、节点 ID 等关键参数的设置位置并附带注意说明文本便于对照学习排查连接失败、会话超时等常见问题Form1.vb 中的中文注释也有助于降低初学者的上手难度。目前已有 1389 人学习下载适合作为入门参考或后续二次开发的代码模板。1. OPC UA 客户端 vb.net 编写别急着写代码先弄清它解决什么问题很多工控从业者拿到“OPC UA 客户端 vb.net 编写”这个需求时第一反应是搜一个示例代码然后抄进去。但真正做过几个项目之后你会发现OPC UA 客户端最难的不是连接和读写那几个 API而是搞清楚数据模型、安全策略、订阅生命周期和断线重连。这些事没想明白代码写得再快上了现场一样翻车。本文要讲的是用 VB.NET 从零搭一个可用的 OPC UA 客户端从协议选型、官方库的选择到连接、读写、订阅、断线重连再到排查现场最常见的几个坑。我默认你用的是 Visual Studio 2019 或 2022、.NET Framework 4.8 或 .NET 6/8且现场有一个可用的 OPC UA 服务器比如某公司的模拟服务器或某设备厂商的软 PLC。如果你是第一次接触 OPC UA这篇文章能帮你少走一个月的弯路如果你已经在用别的语言写过客户端也能从 VB.NET 的实现细节里找到对照。先说结论VB.NET 写 OPC UA 客户端完全可行官方库对 .NET 生态支持很好绝大多数代码和 C# 版一一对应。真正决定项目成败的往往是你对 UA 模型和通信行为的理解程度而不是语法本身。2. 从协议到代码OPC UA 客户端必须理解的四件事2.1 OPC UA 不是“加强版 OPC DA”它的通信模型完全不同很多从 OPC DA 转过来的工程师会下意识把 OPC UA 理解成“OPC DA 换个传输方式”于是写出类似 DA 时代的代码连接、读、写、断开。这个思路能用但会把 UA 最值钱的能力全部丢掉而且会在订阅、历史数据、复杂数据类型上撞墙。OPC UA 的核心不是“读一个变量”而是一套面向对象的信息模型。服务器的地址空间是一棵树树上每个节点都有 NodeId、BrowseName、DisplayName 和一组属性。你要读的“温度”不是一个简单的 tag 字符串而是一个带有类型、工程单位、数据质量、时间戳的节点。这个模型决定了客户端代码的结构先找到节点再操作节点而不是“按名字查值”。第二个关键是传输层。OPC UA 支持多种传输协议和编码方式常见的有 opc.tcp二进制、httpsXML 或二进制、WebSocket。VB.NET 客户端默认用 opc.tcp延迟低适合实时读写和订阅。如果现场有防火墙限制 4840 端口之外的通信才需要考虑 https 或反向代理。第三个关键是会话Session与安全SecurityPolicy。UA 的连接不是裸 TCP 长连接而是先通过 Endpoint 描述协商安全策略再建立 SecureChannel最后创建 Session。VB.NET 代码里你通常只需要设置 endpoint URL、安全策略、证书剩下由 SDK 处理。但你不能不懂这些概念否则遇到“连接被拒绝”“证书不受信任”会毫无头绪。第四个关键是订阅Subscription机制。UA 的订阅不是“定时轮询”而是由服务器在数据变化时主动推送。订阅里有 PublishingInterval、KeepAliveCount、LifetimeCount 三个参数它们共同决定推送频率和订阅存活时间。很多人刚写订阅时只设一个 PublishingInterval结果要么收不到数据要么会话莫名断开这就是参数理解不到位。2.2 选库官方 SDK 还是第三方库VB.NET 只有一个靠谱答案OPC UA 的官方 .NET SDK 是 OPC Foundation 维护的 OPC UA .NET Standard 库GitHub 上开源NuGet 包名是 OPCFoundation.NetStandard.Opc.Ua。这是当前 VB.NET 编写 OPC UA 客户端唯一值得认真考虑的库。它覆盖了客户端、服务器、证书管理、复杂类型解析、发布/订阅等几乎全部 UA 功能并且持续跟随 OPC UA 规范更新。另有一些第三方商业库也支持 .NET但要么需要付费授权要么某些高级特性比如聚合、方法调用、事件订阅支持不完整。对于绝大多数产线数据采集、设备监控、MES 对接场景官方库完全够用。我个人在模拟项目X里用官方库做过三个不同厂商服务器的客户端没有遇到必须上商业库的瓶颈。选择官方库还有一个现实理由社区资料足够多。C# 的示例到处都有VB.NET 的示例少一些但两类语言在 API 调用上几乎能逐行互译。你拿着 C# 示例改成 VB.NET 语法基本不会遇到库层面的障碍。NuGet 安装命令Install-Package OPCFoundation.NetStandard.Opc.Ua如果你用的是 .NET Framework 4.8还需要确认是否引入了 OPCFoundation.NetStandard.Opc.Ua.Security 和相关依赖包如果用的是 .NET 6/8官方库直接支持无需额外步骤。2.3 证书与安全策略现场 80% 连接问题的根源OPC UA 客户端连接服务器时默认会做双向证书校验。客户端需要持有一张应用证书服务器需要信任这张证书反过来客户端也要信任服务器证书。这个机制是 UA 和 DA 最大的体验差异——DA 时代没人管安全UA 时代证书不配好连都连不上。开发阶段最简单的做法是使用 SDK 自带的证书生成逻辑把证书放到本地证书存储里并在服务器端把客户端证书加入信任列表。VB.NET 代码里通常不需要手动生成证书SDK 会在首次创建 Application 时自动生成并持久化。安全策略SecurityPolicy的选择也很重要。None 表示不加密不签名Basic256Sha256 是当前最常用的加密签名组合。如果现场对数据安全要求不高、设备性能有限可以用 None如果对接的是新能源、医药、半导体这类合规要求高的产线至少要用 Basic256Sha256。有一点必须提醒不要把安全策略设为 None 就以为万事大吉。None 策略下 UA 的二进制协议仍然有基本的会话管理但数据明文传输且服务器可以选择拒绝无安全策略的连接。实际项目里我见过不少设备厂商把服务器配置成只允许 Basic256Sha256结果客户端这边用 None两端握手失败排查半天才发现是策略不匹配。2.4 地址空间的节点查找Browse 比直接 Read 更实用很多 UA 初学者拿到一个服务器第一件事就是拿已知的 NodeId 去 Read 节点值。但真实项目里你往往不知道 NodeId只知道节点名字或路径。这时必须用 Browse 服务去遍历地址空间找到目标节点再读取它。在 VB.NET 里Browse 的代码逻辑不复杂但要注意两点一是 UA 的地址空间是分层的根节点是 ObjectsFolderNodeId 是 Objects二是每个节点的 BrowseName 在同一个父节点下才是唯一的跨层级可能会有重名。所以查找节点时要么用 DisplayName 过滤要么用 ReferenceTypeId 限定层级关系。另一个实用手段是读取节点的 BrowseName、NodeClass、DataType、AccessLevel 等属性判断这个节点是变量、对象还是方法。这些属性决定后续能不能读值、能不能写值、能不能订阅。很多人在 Read 节点值之前不检查 AccessLevel结果代码执行时报 BadNotReadable实际上是权限问题而非连接问题。在动手写代码之前我强烈建议你先用一个现成的 UA 客户端工具比如某通用 OPC UA 客户端把现场服务器的地址空间看一遍确认目标节点的 NodeId、数据类型、读写权限。这个步骤能省掉大量调试时间也能让你写 VB.NET 代码时心里有底。3. 用 VB.NET 搭一个最小 OPC UA 客户端连接、读值、写值3.1 最小连接代码从创建 Application 到建立 Session用官方 SDK 写 OPC UA 客户端代码结构是固定的先创建 ApplicationConfiguration再创建 UAClient 或直接使用 Session。我在 VB.NET 项目里习惯把配置封装成一个函数方便多个连接点复用。下面是一段最简连接代码Imports Opc.Ua Imports Opc.Ua.Configuration Public Function ConnectToUaServer(endpointUrl As String) As Session 1. 创建应用配置指定应用名称和证书存储路径 Dim applicationName VbNetUaClient Dim application New ApplicationInstance(applicationName) Dim config New ApplicationConfiguration() config.ApplicationName applicationName config.ApplicationUri Utils.Format(urn:{0}:{1}, Utils.GetHostName(), applicationName) config.ApplicationType ApplicationType.Client 2. 加载或创建应用证书 Dim securityId New CertificateIdentifier() securityId.StoreType CertificateStoreType.Directory securityId.StorePath pki/client securityId.SubjectName CNVbNetUaClient config.SecurityConfiguration.ApplicationCertificate securityId 3. 注册并校验证书会自动生成证书文件 application.ApplicationConfiguration config application.CheckApplicationInstanceCertificates().Wait() 4. 创建客户端对象并连接 Dim client New UAClient() client.ApplicationConfiguration config client.Connect(endpointUrl, SecurityPolicies.Basic256Sha256) Return client.Session End Function这段代码里最值得留意的是第四步的 Connect。它内部做了端点探测、安全策略协商、证书交换、会话创建四件事。如果服务器要求匿名登录Connect 默认就能通过如果需要用户名密码需要在 Connect 之前设置 UserIdentity。按上面代码Connect 的第二个参数指定了安全策略。Basic256Sha256 是普适性最好的选择。如果服务器只支持 None可以把参数换成 SecurityPolicies.None。注意这里传入的是策略 ID不是字符串写错会直接抛异常。3.2 读点值ReadValue 与 ReadValues 的差别和使用场景连接建立之后读单个节点值是最基本的操作。UA SDK 提供了 ReadValue 和 ReadValues 两种方式。前者适合单个节点、对实时性要求不高的场景后者适合批量读取因为它在一次请求里携带多个节点减少了通信往返。单个读取的代码Dim nodeId As NodeId New NodeId(ns2;sChannel1.Device1.Temperature, 2) Dim value As DataValue client.Session.ReadValue(nodeId) If value.StatusCode.Code StatusCodes.Good Then Dim temp As Double CType(value.Value, Double) Console.WriteLine(温度值: temp.ToString(F2)) Else Console.WriteLine(读取失败状态码: value.StatusCode.Code) End If参数说明NodeId 的字符串格式是“ns2;s标识”ns 表示命名空间索引s 表示字符串标识符。不同服务器的 NodeId 格式可能不同有些用 i数字标识符有些用 gGUID。读取前最好先通过 Browse 确认。批量读取的代码Dim nodeIds As New List(Of NodeId)() nodeIds.Add(New NodeId(ns2;sChannel1.Device1.Temperature, 2)) nodeIds.Add(New NodeId(ns2;sChannel1.Device1.Pressure, 2)) nodeIds.Add(New NodeId(ns2;sChannel1.Device1.FlowRate, 2)) Dim results As List(Of DataValue) client.Session.ReadValues(nodeIds) For i As Integer 0 To results.Count - 1 If results(i).StatusCode.Code StatusCodes.Good Then Console.WriteLine(节点 {0}: {1}, nodeIds(i), results(i).Value) End If Next批量读取时要注意ReadValues 返回的结果列表顺序与请求列表一致但某个节点出错时它不会中断其他节点而是返回 Bad 状态码。所以遍历时一定要检查 StatusCode不能假设每个结果都有效。3.3 写点值WriteValue 的权限陷阱与数据类型匹配写操作和读操作在 API 结构上对称但坑更多。UA 的写权限由节点属性的 AccessLevel 决定而且服务器可能对写入值做范围校验、类型校验。最典型的翻车现场是代码里写入的是 Double服务器节点定义的是 Float某些服务器会隐式转换某些会直接拒绝还有的服务器要求 Value 属性带上精确的时间戳和状态码。推荐写法是构造一个 DataValue 并显式指定时间戳Dim nodeId As New NodeId(ns2;sChannel1.Device1.SetPoint, 2) Dim newValue As Double 35.5 Dim dv As New DataValue() dv.Value newValue dv.SourceTimestamp DateTime.UtcNow dv.StatusCode StatusCodes.Good Try Dim statusCode As StatusCode client.Session.WriteValue(nodeId, dv) If statusCode.Code StatusCodes.Good Then Console.WriteLine(写入成功) Else Console.WriteLine(写入被拒绝状态码: statusCode.Code) End If Catch ex As ServiceResultException Console.WriteLine(写入异常: ex.Message) End Try参数说明SourceTimestamp 用 UtcNow 而不是 Now是因为 UA 规范里时间戳统一使用 UTC虽然很多服务器不做强校验但规范一致性好的服务器会在校验严格时拒掉本地时间戳。StatusCode 设置为 Good 是表示“客户端认为这个值有效”如果服务器对数据质量敏感这个字段会影响它是否接受写入。写入前务必确认节点 AccessLevel 包含 Write 权限AccessLevel 的位标志里第 1 位表示可写。很多工程师栽在“能读但写不了”上排查半天发现是服务器端把节点设成了只读。4. 从轮询到订阅VB.NET 实现 OPC UA 数据变化推送4.1 订阅的三参数模型PublishingInterval、KeepAliveCount、LifetimeCount轮询适合低频采集但上位机做实时监控或历史存储时轮询会产生大量无效请求而且延迟不可控。OPC UA 的订阅机制把主动权交给服务器节点值一旦变化服务器就按你设定的采样间隔推送非常适合实时画面刷新和报警捕获。订阅包含两个层级Subscription订阅和 MonitoredItem监控项。一个订阅可以挂多个监控项每个监控项可以有自己的采样间隔和触发方式。创建订阅之前必须先理解三个参数PublishingInterval订阅的发布周期单位毫秒。服务器会在每个周期内检查所有监控项若发现变化就把变化数据打包推送。KeepAliveCount连续多少个发布周期内没有数据变化时服务器发送一个保活报文。如果客户端在这个周期数内没收到任何报文就会认为连接可能中断。LifetimeCount客户端允许的最大“失联”周期数。如果超出这个值服务器会删除订阅。一个典型设置PublishingInterval 10001 秒检查一次KeepAliveCount 55 秒一保活LifetimeCount 1010 秒超时后删订阅。如果你的现场网络抖动较大可以把 KeepAliveCount 调大一些避免因为瞬时丢包导致订阅被服务器删除。4.2 创建订阅并接收数据变化事件的完整代码在 VB.NET 里创建订阅核心是订阅数据变化事件 Notification 的处理。下面是一段完整代码Public Sub SetupSubscription(client As UAClient, nodeIds As List(Of NodeId)) Dim session client.Session 1. 创建一个订阅设置发布周期为 1000ms Dim subscription New Subscription() subscription.PublishingInterval 1000 subscription.KeepAliveCount 5 subscription.LifetimeCount 10 session.AddSubscription(subscription) subscription.Create() 2. 为每个节点创建监控项 For Each nodeId In nodeIds Dim item New MonitoredItem() item.StartNodeId nodeId item.AttributeId Attributes.Value item.SamplingInterval 500 item.QueueSize 1 item.DiscardOldest True 绑定数据变化事件 AddHandler item.Notification, AddressOf OnMonitoredItemNotification subscription.AddItem(item) Next 3. 应用所有监控项 subscription.ApplyChanges() End Sub Private Sub OnMonitoredItemNotification(item As MonitoredItem, args As MonitoredItemNotificationEventArgs) Dim notification TryCast(args.NotificationValue, MonitoredItemNotification) If notification Is Nothing Then Return Dim value notification.Value Console.WriteLine(节点 {0} 新值: {1} 时间戳: {2}, item.DisplayName, value.Value, value.SourceTimestamp) End Sub参数说明SamplingInterval 是服务器读取底层数据源的采样周期通常可以设得比 PublishingInterval 小这样服务器能捕捉到更细的变化。QueueSize 表示服务器为这个监控项缓存的样本数设 1 表示只保留最新值设更大值可以防止高速变化时丢中间值。DiscardOldest 表示队列满时丢弃最旧的数据。如果要做趋势分析或精确历史记录QueueSize 建议设 10 以上。还要注意订阅创建之后事件回调运行在 SDK 的内部线程池上不能在回调里做界面刷新或长时间阻塞操作。正确做法是把数据放到队列或缓冲区再由界面线程定时取用。4.3 触发模式与采样间隔不变化的节点为什么收不到数据很多人在测试订阅时遇到一个困惑节点值一直不变时客户端收不到任何数据。这其实不是故障而是触发模式在起作用。MonitoredItem 默认是 ReportingMode.Report 加 Trigger 由服务器按数据变化决定推送。如果服务器觉得值没变就不会推送只在 KeepAlive 报文里告诉你“连接还活着”。如果你的业务需要周期性读取节点的当前值即使值没变可以在监控项上设置 SamplingInterval 和 QueueSize并将 Trigger 模式设为数据变化加时间触发或者干脆牺牲一部分订阅优势回到定时 Read。实际项目里我会把“变化推送”和“周期读一次”混合使用关键报警点用订阅基础状态点用 5 秒一次轮询。这样既不漏报警也不至于让订阅包和回调过于拥挤。如果要监控一个节点的状态码是否异常、是否 Bad不要只订阅 Value 属性因为 StatusCode 可能会变化而 Value 不变。这时候需要额外创建一个 AttributeId 为 Attributes.StatusCode 的监控项单独处理。5. 避坑OPC UA 客户端 VB.NET 现场常见问题与排查记录5.1 连接超时但服务器明明在线现象代码执行到 Connect 时卡住或抛出超时异常但用服务器自带的诊断工具看服务器进程正常端口也通。原因最常见的是安全策略不匹配。服务器配置为只接受 Basic256Sha256客户端却用 None或者反过来。其次是证书校验失败服务器不信任客户端证书连接会在握手阶段被静默拒绝表现就是超时而不是明确报错。解决先检查 endpoint 的 SupportedMessageSecurityModes确认服务器支持哪些安全策略然后用调试模式启动服务器看拒绝日志。如果是证书问题把客户端证书一般在 pki/client/certs 目录下的 .der 文件导入服务器的信任证书列表再把服务器证书导入客户端的信任列表。开发阶段可以把证书校验临时设为 AcceptAll但生产环境千万不要这么做。5.2 订阅创建成功但回调一次都不触发现象订阅和监控项都创建成功服务器也不报错但事件回调从未执行。原因最常见的两个一是节点值的 StatusCode 不是 Good服务器按数据质量决定是否推送二是 Debug 阶段把 SamplingInterval 设成了 -1表示按服务器默认而服务器默认采样间隔非常长或者根本没有变化数据。另外如果你在 UI 线程里创建订阅并阻塞等待回调也可能因为线程池饥饿而不被调度。解决先用 ReadValue 确认节点当前值确实可读且状态码为 Good然后把 SamplingInterval 设置为 100 或 500 毫秒观察是否触发再检查回调中是否有异常被吞掉。在 VB.NET 里AddHandler 绑定的回调如果抛出未处理异常可能被线程池静默捕获建议在回调最外层包 Try-Catch 并输出日志。5.3 写值返回 BadOutOfService 或 BadUserAccessDenied现象写节点值返回 BadUserAccessDenied 或 BadOutOfService但读是正常的。原因前者是会话身份没有写权限。匿名会话许多服务器只给只读权限必须配置用户名密码或证书身份。后者通常是服务器端处于某种运行状态比如设备未运行或写操作被 PLC 程序锁定。解决先用带用户名密码的身份建立会话确认节点 AccessLevel 的 Write 位为 1。如果确认是服务器内部状态导致的 BadOutOfService就需要去服务器端往往是 PLC 程序开启写使能。不要试图在客户端绕过这个状态位这是硬性的安全设计。5.4 断线后重连失败原有订阅全部失效现象网络中断或服务器重启后客户端自动重连成功但回调不再触发或者重连过程反复失败。原因UA 的 Session 和 Subscription 都是有时效的。Session 断线后如果超过超时时间服务器会把它销毁原有 Subscription 随之失效。重连新建的 Session 没有携带旧订阅所以回调不触发。解决重连成功后必须重新创建订阅。我通常在客户端封装一个 ReconnectHandler连接恢复后执行 SetupSubscription 的全部逻辑。另外LifetimeCount 和 KeepAliveCount 不要设得太小否则一次短暂网络抖动就会让服务器销毁订阅重连后还得重新建。5.5 读取复杂数据类型时只拿到字符串或字节流现象节点是结构体或数组比如产线配方、诊断数据包读取结果不是预期对象而是字符串或 byte[]。原因UA 地址空间里复杂类型需要 DataTypeDefinition 参与解析。官方 SDK 默认只能解析内建类型自定义结构体要想读成强类型对象必须注册自定义 DataType。解决开发阶段先读取该节点的 DataTypeDefinition 和 DataType NodeId然后注册一个对应的 Encoder 或直接用 JSON 解码。VB.NET 里常见做法是先用 RawData 拿到 byte[]再用服务器端提供的 JSON Schema 反序列化。如果你不需要强类型对象也可以用 UA 的 JSON 编码方式直接读 JSON 文本现场调试时这个技巧很实用。6. 把客户端做成服务OPC UA 客户端 vb.net 的工程化收尾技巧把 OPC UA 客户端从“能跑”变成“能上线”需要处理几个容易被忽略的工程问题。第一个是证书的生命周期管理。开发阶段的证书如果设置了较长的有效期上线前要检查服务器是否已经信任它换证书时要同时在服务器和客户端更新否则线上会突然断连。我习惯在客户端启动时打印证书指纹方便和服务器端比对。第二个是把连接、读写、订阅封装成独立模块不要让 UI 直接碰 SDK。我的做法是做一个 UaClientService 类内部管理 Session 和 Subscription对外只暴露 Connect、Read、Write、Subscribe、Disconnect 五个方法所有事件通过回调向外传递。这样可以避免界面线程和 SDK 线程互相阻塞也方便做单元测试。第三个是日志与状态码追踪。UA 的 ServiceResultException 信息非常丰富包含 ServiceResult、SymbolicId、Namespace 等。调试时不要只打印 Message要把 SymbolicId 和 StatusCode 十进制值一起打出来。很多现场问题靠状态码能直接定位比如 BadSessionIdInvalid 表示会话已失效BadNodeIdUnknown 表示节点不存在。第四个是断线重连的指数退避策略。不要做固定间隔重试因为服务器刚重启时需要时间恢复固定重试可能造成风暴。我用的是 1 秒、2 秒、4 秒递增最多到 15 秒封顶重连成功后重新初始化订阅。最后再提一个我踩过多次的坑不要把 OPC UA 客户端进程做成无人值守的 Windows 服务却在里面写控制台输出。服务模式下控制台输出会被丢弃而所有注意事项打印到日志文件里你的现场运维同事会感谢你。每次写完一个 UA 客户端我都会用模拟服务器反复杀掉进程再重启验证重连逻辑是否稳定。这个习惯帮我避免了好几次上线的尴尬。希望这篇笔记对正要动手写 OPC UA 客户端的你能有些帮助。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?