简介:这是一份面向工业自动化开发者与OPC UA初学者的VB版OPC UA客户端示例压缩包。包内包含完整的VB工程源码,以及配套的OPC Helper 2.02工具,后者可直接连接OPC服务器执行数据读写,便于新手快速上手。压缩包共181个文件,以dll动态库为主,另有vb源码文件、xml配置、exe可执行程序、pdb调试符号等资源,整体大小15.26MB,目录结构较清晰。目前已有481人学习下载。通过学习这份示例,开发者可以深入掌握OPC UA客户端的关键实现环节:配置连接参数与安全证书、浏览服务端节点与信息模型、创建订阅并处理数据变更事件、执行单值与数组读写、处理通信异常;同时还能了解如何在VB中使用异步和多线程保证程序响应性能,并借助SDK封装库快速搭建测试环境,验证与不同品牌OPC服务器的互通性,为实际工业现场设备集成与上位机软件研发提供坚实参考。
1. 拿到 OpcUaClient_VB.zip,先搞清楚这个包解决什么问题
一个很常见的现场画面:产线上跑数据采集的程序还是 VB 写的,上位机想直接读设备的 OPC UA 服务器里的温度、电流、产量,但不想把 UA 协议当黑匣子,只想拿到一份能改、能调、能跟着项目走的客户端源码。OpcUaClient_VB.zip 这个包,目标就是把 UA 客户端用 VB 语言落地的工程示例:里面通常包含建立会话、按节点读取、触发订阅这类最小骨架,适合做设备数据采集、产线看板、老系统改造这些对接场景。它解决的是“VB 程序怎么和 UA 服务器说上话”这件事,适合两类人:一类是用 VB.NET 做上位机但没写过 UA 的工程师,另一类是拿到别人遗留工程需要迁移、换服务器、加节点的维护者。
2. 用 VB 做 OPC UA 客户端:三条实现路线与工程结构判断
2.1 三条路线的取舍
VB 生态里做 UA 客户端,我见过的主要有三条路,选错路会导致后面所有代码都白写,先把它们的边界说清楚。
第一条是直接用 UA 标准库的 .NET 实现,在 VB.NET 工程里通过 NuGet 引用官方 UA 客户端库。这条路线最稳,协议覆盖全,安全策略、订阅、方法调用都齐全,缺点是你得接受异步 API 和配置结构,上手有学习成本。绝大多数场景我建议走这条,因为后续换服务器、升级协议版本都有保障,不会被人卡住。
第二条是走第三方 ActiveX/COM 封装,VB6 也能用。这条路的优势是老工程改动小,VB6 里直接 CreateObject 就能用,但它通常只封装了 UA 的部分特性,很多服务器的自定义数据结构和历史数据接口根本调不到,遇到问题你只能等封装方更新,基本是个黑匣子。我一般只在没法升级的 VB6 系统里才用它,且会尽量在外面套一层网关。
第三条是用 UA 到 HTTP/REST 的映射网关,让 VB 程序只发 HTTP 请求,由网关去和 UA 服务器沟通。这适合快速联调或跨部门协作,缺点是延迟高、断线重连逻辑要自己写,现场长期跑容易出幺蛾子。三者的选择其实就一句话:新工程用标准库,老 VB6 用兼容层,临时演示用网关。
| 路线 | 适用 VB 版本 | 优点 | 主要坑 |
|---|---|---|---|
| UA 标准库 .NET 实现 | VB.NET(Framework 或 Core) | 协议覆盖全,社区资料多 | 异步 API 和配置结构稍复杂 |
| 第三方 ActiveX/COM 封装 | VB6 / VB.NET | 老工程改动小 | 特性不全,升级看厂商 |
| UA→HTTP/REST 网关 | 任意 | 联调快 | 延迟高,断线处理要自己做 |
2.2 解开 zip 先看什么:判断工程类型与依赖
拿到 OpcUaClient_VB.zip,我建议先别急着编译,先花两分钟判断这是个什么工程,避免在错误的前提上浪费时间。用命令行解开压缩包,再看文件名,比直接双击工程文件更直观。
mkdir -p unpack unzip OpcUaClient_VB.zip -d unpack find unpack -maxdepth 2 -type f | sort | head -60这个命令把压缩包解开到 unpack 目录,再列出前两层的文件。逻辑上,后缀名是最关键的信号:看到 .sln 和 .vbproj 说明是 VB.NET 工程;看到 .vbp 说明是 VB6 工程。依赖方面主要看有没有 packages.config、packages 目录或者本地 DLL 目录,这决定了你编译时是自动还原 NuGet 包还是要手动指定引用路径。参数上,maxdepth 2 是为了不把 bin 和 obj 里的编译产物刷屏,如果你发现目录很深,把 2 改成 3 再看。
这时候还要做一步:用文本编辑器打开 .vbproj 或 .vbp,确认 TargetFramework。如果是旧的 .NET Framework 4.x,那大概率依赖本地 DLL;如果是 SDK 风格工程,那依赖会写在 PackageReference 里。这一步决定了你接下来是改代码还是先解决环境。我见过有人拿到的工程是 VB.NET 写的,却按 VB6 的 COM 思路去找调用方式,方向一错就是整个周末搭进去。
3. 最小可用客户端:连接、读点、写点三段代码跑通
3.1 建立会话:配置、选端点、连接
UA 客户端的第一步不是读数据,而是建立一个应用身份并和服务端协商会话。常见做法是先写一个 ApplicationConfiguration,把应用证书、受信列表都指向本地目录,再用 SelectEndpoint 自动选一个可用端点。
Imports Opc.Ua Imports Opc.Ua.Client Public Function CreateUaSession(endpointUrl As String) As Session Dim appCfg As New ApplicationConfiguration() appCfg.ApplicationName = "VB_Ua_Client" appCfg.ApplicationUri = "urn:localhost:VB_Ua_Client" appCfg.SecurityConfiguration = New SecurityConfiguration() With { .ApplicationCertificate = New CertificateIdentifier() With { .StoreType = "Directory", .StorePath = ".\Certs\Application", .SubjectName = "CN=VB_Ua_Client" }, .TrustedPeerCertificates = New CertificateTrustList() With { .StoreType = "Directory", .StorePath = ".\Certs\Trusted" }, .RejectedCertificateStore = New CertificateTrustList() With { .StoreType = "Directory", .StorePath = ".\Certs\Rejected" } } appCfg.CertificateValidator = New CertificateValidator() appCfg.CertificateValidator.Update(appCfg.SecurityConfiguration) Dim endpoint As EndpointDescription = CoreClientUtils.SelectEndpoint(appCfg, endpointUrl, useSecurity:=False) Dim session As Session = Session.Create(appCfg, endpoint, "VB_Session", 60000, Nothing, Nothing).GetAwaiter().GetResult() Return session End Function这段代码的逻辑是先把 UA 库需要的基础配置铺好:StoreType 指定证书用什么形式存储,Directory 就是文件夹;StorePath 里的 Certs\Application 放应用自己的证书,Trusted 放服务端证书,Rejected 放被拒的。然后 CertificateValidator.Update 把这些配置绑到验证器上,没有这一步,连接时服务端证书校验会直接报错。SelectEndpoint 的第三个参数 useSecurity:=False 表示先不强制使用安全策略,联调阶段这样能省掉很多证书交互的困扰;Session.Create 的第四个参数 60000 是会话超时毫秒数,意思是服务端认为客户端失联多久之后可以自动清理这个会话。
要提醒的是,Session.Create 本身是异步的,这里用 GetAwaiter().GetResult() 是为了在控制台示例里同步拿结果。正式上位机里不建议这么写,界面线程会卡死,放后台线程或直接用 Async/Await 才是正路。第一次跑通后,建议把 useSecurity 改成 True,因为很多现场服务器默认拒绝无安全策略的会话。
3.2 读一个模拟量:NodeId 与返回值处理
会话建好后,读一个模拟量核心就三件事:构造 NodeId、发起 Read 请求、检查 StatusCode。NodeId 是 UA 世界里变量的“门牌号”,写错一格都读不到。
Dim nodeId As New NodeId("ns=2;s=Channel1.TagValue") Dim readValue As New ReadValueId() readValue.NodeId = nodeId readValue.AttributeId = Attributes.Value Dim readRequest As New ReadRequest() readRequest.NodesToRead.Add(readValue) Dim session As Session = CreateUaSession("opc.tcp://192.168.1.10:4840") Dim response = session.Read( Nothing, 0, TimestampsToReturn.Both, readRequest.NodesToRead).GetAwaiter().GetResult() If response.Results(0).StatusCode.Code = 0 Then Dim dataValue As DataValue = response.Results(0) Console.WriteLine( $"值={dataValue.Value} 质量={dataValue.StatusCode} " & $"时间={dataValue.SourceTimestamp}") Else Console.WriteLine($"读失败: {response.Results(0).StatusCode}") End IfNodeId 的字符串格式值得细说:ns=2 是命名空间索引,s=Channel1.TagValue 是标识符。不同服务器的 ns 不同,有的设备 ns=2 是数据区,有的 ns=3 才是,你需要用 UA 浏览器或者服务端文档确认,不能想当然。把 NodeId 和 AttributeId 分开填是为了将来读取其他属性做准备,比如你要读 EngineeringUnits 或 Description,只需要换 AttributeId,不必改请求结构。Read 方法里第二个参数 0 是请求超时,0 表示用服务端默认;TimestampsToReturn.Both 表示同时返回数据源时间戳和服务端时间戳,这在排查数据是否更新时非常有用。
代码里判断 StatusCode.Code = 0 是关键一步,UA 协议里 0 代表 Good,其他值都是各种坏质量。新手最容易犯的错是只看 dataValue.Value 是否为 Nothing,忽略了 StatusCode。现场很多模拟量在设备停机时质量会变成 BadWaitingForInitialData,这时候 Value 是 Nothing 是正常的,不能当 bug 处理。
3.3 写变量与订阅变化通知
读通了再聊写变量和订阅。写变量的套路和读差不多,构造 WriteValue,带上新值,调用 Write;订阅则是把“轮询”变成“推送”,服务端按采样间隔检测变化,变化了才通知客户端。
Dim writeValue As New WriteValue() With { .NodeId = nodeId, .AttributeId = Attributes.Value, .Value = New DataValue(42.0) } Dim writeResponse = session.Write(Nothing, {writeValue}).GetAwaiter().GetResult() If writeResponse.Results(0).Code = 0 Then Console.WriteLine("写点成功") Else Console.WriteLine($"写点失败: {writeResponse.Results(0)}") End If Dim subscription As New Subscription(session) With { .PublishingInterval = 1000, .LifetimeCount = 60, .KeepAliveCount = 10 } Dim monitoredItem As New MonitoredItem(subscription) With { .StartNodeId = nodeId, .SamplingInterval = 500, .QueueSize = 10, .DiscardOldest = True } AddHandler monitoredItem.TagChanged, Sub(item, args) Console.WriteLine( $"订阅值={args.ItemValue.Value} " & $"时间={args.ItemValue.SourceTimestamp}") End Sub subscription.AddItem(monitoredItem) session.AddSubscription(subscription) subscription.Create()写点和读点的坑不太一样:Write 返回的结果数组里,每一项对应一个写操作,必须逐个判断 Code,不能只看整体返回。写 42.0 这个值之前,最好先读一下节点的 AccessLevel,确认它确实可写,否则你很可能得到一个好状态码但服务端根本没接受。订阅部分有三个参数是重点:PublishingInterval 是服务端发布间隔,单位毫秒,1000 表示 1 秒发布一次;LifetimeCount 表示若客户端连续收不到发布消息,超过 PublishingInterval 的 60 倍就判会话失效;KeepAliveCount 是发布消息里可以包含多少条“空保活”消息。SamplingInterval 500 是采样间隔,它负责“多快检测变化”,实际通知频率还受发布间隔限制。
还有个细节:TagChanged 事件的 args.ItemValue.Value 是 DataValue,你在事件里拿到的已经是服务端打包好的“最新值”,不用再手动 Read 一次。很多新手在这里多读一次,白白增加了网络往返。订阅创建后,记得在程序退出时释放,不然服务端会一直替你维护这个订阅。
4. 服务端换来换去:URL、超时与会话参数的调法
4.1 URL 三段式与安全策略
把 UA 客户端从一台服务器换到另一台,最先改的就是连接 URL。UA 的 URL 格式是 opc.tcp://主机:端口/路径,三段各有含义:opc.tcp 是传输协议,指明走 UA 的二进制 TCP 通道;主机和端口决定连谁,默认端口绝大多数是 4840;路径是服务端应用自己定义的标识,允许为空,但有些服务器在路径里区分了不同实例。
| URL 片段 | 作用 | 常见改法 |
|---|---|---|
| opc.tcp | 传输协议标识 | 也有 https 但少见,一般不动 |
| 192.168.1.10:4840 | 目标主机和端口 | 换服务器时基本只改这里 |
| /UA/Server | 服务端路径 | 有些服务器要求精确匹配 |
| 末尾安全策略 | 加密与签名策略 | none / Basic256Sha256 常见 |
安全策略这行很多人忽略:URL 本身没有安全策略字段,策略是在端点协商时选的。如果你的工程里写死了 Basic256Sha256,而新服务器只支持 Basic256,你就得在 SelectEndpoint 的匹配逻辑里允许降级。我一般会写一个小函数,把服务器返回的端点列表按安全策略做个排序,优先用签名加加密的策略,没有再退到纯签名,最后才考虑 none。这样换服务器时不会因为策略不匹配连不上,也不会默认用不安全的裸连接。
4.2 三个必调参数:超时、重连与会话保持
联调阶段跑一次成功不难,难的是在现场环境连续跑几周不翻车。我建议重点关注三个参数,它们直接决定稳定性。
| 参数 | 建议初值 | 量级参考 | 影响 |
|---|---|---|---|
| 会话超时 | 60000 ms | 服务端上限内 | 太短会被服务端主动断开 |
| PublishingInterval | 1000 ms | 100~5000 | 太短增加负载,太长丢失实时性 |
| KeepAliveCount | 10 次 | 5~20 | 决定网络抖动多久后判死连接 |
会话超时不是越高越好,有些服务端限定了最大值,你设 600 秒,服务器可能直接拒绝。PublishingInterval 需要和数据变化频率匹配:高速产线用 200ms,普通设备 1 秒足够。KeepAliveCount 的作用是容忍网络抖动,比如断线 3 秒,KeepAliveCount 够大就能在不重建会话的情况下恢复,但前提是你开了自动重连。
Dim connectTimeout As Integer = 10000 session.KeepAliveInterval = 5000 AddHandler session.KeepAlive, Sub(s, e) If Not e.StatusCode.Code = 0 Then Console.WriteLine("连接异常,准备重连") End If End SubKeepAliveInterval 是客户端主动发送保活的心跳间隔,5000 表示 5 秒一次。KeepAlive 事件里,e.StatusCode 非 0 说明连接已经出现异常,这时候触发重连策略是常规玩法。但要注意,重连逻辑应该做退避,比如第一次等 1 秒,第二次 3 秒,最多到 30 秒,避免服务端还没起来,客户端就在疯狂重连。
4.3 可视化配置:把端点与节点放进配置文件
代码里写死服务器地址和节点是最容易埋雷的做法,因为你不知道半年后这个程序会被搬到哪条产线。常见做法是把端点、节点、超时都放到 XML 配置文件里,程序启动时读一次。
<UaClient> <Endpoint Url="opc.tcp://192.168.1.10:4840" /> <Node Id="ns=2;s=Channel1.TagValue" /> <Session Timeout="60000" /> </UaClient>Dim cfgPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ua_client.config") Dim xml As New XmlDocument() xml.Load(cfgPath) Dim endpointUrl As String = xml.SelectSingleNode("//Endpoint/@Url").GetAttribute("Url") Dim nodeIdStr As String = xml.SelectSingleNode("//Node/@Id").GetAttribute("Id") Dim sessionTimeout As Integer = Integer.Parse(xml.SelectSingleNode("//Session/@Timeout").GetAttribute("Timeout"))XML 是目前维护成本最低的方式:现场改地址,用记事本改一行就行,不用重新编译。但有两个注意点:配置文件必须存成 UTF-8,否则中文注释或含中文的节点名会乱码;SelectSingleNode 的 XPath 里 @ 符号是取属性的意思,写错会得到空值,而且不会报错,你查半天才发现是读到了 Nothing。
5. 现场迁移 OpcUaClient_VB 的避坑记录:现象、原因、修法
5.1 一连接就报 BadSecurityChecksFailed
现象:程序在开发环境连接模拟服务器一切正常,搬到现场连真设备,第一步 SelectEndpoint 就抛 BadSecurityChecksFailed,完全连不上。原因:UA 的安全校验包含三样东西——应用证书是否被服务端信任、两端系统时间偏差是否过大、证书是否过期。现场工控机时间经常不准,偏差超过 5 分钟证书校验就会失败。解决:先让工控机与服务器做时间同步,再把客户端生成的证书公钥导入服务端的信任列表。证书可以在 Certs\Application 目录里找到,扩展名通常需要改成服务端可识别的格式后导入。如果时间没问题,再把 Certs\Rejected 目录里被拒的证书拿出来,确认是同一张之后再加入 Trusted。
5.2 读回来的值全是 Nothing 但状态码看着正常
现象:Read 成功返回,StatusCode.Code 也是 0,但 Value 是 Nothing,界面上什么都不显示。原因:UA 的 DataValue 里值、质量、时间戳是三个独立字段,质量 Good 只能说明数据源可达,不代表此刻有有效值。模拟量在设备未运行时,很多服务器返回的 Value 本身就是空,但质量是 BadWaitingForInitialData;还有一类是类型映射问题,服务端返回的是 UInt16 数组,你代码里却按 Double 处理,转换失败后拿到 Nothing。解决:先看 StatusCode,再确认 SourceTimestamp 是否在刷新,最后用 UA 浏览器看这个节点的实际数据类型,把 VB 端转换写成按 VarType 分发。
5.3 程序退出后服务端还显示客户端在线
现象:客户端程序关闭后,在服务端管理界面里仍然能看到这个会话和订阅,连接数一直涨,最后服务器拒绝新连接。原因:进程被强制结束或退出路径没走释放逻辑,UA 服务端要等会话超时才会回收。解决:把会话关闭写进 Finally,确保异常时也能释放。代码里 CloseSession 的传参要写 True,表示删除服务端所有订阅,否则订阅还会残留。
Try ' 业务代码 Finally session.CloseSession(True) session.Dispose() End Try5.4 节点标识里的中文变成乱码
现象:UA 浏览器里节点名是“2号机组.温度”,复制到工程代码里,运行时代码里显示正常,但实际请求的字符串是乱的,读不到数据。原因:源码文件保存编码和编译时解析编码不一致。老工程从某台 Windows 机器拷贝过来,VB 源码是 GB2312 或 GBK 保存的,新机器默认 UTF-8,字符串字面量就会错位。解决:统一编码。如果是 VB.NET 工程,把源码和配置文件都转成 UTF-8 带 BOM;如果是 VB6 工程,反而要确认项目文件用系统 ANSI 编码读,别乱转。节点字符串最好也不要写中文,能换 s=TagValue 这类英文标识更稳妥。
5.5 32 位/64 位与依赖装配问题
现象:编译时引用正常,运行时抛 BadImageFormatException 或者提示找不到依赖 DLL。原因:工程目标平台是 x86,但某些 UA 依赖的原生组件是 64 位;或者引用的 DLL 被放在子目录,程序启动时当前目录不对。解决:打开工程属性,把平台目标显式设成 x64 或 AnyCPU,不要用默认的 Prefer 32-bit;引用本地 DLL 时设置 Copy Local = True,并确认输出目录里确实同步了依赖。更省心的办法是改用 NuGet 引用替代本地 DLL,包的依赖关系由构建还原,不会出现少拷一个文件的问题。
6. 让这个客户端稳定跑半年:证书预装、日志与写点保护
如果不做任何加固,OpcUaClient_VB 风格的客户端通常会在上线后第二到四周暴露问题,集中表现在证书交互、故障定位难、写点误操作这三个方面。我的习惯是做三件事,成本都极低。
第一件,把证书交互做成预装流程。UA 客户端第一次连接时,弹窗让用户确认证书,在很多工控现场是不被允许的——操作员根本不知道该点哪个按钮。我的做法是写一个一次性工具:生成证书后,把公钥文件放到指定目录,同时给现场运维一份导入说明,让服务器管理员在测试阶段完成信任导入。正式程序里不再弹任何确认框,连接逻辑从头到尾不依赖人工点击。
第二件,把 UA 库的跟踪日志打开,这会在关键时刻救你一命。
Utils.SetTraceMask(Utils.TraceMask.All) Utils.SetTraceOutput(Utils.TraceOutput.File, "ua_trace.log")这两行放在 ApplicationConfiguration 创建之前,UA 库会把握手、证书校验、订阅发布的所有内部消息写到 ua_trace.log 里。有个真实教训:某次现场报“偶尔连不上”,复现时程序看起来卡在连接,但没有任何异常,最后是在跟踪日志里看到握手阶段证书链校验偶发超时,才定位到是工控机杀毒软件扫描证书目录导致的。没有日志,这种问题几乎是无头案。
第三件,写点必须带保护。UA 服务器默认不会校验你写的值是否合理,你写个负温度它可能照收。我的套路是三点:写之前读 AccessLevel 确认可写,写之后立刻回读并跟目标值比较,误差超过阈值就告警。
Dim writeResp = session.Write( Nothing, {writeValue}).GetAwaiter().GetResult() Dim verifyResp = session.Read( Nothing, 0, TimestampsToReturn.Neither, {readValue}).GetAwaiter().GetResult() Dim written = CDbl(writeResp.Results(0).Code) Dim actual = CDbl(verifyResp.Results(0).Value) If Math.Abs(actual - 42.0) > 0.001 Then Console.WriteLine("写点校验失败,请检查服务端权限") End If这段代码里,写响应和读响应的结果都要判断状态码,然后才比较数值。误差阈值 0.001 是给浮点场景用的,如果你写的是整数型变量,直接用不等于比较更能暴露问题。回读校验的目的不是防服务器篡改,而是防两种情况:一种是服务端配置了量程转换,写入值和读取值之间存在隐含换算;另一种是权限配置导致写入被静默拒绝,而客户端没收到错误码。
我最初把这种客户端搬上线时,就是在证书和日志上栽了跟头,后来把这两件事固定成每个工程必做的基础配置,维护成本才真正降下来。证书预装解决信任问题,日志解决排查问题,写点保护解决误操作问题——做到这三样,这个方案就能从“能跑”变成“能一直跑”。希望帮到你。
本文还有配套的精品资源,点击获取