简介:这份压缩包围绕 VB .NET 环境下的 OPC 通信入门展开,面向希望了解工业自动化数据交换、准备在 .NET 项目中接入 OPC 服务器的初中级开发者,也可作为教学演示的基础案例。内容以可运行示例为主线,涉及 OPC 客户端对象创建、服务器连接、数据项读写及数据变化事件注册等核心环节,读者可对照源码理解 OPC 自动化接口的调用方式与互操作程序集的引用关系。压缩包共 38 个文件,约 194KB,包含 4 个 VB 源码文件、2 个 vbproj 项目文件、3 个 dll(含 OPC 互操作程序集)、3 个 exe 及 png、xml、resx 等资源与配置文件,兼顾源码查阅、项目加载和运行验证。已有 198 人学习,可作为 VB .NET 与 OPC 技术结合的起步参考,也为客户端功能扩展、联调排错和课堂演示提供可复用的结构样本。
1. 从一个“读不到数”的现场问题说起:OPC 到底帮你摆平了什么
刚接触这个方向的人,多半是被一份 Simple VB .NET OPC Example 示例工程吸引过来的:打开一看只有几十行 VB 代码,却满屏 Session、Endpoint、NodeId 这类词,第一反应往往是先关掉再说。其实它要干的事特别朴素——让一个用 VB.NET 写的程序,去另一个软件或者另一台机器里,把某个温度值、某个开关状态读出来,或者反过来写一个值回去。之所以要绕一层 OPC,是因为现场设备的口音太多了,A 家的协议、B 家的接口、C 家的数据结构各不相同,逐个对接等于给自己挖坑。
OPC 就是这些设备之间约定好的“普通话”。设备侧跑一个 OPC 服务器负责翻译,你的 VB.NET 程序当客户端,按同一套规矩说话,就不用再关心对面到底是哪家的东西。这份示例适合两类人:一是刚进工控软件方向、需要让上位程序连上现场数据的初学者;二是手上有一份老 VB 程序要维护、但完全没接触过 OPC 的开发者。
2. 先分清两条路:OPC DA 与 OPC UA 到底差在哪
学 OPC 最容易犯的错,是一头扎进代码里,结果连自己连的是哪一代技术都没弄清。OPC 本身分了两代主流做法,它们解决的是同一类问题,但底层机制、编程模型、甚至能不能在 64 位程序里跑,都完全不同。你手上这份 VB.NET 示例(Simple VB .NET OPC Example)在网上的写法通常也分这两派,先花十分钟把区别摸清,后面能少走很多弯路。
2.1 OPC 里的两个角色:谁在提供数据,谁在拿数据
先说清角色,不然后面全是糊涂账。整个体系里只有两种程序:服务器和客户端。服务器一般不单独存在,它挂在某个采集软件或者设备网关里,负责把现场的点位整理成一张“可以按名字取值的表”;客户端就是你写的 VB.NET 程序,负责连上去、报一个名字、把值拿回来。
这里有个新手常绕不过去的点:你在服务器界面里看到的点位名字,和代码里要填的名字,经常不是同一个东西。服务器界面上显示的是给人看的“友好名称”,代码里要填的是协议层面的标识符。OPC DA 里这个标识符叫 Item ID 或者 Tag,OPC UA 里叫 NodeId。这个坑后面会单独讲,先记住一句话:界面里能看懂的,代码里不一定认。
还有一层是“组”的概念。老一代 OPC DA 里,客户端不能直接拿点,必须先建一个组,把点位加进组里,再对组做读或订阅;OPC UA 把这个中间层弱化了,你可以直接按 NodeId 读,也可以自己建订阅把一批点放进去。所以看老示例代码时会发现一堆 Group 相关的东西,那不是我写复杂了,是那个年代的规定动作。
2.2 OPC DA:老现场里最常见的那条路
OPC DA 是 Data Access 的缩写,诞生时间很早,基于 Windows 的组件对象模型实现。翻译成人话:服务器的能力被注册进系统里,客户端通过一个叫 ProgID 的字符串把服务器“叫起来”,然后调用它暴露出来的接口。你项目里那句Imports OPCAutomation,导入的就是这层接口的包装。
它的优点很实在:大量老现场的服务器只支持这一代,你绕不开;调用模型简单,读一个点就是取一个值,写一个点就是赋值。它的麻烦也实在:
第一,它绑死在 Windows 上,换个系统就没得玩。第二,它依赖系统级的组件注册和权限配置,两台机器网络位置不同、账户不同,可能一台通一台不通,这类问题排查起来很消耗耐心。第三,它有个极易被忽略的限制——进程位数。32 位的组件在 64 位进程里就是找不到,编译能过,运行报错,报错信息还往往看不出根因。
入门阶段如果手上只有 DA 的服务器,那没办法,硬着头皮上;如果可以选择,先别从这条路进。
2.3 OPC UA:新项目更愿意走的那条路
OPC UA 里的 UA 是 Unified Architecture,可以理解成一次彻底重做。它不再依赖系统组件注册,走的是标准的 TCP 连接,地址形如opc.tcp://主机:端口,服务器和客户端用一套独立定义的消息格式通信。
对写 VB.NET 的人来说,最直接的差别有三个。一是不用再折腾系统级注册,装一个客户端库(NuGet 包里搜 OPC UA 能找到,命名空间是Opc.Ua.Client)就能开工,跨机器跨系统都一样。二是数据模型变了:每个点位是一个NodeId,形如ns=2;s=Demo.SetPoint,ns=后面是命名空间索引,分号后面是标识符,标识符可以是字符串也可以是数字。三是自带安全机制,加密和身份认证是协议的一部分,不像老一代那样基本裸奔。
代价是概念多一点。你会遇到会话(Session)、订阅(Subscription)、监视项(MonitoredItem)、状态码(StatusCode)这些词。别被吓到,入门阶段真正要用的只有四个动作:连上、读一个点、写一个点、断开。剩下的等你能跑通了再补。
2.4 VB.NET 接进来分别要准备什么
两条路在 VB.NET 里的工程量差别不小,先看个对照,心里有个数:
| 对比项 | OPC DA | OPC UA |
|---|---|---|
| 接入方式 | 引用 COM 组件,代码里Imports OPCAutomation | 装 NuGet 客户端包,Imports Opc.Ua.Client |
| 点位标识 | Item ID / Tag,各服务器风格不统一 | NodeId,格式统一 |
| 能否跨平台 | 不能,绑 Windows | 可以 |
| 典型报错 | COM 异常、类未注册、找不到 ProgID | 连接超时、BadNodeIdUnknown、BadSecurityChecksFailed |
| 新手友好度 | 低,环境问题占一半时间 | 相对高,问题基本在代码和参数上 |
DA 那段代码长这样,结构简单到有点寒酸,但每句话都得有对应环境才能跑:
' 需要先在项目里引用对应的 COM 组件,并保证进程位数与组件一致 Imports OPCAutomation Module Program Sub Main() Dim server As New OPCServer() ' ProgID 是服务器注册进系统里的名字,不是你界面上看到的友好名称 server.Connect("SomeVendor.OpcServer.1") Console.WriteLine($"已连接:{server.ServerName}") Dim group As OPCGroup = server.OPCGroups.Add("DemoGroup") group.UpdateRate = 1000 ' 第二个参数是客户端句柄,随便给个唯一数字即可 Dim item As OPCItem = group.OPCItems.AddItem("Channel1.Device1.Tag1", 1) Console.WriteLine($"读到:{item.Value} 质量:{item.Quality}") server.Disconnect() End Sub End Module这段代码的重点不在语法,在三个参数:SomeVendor.OpcServer.1是要替换成你环境的 ProgID;Channel1.Device1.Tag1是要替换成你服务器里真实的 Item ID;1是客户端自己用的句柄,只要求在一组里唯一。注意item.Value拿的是缓存值,老组件需要先触发一次读取或者开订阅才会有新鲜数据,直接打印经常是空的,这不是代码写错了。
UA 那边的前置工作更轻:装好 .NET SDK,建一个控制台项目,加一个客户端包,代码里导入命名空间就能写。所以下面几章我都以 UA 为主线,DA 的坑在第 5 章单独拎出来讲。
2.5 新手先学哪个:我的建议
如果你手上还没有必须对接的现场服务器,先学 OPC UA。原因很功利:它的环境问题少,能让你把注意力放在“连接、读、写、断开”这四个动作上;等这套流程熟了,再回头看 DA,你会发现差别只在接入层,思路是通的。反过来先啃 DA,很可能第一天就卡在组件注册上,连代码长什么样都没心情看。
如果你手上已经有 DA 服务器、任务又急,那就直接上 DA,但请先确认两件事:服务器的 ProgID 是什么,以及你的程序要编译成 32 位还是 64 位。这两件事没确认之前,写多少代码都是白搭。
3. 上手前把台子搭好:VB.NET 项目、客户端库和一个能连的服务器
这一章全部是准备工作,看着琐碎,但它决定了你后面一小时是顺利跑通还是反复报错。我带过的新人里,卡在第一步的绝大多数不是代码问题,而是环境没对齐:SDK 装了个半成品、项目建成了别的语言、库装错了包、或者根本没有一个能连的服务器可以试。按下面的顺序走一遍,每一步都有办法自己验证成功没有。
3.1 三样东西先备齐:SDK、编辑器、服务器
第一样是 .NET SDK。VB.NET 属于 .NET 体系,没有 SDK 就没有编译器。装完之后打开一个命令行窗口,敲:
dotnet --version能打印出一个版本号就说明装好了。打印不出来,先解决这个,别往下走。这一句看着简单,但它省掉了很多“命令找不到”的无效折腾。
第二样是编辑器。用你顺手的就行,命令行加任意文本编辑器也能写完这份示例;想要补全提示和调试方便,就用带 VB.NET 支持的集成开发环境。入门阶段不建议折腾插件生态,能写能调试就够了。
第三样最容易被忽略:一个能连上的 OPC UA 服务器。客户端代码写得再对,没有服务器可连,你只会看到连接超时,然后开始怀疑人生。入门阶段最省事的办法是在本机跑一个测试服务器,很多采集类软件都自带试用模式的 UA 服务,启动后默认监听opc.tcp://127.0.0.1:4840这类地址。启动之后,用任意一个通用 OPC UA 客户端工具连一次,能浏览出节点树,就说明服务器这一侧没问题了。
提示:先用现成工具确认服务器能连,再去写代码。这一步能把“服务器没起来”和“我的代码有问题”两类原因彻底分开,省下来的时间远超你花的那两分钟。
3.2 用命令行建一个 VB.NET 控制台项目
不要一上来就建 Windows 窗体项目,界面会把你对流程的判断搅乱。先建控制台,能打印出值就算成功,界面是后面的事。
# 建一个 VB 的控制台项目,-lang 指定语言,-n 是项目名 dotnet new console -lang VB -n VbOpcDemo # 进入项目目录 cd VbOpcDemo # 跑一下模板代码,确认工具链是通的 dotnet run三条命令,每条都有它的意义。第一条里的-lang VB是关键,不写默认是 C#,你会得到一个后缀是.csproj的项目,然后对着满屏大括号发呆。第二条是切目录,别小看它,很多人后面所有命令都在错误的目录里执行,报“找不到项目文件”。第三条是自检:模板代码会打印一句 Hello World,能打印出来说明 SDK、项目文件、编译链全都正常。
打开项目目录,你会看到一个Program.vb,里面已经有一个Module Program和Sub Main()。这份示例后面所有的代码都往这个文件里加,不需要新建别的文件。
3.3 把客户端库加进来,确认没装错
VB.NET 本身不认识 OPC UA,得靠库。在项目目录里执行:
# 客户端库,装完代码里就能 Imports Opc.Ua.Client dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client装完之后,打开.vbproj文件,应该能看到一行PackageReference。这一步的验证方法是:在Program.vb顶部敲Imports Opc.Ua,如果编辑器不给你画红波浪线,说明引用生效了;如果画了,八成是包没装进当前项目,或者你装到了别的目录下。
这里有个新手常犯的错:在解决方案根目录执行安装命令,而不是在项目目录里。结果包被加到了别的项目上,当前项目依然一片红。判断方法很简单,看命令执行完输出的路径是不是你正在编辑的那个文件夹。
装完之后顺手做一件事:在Sub Main()里写一行Console.WriteLine("环境就绪"),然后dotnet run。能打印出来,说明库引用没有破坏编译。养成这个习惯,出问题时你就知道是“加库之前坏的”还是“加库之后坏的”。
3.4 先做连通性自检,别急着写业务
在写正式的读写代码之前,先花五分钟做一次纯连接测试。这一步不读数据、不写数据,只验证“我能不能和服务器握上手”。代码如下,放到Program.vb里:
Imports Opc.Ua Imports Opc.Ua.Client Imports Opc.Ua.Configuration Module Program ' 服务器地址,端口以你本地测试服务器实际监听为准 Private Const EndpointUrl As String = "opc.tcp://127.0.0.1:4840" Sub Main() Try Dim config As ApplicationConfiguration = BuildConfig() ' 先问一次服务器支持哪些安全策略,这里取不加密的那条 Dim desc As EndpointDescription = CoreClientUtils.SelectEndpoint(EndpointUrl, False, 15000) Console.WriteLine($"拿到的端点:{desc.EndpointUrl}") Console.WriteLine($"安全策略:{desc.SecurityPolicyUri}") Catch ex As Exception Console.WriteLine($"连不上:{ex.Message}") End Try End Sub ' 客户端自身的配置,入门阶段用默认值加上必要的几项即可 Private Function BuildConfig() As ApplicationConfiguration Dim cfg As New ApplicationConfiguration() With { .ApplicationName = "VbOpcDemo", .ApplicationUri = "urn:localhost:VbOpcDemo", .ProductUri = "urn:localhost:VbOpcDemo", .ServerConfiguration = New ServerConfiguration(), .ClientConfiguration = New ClientConfiguration() With {.DefaultSessionTimeout = 60000}, .TransportConfigurations = New TransportConfigurationCollection(), .TransportQuotas = New TransportQuotas() With {.OperationTimeout = 15000}, .SecurityConfiguration = New SecurityConfiguration() With { .AutoAcceptUntrustedCertificates = True, .AddAppCertToTrustedStore = True }, .TraceConfiguration = New TraceConfiguration() } cfg.Validate(ApplicationType.Client).GetAwaiter().GetResult() Return cfg End Function End Module这段代码做了两件事。SelectEndpoint是向服务器发一次探测,问它“你有哪些端点、用什么安全策略”,第二个参数False表示这次只要不加密的端点,第三个参数是超时毫秒数。BuildConfig是在构造客户端自己的身份信息,ApplicationName和ApplicationUri随便起但别乱改,OperationTimeout决定单次操作最多等多久,AutoAcceptUntrustedCertificates在入门阶段设成 True 能省掉一堆证书信任的麻烦。
跑通的标准是打印出端点和安全策略两行。如果这里就报超时,那问题一定不在后面的读写代码上,先回头确认服务器是不是真的在监听、端口有没有写错、本机有没有别的程序占用。这一步过了,后面才有意义。
注意:
CoreClientUtils.SelectEndpoint在不同版本的库里参数个数略有差异,可能是两个参数也可能是三个。写的时候看编辑器的参数提示,把超时补上就行,逻辑不变。
4. 第一个能跑的例子:连上、读一个点、写一个点
现在做正事。这一章按顺序拆成五步,每一步都能单独跑出结果,别一次性全粘进去再一起调试,那样报错时你分不清是哪一步出的问题。这也是我教新人时反复强调的:能分步验证的东西,绝不合并。
4.1 第一步:把可变的东西集中成常量
先把所有需要你按自己环境改的东西提到文件顶部,做成常量。这个习惯看着笨,实际能省掉大量“改了这里忘了那里”的返工:
' 服务器地址:协议固定 opc.tcp,主机和端口按你的环境改 Private Const EndpointUrl As String = "opc.tcp://127.0.0.1:4840" ' 要读的节点:ns=0;i=2258 是服务端自带的当前时间节点,拿它做首次测试最省事 Private Const TestNodeId As String = "ns=0;i=2258"为什么要用服务端的当前时间节点做第一次测试?因为它几乎每个 UA 服务器都有,不需要你在服务器里额外建变量,也不会有类型和权限问题。你第一次跑通,靠的就是它。等你能稳定读出这个时间值,再换成你真正关心的业务点位。
NodeId 的格式值得单独说两句。ns=0表示命名空间索引 0,这是协议保留的命名空间,里面放的是服务器自身信息;i=2258表示标识符是数字 2258。你自己建的点位通常在ns=2及以上的命名空间里,格式可能是ns=2;s=Demo.SetPoint,s=表示标识符是字符串。这两个前缀搞混,是后面读不到值的第一大原因。
4.2 第二步:建立会话,先打印端点信息
会话(Session)是客户端和服务器之间的一次正式连接,后面所有读写都挂在它上面。把上一章的自检代码扩展一下:
Dim session As Session = Nothing Try Dim config As ApplicationConfiguration = BuildConfig() Dim desc As EndpointDescription = CoreClientUtils.SelectEndpoint(EndpointUrl, False, 15000) Dim endpoint As New ConfiguredEndpoint(Nothing, desc) With {.UpdateBeforeConnect = False} ' 参数依次是:配置、端点、连接前是否刷新、会话名、超时毫秒、用户身份、语言列表 session = Session.Create(config, endpoint, False, "VbOpcDemo", 60000, New UserIdentity(), Nothing).GetAwaiter().GetResult() Console.WriteLine($"已连上:{session.Endpoint.EndpointUrl}") Console.WriteLine($"服务端:{session.Endpoint.Server.ApplicationName}")Session.Create是个异步方法,VB 里用.GetAwaiter().GetResult()同步等它完成,入门阶段这样写最直观,不用一开始就纠缠Async和Await。New UserIdentity()表示匿名登录,本地测试服务器基本都允许;真实环境如果配了账号密码,换成对应的身份对象即可。
ConfiguredEndpoint那一行里的UpdateBeforeConnect = False意思是连接前不再重新探测端点。因为上一步我们已经拿过一次端点描述了,重复探测只是多一次网络往返,还会在某些服务器上引起不必要的延迟。
跑到这里能看到两行打印,就说明会话建立成功。这一步是整个示例的地基,如果这里失败,后面所有代码都不用看了。
4.3 第三步:读一个节点,看懂返回的 DataValue
读的代码只有三行,但返回的东西比你想的多:
Dim node As NodeId = NodeId.Parse(TestNodeId) Dim dv As DataValue = session.ReadValue(node) Console.WriteLine($"状态码:{dv.StatusCode}") Console.WriteLine($"值:{dv.Value}") Console.WriteLine($"源时间戳:{dv.SourceTimestamp}") Console.WriteLine($"服务器时间戳:{dv.ServerTimestamp}")ReadValue返回的不是一个裸值,而是一个DataValue对象。这个设计刚接触时会觉得绕,但它很有必要:工业现场的数据不光有值,还有“这个值可不可信”和“这个值是什么时候的”。四个字段的意思分别是:
StatusCode是状态码,Good表示值可信;BadNodeIdUnknown表示这个节点根本不存在;BadNotReadable表示节点存在但没开读权限。判断读取成功与否,看的是状态码,不是看值是不是空。这点非常关键,很多新手拿到一个空字符串就以为读到了空值,其实是读失败。
Value是实际的值,类型可能是数字、字符串、布尔,甚至是一个结构体。打印之前最好确认一下类型,不然你会看到一串看不懂的东西。
SourceTimestamp是数据源产生这个值的时间,ServerTimestamp是服务器处理这次请求的时间。两个时间差很多,通常意味着服务器缓存了很久没刷新。
4.4 第四步:写入一个值,类型必须对上
写比读多两道手续:一是要指定写哪个属性,二是要检查每个点位的写入结果。代码如下:
' 换成你环境里确实可写的一个节点 Dim writeNode As NodeId = NodeId.Parse("ns=2;s=Demo.SetPoint") Dim wv As New WriteValue() With { .NodeId = writeNode, .AttributeId = Attributes.Value, .Value = New DataValue(New Variant(CObj(36.5F))) } Dim results As StatusCodeCollection = Nothing Dim diag As DiagnosticInfoCollection = Nothing ' 第一个参数传 Nothing 表示用默认请求头 session.Write(Nothing, New WriteValueCollection() From {wv}, results, diag) If results IsNot Nothing AndAlso StatusCode.IsBad(results(0)) Then Console.WriteLine($"写入失败:{results(0)}") Else Console.WriteLine("写入成功") End If几个参数值得说清楚。AttributeId = Attributes.Value表示你要改的是“值”这个属性,节点的属性不止值一个,还有描述、数据类型等。New Variant(CObj(36.5F))是给值套一层类型包装,CObj是为了让编译器别在重载之间犯迷糊。
批量写的接口意味着你要传一个集合,即使只写一个点。返回的results是逐个点位的结果,长度和你传进去的集合一样。写入不抛异常不等于写成功,一定得看results(0)。这是新手最容易误解的地方:程序没崩,但设备纹丝不动,原因就在这里。
还有一个隐藏条件:类型必须和服务器那边定义的完全一致。服务器把点位定义成 32 位整数,你写一个浮点数进去,服务器会直接给你一个BadTypeMismatch。这不是库的问题,是协议在保护数据。
4.5 第五步:断开与异常兜底
最后补上收尾。会话是占用服务器资源的,用完不关,服务器那边的连接数会一直涨:
Catch ex As Exception Console.WriteLine($"出错:{ex.Message}") Finally If session IsNot Nothing Then session.Close() session.Dispose() End If End Try放在Finally里是为了保证无论前面成功还是抛异常,连接都会被释放。Close是礼貌地告诉服务器“我走了”,Dispose是释放本地资源,两个都做才干净。
到这一步,你已经完成了一个最小可用的 OPC UA 客户端:连接、读、写、断开。这份 Simple VB .NET OPC Example 的核心骨架就是这些,剩下的是把它接到界面、改成订阅、加上重连。第一次跑的时候建议把读、写分成两次运行,先只读,确认能读到时间值,再放开写操作,别一上来就写,万一节点选错,改的是别人正在用的参数。
5. 新手最容易翻车的五处:连不上、读空、写不进、断线
这一章全部是踩过的坑。每一条都按“你会看到什么现象、背后是什么原因、怎么解决”来写,遇到问题时可以直接对照。
5.1 现象:程序卡十几秒,然后报连接超时
现象:运行程序后没有任何输出,等十几秒才打印一行类似“连接超时”的报错,改代码怎么改都一样。
原因:绝大多数情况是服务器根本没在监听那个地址,或者端口写错了。少数情况是本机防火墙拦了出站,或者你写的不是opc.tcp而是http。还有一种隐蔽情况:服务器启动了,但只监听了某个具体网卡,你写的127.0.0.1恰好不在它的监听范围里。
解决:先用现成的通用 OPC UA 客户端工具连同一个地址,能连上就说明服务器没问题,问题在你的代码参数;连不上就说明问题在服务器侧,跟 VB 一点关系都没有。确认地址时逐个字符对,端口号是四位数,别把4840写成4804。如果服务器在另一台机器上,把127.0.0.1换成实际地址,同时确认两台机器网络可达。
5.2 现象:连上了,但读出来是空的或者报 BadNodeIdUnknown
现象:会话建立成功,打印出了服务端名称,但一读就返回BadNodeIdUnknown,或者值是空的。
原因:NodeId 写错了。三种常见错法:命名空间索引写错(把ns=2写成ns=0)、标识符类型写错(该用s=字符串的写成了i=数字)、大小写不一致(字符串标识符是区分大小写的,Demo.SetPoint和demo.setpoint是两个节点)。
解决:用客户端工具浏览一遍节点树,把目标节点的 NodeId 原样复制出来,别手敲。复制完再核对一遍前缀。另外要分清“显示名称”和“NodeId”,界面上显示的是给人看的名字,代码里要的是 NodeId,这两个经常不一样。
5.3 现象:写入没报错,但设备那边纹丝不动
现象:调用写入接口,程序没抛异常,打印出来还挺正常,但设备上的值一点变化没有。
原因:三种可能。一是没检查返回的StatusCode,写入失败被静默吞掉了;二是类型不匹配,服务器定义了整型你写了浮点,返回BadTypeMismatch;三是这个节点本身只读,服务器返回BadNotWritable。
解决:把results(0)打印出来看,这一步能解决八成问题。看状态码就知道是类型问题还是权限问题。类型问题改成对应类型,比如服务器定义的是Int16,你就别传Single;权限问题得去服务器侧改配置,客户端改代码没用。
5.4 现象(DA 专属):同一份程序,别人的机器能跑,我这报 COM 异常
现象:拿到的 DA 示例在同事机器上跑得好好的,到你这里一运行就抛 COM 相关异常,提示类未注册或者找不到组件。
原因:这就是 DA 最典型的两个坑。第一是进程位数:项目编译成了 64 位,而服务器提供的组件是 32 位的,系统层面就找不到,编译期完全看不出来。第二是组件没注册:服务器软件装了但没注册到系统,或者装的时候权限不够。
解决:先看项目属性里的目标平台,把它改成和组件一致的位数,改完重新生成再跑。再确认服务器软件是否正常运行、ProgID 是否拼对。如果两台机器网络位置不同,还要看系统级的权限配置——这一层是 DA 最消耗耐心的部分,能避开就避开,实在避不开就找有经验的人一起看一次,比自己盲试快得多。
5.5 现象:程序跑几分钟就崩,或者越跑越慢
现象:单独读一次没问题,放在循环里跑一会儿就抛异常退出,或者响应越来越慢。
原因:会话没有复用,每次循环都新建一个会话,服务器的连接数很快被占满;或者没有任何异常重连机制,网络抖一下会话就废了,后续所有调用全部失败。
解决:会话建立一次,循环里反复用,这是最基本的原则。循环里加一个短暂的等待,别写成死循环满速打服务器。外面套一层重连逻辑:捕获到会话断开的异常就关掉旧会话、重新建立、继续跑。这个改动不大,但它是“能演示”和“能长期跑”的分界线。我第一次写这类程序时就是没做重连,在现场跑了一晚上,第二天早上发现日志停在凌晨两点,血泪经验。
6. 跑通之后学什么:从“读一个点”到“能用起来”
能读出服务端时间、能写一个值回去,说明你已经跨过了最难的那道坎——环境、概念、调用顺序都通了。接下来别急着做界面,先把“读一次”升级成“持续盯着”,这一步的收益最大。
6.1 把一次读取换成订阅
轮询读的写法是开个循环不停问,简单但浪费;订阅是告诉服务器“这个点变了就通知我”,服务器主动推。UA 里对应的三个对象是订阅、监视项、回调:
' 订阅:决定服务器多久推一次、失联多久算断 Dim sub1 As New Subscription(session.DefaultSubscription) With { .PublishingInterval = 1000, ' 每秒推一次 .KeepAliveCount = 10, .LifetimeCount = 30 } ' 监视项:具体盯哪个节点 Dim item As New MonitoredItem(sub1.DefaultItem) With { .StartNodeId = NodeId.Parse(TestNodeId), .AttributeId = Attributes.Value, .DisplayName = "测试点" } AddHandler item.Notification, AddressOf OnValueChanged sub1.AddItem(item) session.AddSubscription(sub1) sub1.Create()PublishingInterval是推送间隔,单位毫秒,别设太小,现场几千个点的时候这个值设成 100 会让网络和服务器都很吃力。KeepAliveCount和LifetimeCount是保活参数,前者表示连续多少次没有数据也发一个空包确认连接还在,后者表示连续多少次收不到响应就认为订阅失效。回调函数里拿到的是MonitoredItem和它对应的DataValue,处理方式和第 4 章读出来的对象完全一样。学会订阅之后,你会发现前面那套同步读的写法,只适合做一次性查询和调试。
6.2 一条三个月能走完的自学路径
不用贪多,按下面这个顺序走,每一阶段都有能看得见的产出:
| 阶段 | 练什么 | 达到什么程度算过关 |
|---|---|---|
| 第 1~2 周 | 连接、读、写、断开四个动作 | 能独立跑通示例,能自己换节点 |
| 第 3~4 周 | 订阅与回调 | 值变化时能实时打印,不丢数据 |
| 第 5~6 周 | 异常与重连 | 拔网线再插回去,程序能自己恢复 |
| 第 7~8 周 | 批量读与批量写 | 一次处理几十个点,不逐个发请求 |
| 第 9~12 周 | 接到界面或存库 | 能做出一个自己愿意天天开的小工具 |
入门阶段最值得先补的其实不是 OPC 本身,是两件周边能力:一是会看状态码,二是会看日志。状态码告诉你这次操作到底成没成,日志告诉你是从哪一刻开始不成的。这两个能力练出来,后面换任何协议都能迁移。
6.3 我自己的一个习惯
我刚开始写这类程序时,喜欢一次性把功能全堆上去,结果一报错就完全不知道问题出在哪一层。后来我强制自己每加一个功能就先单独跑一次:先只连接,跑通;再加读,跑通;再加写,跑通;最后加订阅。听起来慢,实际上比反复回滚快得多,因为每次出问题你都确切知道是刚加的那一小块。
从那以后我每次接一个新的 OPC 环境,都会强制走一遍这个顺序:连上、读一个自带节点、读一个业务节点、写一次、断线重连,一步都不跳。这套流程帮我避开了太多“代码没错但就是不工作”的情况。你手上这份示例工程,最好的用法不是照抄一遍就完事,而是把它当成骨架,按你自己的环境一点点替换参数,每换一处就跑一次。跑到哪一步报错,问题就在那一步,比任何猜测都准。希望帮到你。
本文还有配套的精品资源,点击获取