简介这份压缩包围绕 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 或者 TagOPC 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形如ns2;sDemo.SetPointns后面是命名空间索引分号后面是标识符标识符可以是字符串也可以是数字。三是自带安全机制加密和身份认证是协议的一部分不像老一代那样基本裸奔。代价是概念多一点。你会遇到会话Session、订阅Subscription、监视项MonitoredItem、状态码StatusCode这些词。别被吓到入门阶段真正要用的只有四个动作连上、读一个点、写一个点、断开。剩下的等你能跑通了再补。2.4 VB.NET 接进来分别要准备什么两条路在 VB.NET 里的工程量差别不小先看个对照心里有个数对比项OPC DAOPC 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是要替换成你环境的 ProgIDChannel1.Device1.Tag1是要替换成你服务器里真实的 Item ID1是客户端自己用的句柄只要求在一组里唯一。注意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 要读的节点ns0;i2258 是服务端自带的当前时间节点拿它做首次测试最省事 Private Const TestNodeId As String ns0;i2258为什么要用服务端的当前时间节点做第一次测试因为它几乎每个 UA 服务器都有不需要你在服务器里额外建变量也不会有类型和权限问题。你第一次跑通靠的就是它。等你能稳定读出这个时间值再换成你真正关心的业务点位。NodeId 的格式值得单独说两句。ns0表示命名空间索引 0这是协议保留的命名空间里面放的是服务器自身信息i2258表示标识符是数字 2258。你自己建的点位通常在ns2及以上的命名空间里格式可能是ns2;sDemo.SetPoints表示标识符是字符串。这两个前缀搞混是后面读不到值的第一大原因。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(ns2;sDemo.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 写错了。三种常见错法命名空间索引写错把ns2写成ns0、标识符类型写错该用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 一条三个月能走完的自学路径不用贪多按下面这个顺序走每一阶段都有能看得见的产出阶段练什么达到什么程度算过关第 12 周连接、读、写、断开四个动作能独立跑通示例能自己换节点第 34 周订阅与回调值变化时能实时打印不丢数据第 56 周异常与重连拔网线再插回去程序能自己恢复第 78 周批量读与批量写一次处理几十个点不逐个发请求第 912 周接到界面或存库能做出一个自己愿意天天开的小工具入门阶段最值得先补的其实不是 OPC 本身是两件周边能力一是会看状态码二是会看日志。状态码告诉你这次操作到底成没成日志告诉你是从哪一刻开始不成的。这两个能力练出来后面换任何协议都能迁移。6.3 我自己的一个习惯我刚开始写这类程序时喜欢一次性把功能全堆上去结果一报错就完全不知道问题出在哪一层。后来我强制自己每加一个功能就先单独跑一次先只连接跑通再加读跑通再加写跑通最后加订阅。听起来慢实际上比反复回滚快得多因为每次出问题你都确切知道是刚加的那一小块。从那以后我每次接一个新的 OPC 环境都会强制走一遍这个顺序连上、读一个自带节点、读一个业务节点、写一次、断线重连一步都不跳。这套流程帮我避开了太多“代码没错但就是不工作”的情况。你手上这份示例工程最好的用法不是照抄一遍就完事而是把它当成骨架按你自己的环境一点点替换参数每换一处就跑一次。跑到哪一步报错问题就在那一步比任何猜测都准。希望帮到你。本文还有配套的精品资源点击获取