简介面向VB.NET开发者与工业自动化入门者的OPC通信基础示例围绕在VB.NET项目中接入OPC服务器这一常见需求展开适合作为教学演示和二次开发的起点。压缩包共38个文件约194KB包含4个vb源代码文件、2个vbproj项目文件与2个sln解决方案另有3个DLL含OPC自动化互操作库、3个exe、resx/resources资源及txt说明覆盖项目结构、编译产物与OPC接口引用便于直接打开研究。已有198人学习说明其作为入门样例具有一定参考价值。示例重点呈现创建OPC客户端、连接服务器、浏览数据项、读写OPC项以及注册数据变化事件等基本流程并保留备份与升级报告等目录可帮助读者理解VB.NET与OPC自动化接口的配合方式快速搭建可运行的通信原型也为后续扩展实际采集与监控功能提供参照。1. 从一条老产线的温度曲线说起VB.NET OPC 示例解决的是哪类活车间里一台跑了十年的注塑机控制柜里还是早期 PLC上位机要采十几个温度和压力点。现场没有 REST 接口也没有消息中间件唯一能对接的就是工控机上装着的一套 OPC DA 服务器。这份 Simple VB .NET OPC Example 就是为这种局面准备的一份用 VB.NET 写的最小客户端工程把「枚举本机服务器、浏览 Item ID、建组加项、同步读写、异步订阅、断开释放」这条链路完整走了一遍。它没有做框架抽象代码摊开就能读适合拿来做工艺参数采集的边缘小程序、老设备数据上云的中间层也适合刚接手 OPC 项目、被 COM 类型库和 DCOM 权限绕晕的人。前提要清楚它锁死在 OPC DA 这一层不是 OPC UA。2. OPC DA 的 COM 底座为什么 VB.NET 调用它要先过三道路径关2.1 先分清 DA 与 UA这份示例锁死在哪一层OPC DA 是九十年代定下来的规范底层是 COM跨进程靠 DCOM接口分成 Custom Interface 和 Automation 两套。OPC UA 是后来重写的一套走 TCP安全模型内置跨平台。两者名字像实现上几乎没有交集。这份示例用的是 DA所以它只能在 Windows 上跑必须注册 COM 组件跨机器访问要额外配 DCOM。这个限制在选型时是第一道分水岭如果项目是新上的、要跨网段、要跑在 Linux 服务器上或者甲方明确要求证书加密和用户认证那 DA 这条路一开始就不该走。但存量产线是另一回事。大量老 PLC、老仪表、老 DCS 只对外提供 DA 服务厂家不再更新固件也没有 UA 网关可买。这时候不是「要不要用 DA」而是「只有 DA 可用」。我在几个改造项目里都遇到过同一种情况采集端用 DA 把数据拿到再转成内部消息格式往外发DA 只是最后一公里不承担安全和跨网职责。这份示例的价值就在这一公里不在别处。还有一点容易被忽略DA 规范有 2.05a 和 3.0 两个常见版本多数服务器两个都实现但 3.0 多了 Browse 的优化接口和部分属性集。示例里用的 Automation 包装对版本差异做了抹平代价是拿不到某些 3.0 才有的细节比如 Item 属性集的完整列表。做普通点位采集够用要做组态级别的元数据管理就得换 Custom Interface。2.2 三套接口OpcAutomation、OpcRcw.Da 与手写定义VB.NET 调 OPC DA实际上有三种写法取舍差别很大。路径依赖组件上手成本单点调用开销适合的点数与场景OpcAutomationOPCDAAuto.dll需 regsvr32 注册低方法名可读较高走 IDispatch 晚期绑定500 点以内、原型验证、VB 系老手OpcRcw.Da.NET 包装OPC 核心组件包里的 OpcCom.Da 等程序集中要理解接口分组低强类型早期绑定上千点、需要属性集与批量读写手写 ComImport 声明无额外依赖高要自己翻译 IDL最低特殊环境、不能装组件包的机器示例走的是第一条。理由很实在代码短一个New OPCServer()加Connect就能通VB.NET 的语法和 Automation 的命名风格天然贴合新手照着改 ProgID 和 Item ID 就能跑起来。缺点是每次调用都要经过一层 IDispatch 分发几千点轮询的时候 CPU 会明显抬起来我在一台低配工控机上测过2000 点、500 毫秒周期光分发开销就吃掉了单核的三成。OpcRcw.Da 那条路更「正」但需要引用核心组件包里的几个程序集接口名从OPCServer变成IOPCServer方法从属性赋值变成AddGroup带一堆参数第一次看会懵。我的习惯是原型期用 Automation 把链路跑通、把 Item ID 全部验证一遍正式版再迁到 Rcw 那套迁移时业务逻辑基本不用动只换数据访问层。手写声明那条路除非现场禁止安装任何组件包否则不建议碰。2.3 把类型库拉进 VB.NET 工程注册、引用与位数对齐真正卡住新手的不是写代码是把类型库正确拉进工程。顺序错了后面全是0x80040154。第一步确认 OPC 核心组件包已经装好OPCDAAuto.dll落在系统目录里。第二步用管理员权限注册。第三步在 VB.NET 工程里添加 COM 引用或者用tlbimp生成互操作程序集。第四步把平台目标改成 x86。:: 管理员命令行注册 32 位自动化包装 :: 关键点32 位 DLL 必须用 SysWOW64 目录下的 regsvr32而不是 System32 下那个 C:\Windows\SysWOW64\regsvr32.exe C:\Program Files (x86)\Common Files\OPC Foundation\OPCDAAuto.dll :: 注册完确认一下能看到 CLSID 才算成功 reg query HKCR\OPC.Automation.1 /s这段命令做两件事把 COM 组件写进注册表以及验证写入结果。regsvr32一定要用对应位数的版本很多机器上直接敲regsvr32命中的是 64 位那个注册完照样报类未注册因为 64 位进程根本看不到 32 位注册表分支的 CLSID。reg query那条是后悔药注册完立刻查一次比等到运行时报错再回头找强。如果工程里不想留 COM 引用比如要做 CI 构建可以提前生成互操作程序集:: 从类型库生成托管互操作程序集产物可随源码一起提交 tlbimp C:\Program Files (x86)\Common Files\OPC Foundation\OPCDAAuto.dll ^ /out:Interop.OPCAutomation.dll ^ /namespace:OPCAutomation ^ /sysarray/out指定产物路径/namespace决定 VB.NET 里Imports的名字/sysarray让 SAFEARRAY 映射成System.Array而不是 object后面遍历数组时省掉一层转换。生成的 DLL 提交进版本库其他机器拉下来直接引用不用每台都注册一遍类型库——但注意运行时那个OPCDAAuto.dll还是得在互操作程序集只是翻译层不是替代品。3. 把示例跑起来从组态服务器到 VB.NET 客户端的四步接线3.1 跑之前先把环境对齐四件必须确认的事动手前有四件事必须确认缺一件后面就要多花两小时排查。第一服务器端自己能跑通。用服务端自带的客户端工具或者任意一个通用 OPC 测试工具连一次能看到点位值在变说明服务端、设备、组态都没问题。这一步不做后面所有报错都分不清是客户端写错了还是服务端没数据。第二位数一致。OPC DA 服务器绝大多数是 32 位 COM 进程客户端如果是 AnyCPU 编译、跑在 64 位运行时上必然连不上。工程属性的「目标平台」改成 x86不要用 AnyCPU。第三权限。本机连本机一般没问题跨机器就必须配 DCOM。示例默认按本机场景写跨机器的坑留到避坑章展开。第四VB.NET 工程的Imports OPCAutomation不报错。报错说明引用没加成功回去看 2.3 那节。3.2 连接与浏览先把服务器里的 Item ID 捞出来连接本身只有两行真正花时间的是确认 ProgID。ProgID 不是服务端软件的名字是它在注册表里登记的标识常见形式是厂商前缀.服务器名.版本必须逐字一致多一个空格都连不上。Imports OPCAutomation Imports System.Runtime.InteropServices Public Class OpcClient 用字段持有引用避免 COM 对象被 GC 提前回收导致后续调用报「对象已断开」 Private _server As OPCServer Public Function Connect(ByVal progId As String) As Boolean Try _server New OPCServer() _server.Connect(progId) Return True Catch ex As COMException 把 HRESULT 打出来排查时比 Message 有用得多 Debug.WriteLine($Connect 失败 HRESULT0x{ex.HResult:X8} {ex.Message}) _server Nothing Return False End Try End Function End ClassConnect的参数就一个 ProgID。方法内部走的是 COM 的CoCreateInstance加IOPCServer查询接口失败时抛的一定是COMException把它单独 catch 住别用Exception兜底否则 HRESULT 会被吞掉。_server声明成类字段而不是局部变量是因为 COM 对象在托管侧只有一个 RCW 引用局部变量出了作用域就可能被回收表现是「明明连上了过几秒再调就报对象已释放」这是很典型的玄学故障。连上之后第一件事是列出这台机器上所有可用的 OPC 服务器确认自己连的那台在列表里Public Function ListServers() As String() Dim raw As Object Nothing _server.GetOPCServers(raw) 返回本机已注册的 ProgID 列表 Dim arr As Array CType(raw, Array) Dim result(arr.Length - 1) As String For i As Integer 0 To arr.Length - 1 result(i) arr.GetValue(i).ToString() Next Return result End FunctionGetOPCServers的参数是 ByRef 的 ObjectVB.NET 里必须先声明一个Nothing变量传进去返回值塞在里面不能直接当函数返回值用。这个方法枚举的是注册表里OPC.Server分支下的条目也就是本机装了哪些服务端不是网络上能连到哪些。跨机器枚举要另配 DCOM 的远程注册表访问权限一般不做。拿到服务器列表后接着浏览它内部的 Item ID 树。这一步比写代码重要Item ID 拼错一个字读回来就是 Bad 质量Public Sub DumpTags() Dim browser As OPCBrowser _server.CreateBrowser() Walk(browser, 0) browser Nothing 显式放掉浏览器对象也占 COM 引用 End Sub Private Sub Walk(ByVal browser As OPCBrowser, ByVal depth As Integer) If depth 6 Then Return 防止异常组态导致无限递归 Dim branches As Object browser.ShowBranches() For Each name As Object In CType(branches, Array) browser.MoveDown(name.ToString()) Walk(browser, depth 1) browser.MoveUp() Next Dim leaves As Object browser.ShowLeafs(False) For Each name As Object In CType(leaves, Array) CurrentPosition 是当前所在分支路径GetItemID 把短名拼成完整 Item ID Console.WriteLine(${browser.CurrentPosition}\{name} {browser.GetItemID(name.ToString())}) Next End SubShowBranches返回当前层的子分支ShowLeafs(False)返回当前层的叶子节点参数False表示只列本层、不递归展开。MoveDown/MoveUp是游标移动必须成对出现异常路径上漏了MoveUp下一次遍历的位置就是错的。depth 6这个限制不是拍脑袋某次现场的分支结构里存在一个指回上层的环不加保护直接栈溢出。GetItemID拿到的是完整路径而ShowLeafs给的是短名很多新手直接把短名塞进AddItem服务器当然找不到。3.3 同步读写一次拿一批别一个点一个点地读建组、加项、读值这三步是采集程序的骨架。组Group是 OPC 里的基本调度单位刷新周期、激活状态、订阅开关都挂在组上一个客户端可以建多个组按不同频率分别刷新。Public Function BuildGroup(ByVal name As String, ByVal updateRateMs As Integer) As OPCGroup Dim group As OPCGroup _server.OPCGroups.Add(name) group.UpdateRate updateRateMs 服务器建议刷新周期毫秒 group.IsActive True 组处于激活态才会真正去设备取数 group.IsSubscribed True 打开订阅DataChange 才会回调 group.Deadband 0 模拟量变化死区百分比0 表示任何变化都上报 Return group End Function Public Function AddTag(ByVal group As OPCGroup, ByVal itemId As String, ByVal clientHandle As Integer) As OPCItem clientHandle 是客户端自己编的号回调里用它反查是哪个点与服务器无关 Return group.OPCItems.AddItem(itemId, clientHandle) End FunctionOPCGroups.Add的参数是组名重名会抛异常所以程序里最好用「业务名 序号」的规则。UpdateRate是客户端给服务器的建议值服务器不一定采纳实际周期以回调时间戳为准。IsActive和IsSubscribed是两个独立开关前者控制组是否去设备取数后者控制取到之后是否回调客户端两个都打开才有数据推送。Deadband只在模拟量上有意义设成 0 意味着哪怕变化 0.001 也上报点数多的时候会把回调线程打满。同步读适合启动时一次性拉全量不适合放进循环Public Sub ReadBatch(ByVal group As OPCGroup, ByVal handles As Array) Dim values As Object Nothing Dim errors As Object Nothing Dim qualities As Object Nothing Dim stamps As Object Nothing 第一个参数 OPCDevice 表示直接读设备OPCCache 读服务器缓存快但可能滞后 group.SyncRead(OPCDataSource.OPCDevice, handles.Length, handles, values, errors, qualities, stamps) For i As Integer 0 To handles.Length - 1 Dim v As Object CType(values, Array).GetValue(i) Dim q As Short CShort(CType(qualities, Array).GetValue(i)) Dim e As Integer CInt(CType(errors, Array).GetValue(i)) If e 0 Then Console.WriteLine($handle{handles.GetValue(i)} 读取错误 0x{e:X8}) Else Console.WriteLine($handle{handles.GetValue(i)} value{v} quality0x{q:X2}) End If Next End SubSyncRead的七个参数依次是数据源、点数量、服务器句柄数组、值数组、错误码数组、质量码数组、时间戳数组。后四个都是输出参数必须先声明成Nothing再传。handles必须是AddItem时返回的 ServerHandle不是 ClientHandle这两者搞混是高频错误。一次读一批比循环单点读快一个数量级因为省掉了 N-1 次跨进程调用的固定开销。写回值走SyncWrite参数更少但错误处理不能省Public Function WriteOne(ByVal group As OPCGroup, ByVal serverHandle As Integer, ByVal value As Object) As Integer Dim errors As Object Nothing group.SyncWrite(1, New Object() {serverHandle}, New Object() {value}, errors) 返回 0 表示写成功非 0 是服务器侧拒绝常见于量程越界或点位只读 Return CInt(CType(errors, Array).GetValue(0)) End FunctionSyncWrite的四个参数是点数量、服务器句柄数组、值数组、错误码数组。值的类型必须和点位定义的类型匹配把一个Double写进VT_I2的点有的服务器会自动截断有的直接返回0xC0040003。写操作在多数产线上要谨慎示例里把它单独拎出来做演示实际项目里我一般会在写入前加一道量程校验。3.4 异步订阅DataChange 回调与界面刷新真正跑长稳的采集程序不会用同步轮询用的是订阅。组打开IsSubscribed之后服务器在数据变化时回调客户端的DataChange事件客户端只负责处理不占轮询线程。Public Sub StartPush(ByVal group As OPCGroup) AddHandler group.DataChange, AddressOf OnDataChange End Sub Private Sub OnDataChange(ByVal TransactionID As Long, ByVal NumItems As Long, ByVal ClientHandles As Object, ByVal ItemValues As Object, ByVal Qualities As Object, ByVal TimeStamps As Object) 回调来自 COM 的线程池不是 UI 线程直接碰控件会抛跨线程异常 If Not Me.IsHandleCreated Then Return Me.BeginInvoke(New Action(Of Long, Long, Object, Object, Object, Object)(AddressOf RenderRows), TransactionID, NumItems, ClientHandles, ItemValues, Qualities, TimeStamps) End Sub Private Sub RenderRows(ByVal txId As Long, ByVal count As Long, ByVal handles As Object, ByVal values As Object, ByVal qualities As Object, ByVal stamps As Object) Dim h As Array CType(handles, Array) Dim v As Array CType(values, Array) Dim q As Array CType(qualities, Array) Dim t As Array CType(stamps, Array) For i As Integer 0 To count - 1 Dim row As DataGridViewRow gridMap(CInt(h.GetValue(i))) 用 ClientHandle 反查行号 row.Cells(Value).Value v.GetValue(i) row.Cells(Quality).Value QualityText(CShort(q.GetValue(i))) row.Cells(Stamp).Value CDate(t.GetValue(i)).ToString(HH:mm:ss.fff) Next End Sub六个参数里ClientHandles是加项时自己编的号用它反查界面行号比用 ServerHandle 稳因为重连之后 ServerHandle 会变而 ClientHandle 不变。NumItems是本次回调携带的点数服务器可能攒一批再推不是每次一个。BeginInvoke是必须的COM 回调线程是 MTAWinForms 控件只能在创建它的 STA 线程上碰直接赋值的结果轻则界面不刷新重则整个进程无响应。质量码的判断单独抽一个函数别在回调里写 if-else 链Private Function QualityText(ByVal q As Short) As String OPC 质量码高两位是主状态11Good01Uncertain00Bad Select Case (q And HC0) Case HC0 : Return Good Case H40 : Return Uncertain Case Else : Return Bad End Select End Function4. Item ID、Variant 与刷新周期参数设错比代码写错更难查4.1 Item ID 的三种写法与浏览树的关系Item ID 是 OPC 里唯一标识一个点位的字符串格式完全由服务器决定没有统一标准。常见三种扁平式Temperature1、分层式Channel1.Device1.Tag001、带命名空间的ns2;sMachine.Temp。前两种在 DA 里最常见第三种多见于从 UA 网关映射过来的场景。浏览树和 Item ID 不是一回事。树上的分支名是给人看的短名Item ID 是给协议用的全名。比如树上看到设备1下面有个温度完整 Item ID 可能是通道1.设备1.温度也可能带前缀后缀。稳妥做法是走一遍 3.2 的GetItemID把浏览结果导出成清单人工核对一遍再写进配置。我在一个项目里见过把Tag001写成Tag1的服务器不报错只是回 Bad查了半个下午。配置化的写法是把 Item ID、ClientHandle、数据类型、量程、单位做成一张表从文件或数据库读代码里不出现硬编码字符串。示例为了简洁写在常量里实际项目我一般都改成外部配置换产线不用重新编译。4.2 Variant 类型码到 .NET 类型的对照Automation 接口传值全走VARIANT托管侧拿到的是Object装箱的类型由 Variant 的vt字段决定。类型对不上做算术就会抛InvalidCastException或者更隐蔽地得到截断值。Variant 类型码十六进制托管类型常见来源VT_I20x0002Int16老 PLC 的整型寄存器VT_I40x0003Int32计数器、累计量VT_R40x0004Single单精度模拟量VT_R80x0005Double温度、压力VT_BSTR0x0008String批次号、状态文本VT_BOOL0x000BBoolean开关量、报警位VT_UI10x0011Byte字节型状态字VT_ARRAY or VT_UI10x2011Byte()配方、波形块注意VT_BOOL映射到Boolean而不是Integer有些服务器会把开关量以VT_I2的 0/1 返回代码里用CBool强转是安全的但用CInt再比较更保险。数组类型只在大块数据配方、波形上出现普通点位不用管。4.3 UpdateRate、PercentDeadband 与回调频率UpdateRate是客户端给服务器的建议扫描周期。设成 100 毫秒服务器可能按 100 毫秒扫也可能按自己的最小周期扫还可能因为点数太多自动降频。判断实际周期的唯一依据是回调时间戳的差值不是这个属性值。Deadband是死区单位是百分比只对模拟量生效。设 0 表示任何变化都推设 1 表示变化超过量程的 1% 才推。这个参数在点数百级以上时影响巨大一个温度点在 0.1 度范围内来回抖动死区为 0 的话每秒可能推十几次全部变成界面刷新和写库操作。我一般的配置是关键工艺参数死区 0.5%一般监测点 1% 到 2%开关量无所谓死区。刷新周期按业务定界面展示 500 毫秒足够写历史库 1000 到 5000 毫秒报警检测单独建一个高优先级组200 毫秒。全塞一个组里等于用最严的周期拖所有点。4.4 质量码与时间戳判断值可不可信质量码是 OPC 里最有价值也最容易被忽略的字段。值本身可能是 0也可能是上一秒的残留只有质量码能说明这个值当前可不可信。主状态三位Good表示值来自设备且正常Uncertain表示值可能是缓存或设备处于降级状态Bad表示取不到。子状态位还能细分比如Bad下面有「配置错误」「设备故障」「通信失败」等。判断时只看高两位就够九成场景需要细分再看低六位。时间戳是服务器侧采到该值的时刻不是客户端收到的时刻。两个时间相减能看出链路延迟延迟突然变大往往预示网络或服务器负载出问题比等报警出现早得多。我习惯在采集程序里加一个延迟监控超过阈值就记一条日志事后排查很有用。5. 避坑与排查OPC DA 在 .NET 里最常见的五个翻车点5.1 现象一运行就报 0x80040154 类未注册原因基本只有一个位数不对。OPCDAAuto.dll是 32 位 COM 组件注册在 32 位注册表分支HKCR\Wow6432Node\CLSID而 VB.NET 工程默认编译成 AnyCPU在 64 位系统上跑起来是 64 位进程查 64 位注册表分支自然找不到 CLSID。第二种可能是根本没注册或者注册到了另一台机器上。解决分三步工程属性里把目标平台改成 x86 并重新编译确认用SysWOW64\regsvr32.exe注册过注册完用reg query HKCR\OPC.Automation.1验证。如果工程必须跑 64 位那只能换 OpcRcw.Da 那套或者找 64 位的服务端不能靠改配置绕过去。5.2 现象0x80070005 拒绝访问本机能连的换台机器就连不上原因是 DCOM 权限。跨机器访问时客户端进程以当前登录用户身份去激活服务器端的 COM 对象服务器端的 DCOM 配置里没给这个用户「本地启动」和「本地激活」权限直接被拒。解决要在服务器端改组件服务里找到对应 OPC 服务器的 DCOM 配置安全标签页把「启动和激活权限」「访问权限」都改成自定义把客户端账号加进去并给全部权限身份标签页选「交互式用户」或者指定一个双方都有的账号。改完必须重启服务器端的 OPC 服务不重启不生效。另外两台机器的账号密码最好一致否则还要处理凭据映射。5.3 现象连接成功但读回来的值全是 Bad原因通常不在代码。第一位是 Item ID 拼错服务器对不存在的 Item 不报错只是回 Bad。第二位是设备侧没数据PLC 没运行、通道没组态、地址越界服务器照样能建项但值是 Bad。第三位是组没激活IsActive忘了设True。排查顺序先用服务端自带的诊断工具读同一个 Item ID能读出 Good 说明问题在客户端读不出说明问题在服务端和设备。客户端这边依次检查AddItem的返回值是否有效、IsActive和IsSubscribed是否都为 True、SyncRead的第一个参数是OPCDevice还是OPCCache——读缓存拿到旧值是正常的但 Bad 不正常。5.4 现象界面卡死几秒或者弹出跨线程操作控件异常原因是 COM 回调线程和 UI 线程不是同一条。DataChange由 COM 的线程池回调默认 MTA而 WinForms 控件只能在创建它的 STA 线程上操作。直接赋值轻则界面不刷新重则抛InvalidOperationException更糟的情况是死锁界面卡住几秒再恢复。解决就是把回调里的界面操作全部BeginInvoke到 UI 线程用Action委托带参数过去。不要用Invoke同步等待回调线程被阻塞久了服务器侧的推送队列会堆积。另外如果自己起后台线程去调 COM记得SetApartmentState(ApartmentState.STA)并且只能在Start之前设运行中改会抛异常。5.5 现象跑两天内存涨到 2GB句柄数一路往上原因是 COM 引用没释放。OPC 的对象层级是 Server → Group → Item每一层都得显式移除只把变量设成Nothing不等于释放RCW 还在服务器侧的组和项也还挂着。长期运行下来服务器端组数量越堆越多客户端侧 RCW 越积越厚。正确顺序是先RemoveItem逐个删项再OPCGroups.Remove删组再Disconnect最后ReleaseComObject。事件处理器也要RemoveHandler摘掉否则回调会一直持有窗体的引用。我在一个跑了三天的采集程序上抓过因为漏了RemoveHandler窗体关了内存还在涨最后是加了个定时器每小时打印一次组数量才定位到。6. 从示例到产线把订阅包进状态机再用一张表验证示例能跑通离能上产线还差一段断线重连。产线网络会抖服务器会重启设备会断电Connect成功一次不代表永远在线。我的做法是把连接状态抽成一个四态机Disconnected、Connecting、Online、Backoff由一个定时器驱动流转退避时间按失败次数指数增长封顶 30 秒。6.1 断线重连状态机把 Connect 包进可退避的循环Public Enum LinkState Disconnected 0 Connecting 1 Online 2 Backoff 3 End Enum Public Class LinkGuard Private _state As LinkState LinkState.Disconnected Private _failCount As Integer 0 Private ReadOnly _maxBackoffMs As Integer 30000 Public Sub Tick(ByVal client As OpcClient, ByVal progId As String) Select Case _state Case LinkState.Disconnected, LinkState.Backoff _state LinkState.Connecting If client.Connect(progId) Then _state LinkState.Online _failCount 0 Else 失败次数封顶 8避免位移溢出 _failCount Math.Min(_failCount 1, 8) _state LinkState.Backoff End If Case LinkState.Online 心跳读一个固定点位失败即判定链路已断 If Not client.Ping() Then client.Dispose() 内部负责 RemoveHandler / RemoveGroup / Disconnect _failCount Math.Min(_failCount 1, 8) _state LinkState.Backoff End If End Select End Sub Public Function NextDelayMs() As Integer If _failCount 0 Then Return 1000 1s, 2s, 4s, 8s, 16s, 30s 封顶 Return Math.Min(1000 * (1 (_failCount - 1)), _maxBackoffMs) End Function End ClassTick由 WinForms 定时器每秒调一次状态机内部不做任何阻塞等待避免卡住 UI。Ping是一次小范围SyncRead读一个必定存在的点位异常或错误码非零都算失败。Dispose里把释放顺序固定下来摘事件、删项、删组、断开、释放 RCW一步都不能少这是 5.5 那条坑的正面解法。NextDelayMs用位移算退避失败次数封顶 8 是防止1 n在 n 很大时溢出成负数。6.2 用一张对照表验证改造是否真的稳了改完之后不能靠「看着没报错」判断得有可量化的对照。我在最近一个采集项目上做了两组各 24 小时的对比指标如下。指标同步轮询版订阅 状态机版观测方式平均 CPU 占用22%7%进程性能计数器采集周期抖动±180 ms±25 ms回调时间戳差值统计断线恢复耗时需人工重启平均 4.2 s拔网线后计时24 小时组数量从 3 涨到 41恒定为 3定时打印OPCGroups.Count内存峰值1.6 GB240 MB任务管理器采样最后一行是重点。组数量必须恒定一旦只增不减说明释放链路有问题早晚会走到 5.5 那个结局。我的习惯是在采集程序里挂一个每小时的日志打印组数量、项数量、回调频率、平均延迟四个数这四行日志比任何监控面板都直观。验证阶段还有个小技巧故意把服务端停掉再启动观察状态机是不是按 1、2、4、8 秒的节奏退避恢复后组数量是不是回到初始值。这一步走通了才算真能用。血泪经验是我早年接手一个改造项目示例代码原封不动搬上去连接写死一次跑了半年赶上一次服务器升级采集中断了两天没人发现因为程序不报错、界面还显示着最后一批值。从那以后我每次写 OPC 客户端都强制加三样东西心跳读、组数量日志、质量码统计。质量码统计尤其重要Good 的比例跌破 99% 就该有人看一眼了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?