简介:VB .NET OPC通讯示例包面向工业自动化领域的.NET开发者,系统演示基于OPC DA/UA协议实现客户端程序的全流程,涵盖OPC Foundation库的引用、Group与Item分组模型、本地或远程服务器连接、数据异步读取写入、事件订阅以及多线程并发处理等关键环节。压缩包内共290个文件,以vb、frm、resx、cls等VB工程文件为主体,辅以h、cpp、cs等跨语言源码和sln、vbproj工程配置,同时包含可直接运行的exe、dll,整体约2.5MB,目录结构清晰,便于按模块对照学习。已有607人学习,尤其适合需要在.NET Framework 2003至2010各版本之间移植OPC功能的开发者。资源不仅提供从简单数据读写到自定义接口实现的多级示例,还展示了Group分组管理、异常与错误处理、多线程异步操作等工程化写法,能够帮助开发者深入理解OPC通信原理,并快速构建稳定可用的工业数据采集与写入方案。
1. VB .Net OPC 通讯例子:为什么现成例子总是不够用
在工业现场抓数据,很多开发者都会先搜“VB .Net OPC 通讯例子”,复制下来改一改就指望能连上 PLC。现实往往是:例子在开发机上跑得好好的,一搬到车间就报错,或者连上了但读写不稳定。问题不出在代码逻辑,而在协议栈选型、组件位数、DCOM 权限这类例子不会写出来的细节。下面内容不讲空概念,直接给出一个能照着改的 DA 例子和一个 UA 例子,并把现场最容易翻车的配置和排查顺序一并讲清楚,适合那些要在设备数据采集、产线看板、质量追溯项目里把 OPC 通道打通后再去维护后续业务的人。
2. 协议与依赖选型:DA 还是 UA,决定你后面调试方式
2.1 OPC DA 与 OPC UA 的本质差异:一个靠 DCOM,一个靠证书会话
OPC DA 和 OPC UA 虽然都挂着一个 OPC 的名字,底层却完全是两套实现。OPC DA 把通讯建立在 COM/DCOM 之上,要求在客户端和服务器之间完成一套 Windows 平台独有的注册、激活、调用流程。这套机制在单机环境下几乎没有障碍,一旦跨机器,DCOM 就会化身成一个黑匣子:端口随机、身份验证来自注册表、授权失败时只给一句“拒绝访问”。很多老产线还在使用这种结构,因为设备端的 OPC 服务器只提供 DA 接口,上层系统想采集数据,就只能顺着老路走。
OPC UA 则把通讯层完全重写了,底层换成了 TCP 或 HTTPS 上的协商会话,身份校验用应用证书完成。它不需要配置 DCOM 的那一长串权限,但多了证书信任和端口管理两个新任务。比如大多数 OPC UA 设备默认监听 4840 端口,防火墙放行后才能握手成功。在 VB .NET 里,接入 DA 的常规做法是添加 COM 引用,创建 OPC 自动化对象;接入 UA 则是引用托管客户端 SDK,代码更像普通类库,调试的时候也更好观察内部状态。
选型我一般不会等到写代码时再纠结,而是对着现场设备资料先确认一次:对方给的是 DA 接口还是 UA 接口。新建项目而且设备支持 UA 时,优先 UA;老产线只有 DA 服务器,或者上位机系统强制要求 DA,就老老实实 DA。两种协议混接在一个程序里不是不行,但连接、鉴权、断线重连的逻辑完全不同,混用会把你排查故障的时间拉长一倍。从工程成本上看,UA 虽然增加了证书管理,但它不需要 DCOM 动态端口,后期多台客户端部署时比 DA 省事得多。经验告诉我,当客户追问“为什么点位读出来是空值”时,我基本能根据协议类型提前判断出问题出在哪一侧,这种判断力来自对协议栈的理解,不是某个调试技巧。
2.2 版本、位数与依赖清单:动手写代码前先确认这三件事
最容易踩的一个坑,是 OPC DA 的 COM 组件在 64 位系统里没有被注册到正确的位置。VB .NET 项目默认平台目标是 AnyCPU,跑在 64 位系统里就是 64 位进程,可不少老 OPC 服务器的自动化组件只提供了 32 位版本,注册表里的 CLSID 只存在于 Wow6432Node 分支下,64 位进程按 ProgID 去创建对象时直接报“类未注册”。我只要确认要走 DA,第一件事就是把项目平台目标改成 x86,这个习惯帮我省掉了大量玄学问题。现在的工控机基本都是 64 位 Windows 也能运行 32 位程序,所以这样改几乎没有副作用。
第二个要确认的是服务器到底部署在哪里。DA 服务器和客户端在同一台机器上时,Connect 里的主机名可以传 Nothing;跨机器时就必须确认 DCOM 权限,并且固定一个服务账号来运行 OPC 服务器。很多工程师只把电脑加入同一个工作组就以为万事大吉,结果在服务器端一台机器装完组件后还是连不上,最后发现是防火墙没给动态端口留空间。我的做法是在组件服务里把 DCOM 的动态端口范围固定下来,避免每次通讯都随机协商一个新端口,抓包时能看到一个相对稳定的连接模式,现场排障会舒服很多。
第三个是依赖组件。DA 客户端机器必须安装并注册 OPC Core Components,否则连服务器列表都会是空的。UA 客户端则需要对应 SDK 的运行时,以及正确存放证书的位置。这些东西在例子里不会写,代码只负责调用,环境不到位,代码再对也跑不起来。所以每次接到任务,我不急着打开开发环境建工程,而是先到现场把这三件事确认完,再开始写代码。
2.3 用一张检查表确定协议选型
我把进场排查的顺序整理成一张表,每次到新现场都按这个走一遍:
| 检查项 | DA 重点关注 | UA 重点关注 |
|---|---|---|
| 通讯端口 | 不固定,受 DCOM 动态端口影响 | 默认 4840,需防火墙放行 |
| 身份校验 | DCOM 用户权限 | 应用证书 + 用户令牌 |
| 位数一致性 | 客户端与 COM 组件位数必须一致 | 托管代码无需过分关心 |
| 依赖安装 | OPC Core Components | SDK 运行时库 |
| 断线后的行为 | 连接对象原地失效,需重建 | 会话可重连,但订阅需重建 |
这张表列的就是我选型时的底线要求。DA 行里最难的是“位数一致”和“断线后的行为”,UA 行里最难的是“证书信任”。后面两章分别给出最小例子,你再回头看这张表,每个坑都能对上号。
3. 最小可跑的 VB .Net OPC DA 通讯例子
3.1 添加 COM 引用与建立连接的核心代码
OPC DA 的自动化接口在 Windows 里是一个 COM 组件。VB .NET 项目里添加 COM 引用后,就能使用 OPCServer、OPCGroup、OPCItem 这些互操作类。我一般会在项目里加一个独立的客户端类,把连接、断开、读、写都封装起来,业务层不直接碰 COM 对象。下面代码先建立一个可以复用的连接类:
Imports OPCAutomation Imports System.Runtime.InteropServices Public NotInheritable Class OpcDaClient Private server As OPCServer Public Function Connect(serverProgId As String, hostName As String) As Boolean Try ' 创建 COM 服务器对象,实际就是实例化一个 OPC 服务器的自动化包装 server = New OPCServer() ' 参数一 serverProgId: OPC 服务器的注册标识,不要自己编 ' 参数二 hostName: 机器名,本机传 Nothing,跨机器传目标机器名 ' 参数三保留参数,固定传 Nothing server.Connect(serverProgId, hostName, Nothing) Return server IsNot Nothing Catch ex As Exception Console.WriteLine("连接失败: " & ex.Message) Return False End Try End Function Public Sub Disconnect() If server Is Nothing Then Return Try ' 先清掉所有组,再断开,顺序反了会让组对象残留 If server.OPCGroups.Count > 0 Then server.OPCGroups.RemoveAll() server.Disconnect() Marshal.FinalReleaseComObject(server) Catch Finally server = Nothing End Try End Sub End Class这段代码的关键在第一处 Try 块。ProgID 不是代码里写出来的字符串,而是 OPC 服务器安装时写入注册表的注册标识。现场最好用服务器自带的配置工具查看,或者用 OPC 枚举工具扫描一遍,确认了 ProgID 再填进 Connect。如果你只照着网上的例子把某个名字写死,很容易在 Connect 这一步就抛“服务器未找到”。Connect 的第三个参数在自动化接口里是一个保留参数,大多数实现要求传 Nothing,传了字符串反而可能多一段不必要的解析。
断开连接的部分容易被忽略。许多人写程序只调用 Disconnect 就结束,但 COM 对象的引用计数不会立刻归零,进程关不掉,后台会残留一个不可见的进程。我在 Disconnect 里先 RemoveAll 清掉所有组,再 Disconnect,最后用 FinalReleaseComObject 强制释放,这样可以保证下次启动时不会出现端口占用或者服务器进程无法退出的问题。
3.2 按 Tag 读取点位:组、项与同步读的完整写法
OPC DA 的读取并不是直接给服务器一个字符串就返回值。你需要先创建一个“组”对象,往组里加入“项”,项指向 OPC 服务器的具体数据点,然后再用 SyncRead 方法读取。为什么要这层间接关系?因为服务器为了性能,把一组点位聚到同一个组里,统一做缓存和死区过滤,这样比一个个独立读要高效得多。下面是一个读取单个点位的代码:
Public Function ReadTag(groupName As String, itemId As String, clientHandle As Integer) As Object If server Is Nothing Then Throw New InvalidOperationException("请先调用 Connect") Dim group As OPCGroup = Nothing Dim item As OPCItem = Nothing Try ' 添加一个激活的组,更新周期 1000 毫秒,客户句柄为 1 group = server.OPCGroups.Add(groupName, True, 1000, clientHandle, 0) ' 第二个参数是客户端句柄,写 2,方便回调时识别 item = group.OPCItems.AddItem(itemId, 2) ' 构造出数组参数,OPC Automation 接口要求传数组 Dim itemCount As Integer = 1 Dim itemHandles As Array = Array.CreateInstance(GetType(Integer), itemCount) itemHandles.SetValue(item.ServerHandle, 0) Dim values As Array = Nothing Dim errors As Array = Nothing Dim qualities As Array = Nothing Dim timestamps As Array = Nothing ' 第一个参数 1 表示从设备读取;第二个参数 1 表示读 1 个项 group.SyncRead(1, itemCount, itemHandles, values, errors, qualities, timestamps) If errors.GetValue(0) = 0 AndAlso values.GetValue(0) IsNot Nothing Then Return values.GetValue(0) Else Return Nothing End If Finally ' 这个示例里每次临时建组,用完即删;正式项目建议缓存句柄 If group IsNot Nothing Then server.OPCGroups.Remove(group) End Try End Function这段代码里最重要的概念是“组”和“项”。ReadTag 返回的是 Object,因为 OPC 服务器读回来的值可能是布尔、数值、字符串或时间,VB .NET 里直接拿 Object 接,在业务层再做类型转换。SyncRead 的参数顺序非常容易写错,尤其是第一位的 Source 参数:传 1 是让服务器从设备数据源读取,传 0 是从服务器缓存读取。如果你需要实时数据,用 1;如果只是看历史趋势,用 0 会更快,但可能拿不到最新值。
后面四个数组分别代表服务器句柄、值、错误码、质量、时间戳。判别是否成功的依据是 errors 数组,而不是 values 是否为空。有时候 values 里会有值,但 errors 返回的却是非零错误码,那是设备端的数据质量不可靠,不能当作正常数据用。质量数组在 DA 里通常 192 表示 good,但为了安全,我仍建议以 error 为准,质量数组只用于辅助判断。
3.3 写入点位:同步写的参数陷阱与缓存思路
写入在 OPC DA 里同样需要一个组和一个项,但调用的方法是 SyncWrite。很多新手把写入想成本地赋值,直接调用一个 WriteValue 方法,结果发现没有任何效果,原因就是没有先建立“项”这个中间对象。下面是一个写入的最小实现:
Public Function WriteTag(groupName As String, itemId As String, value As Object, clientHandle As Integer) As Boolean Dim group As OPCGroup = Nothing Try group = server.OPCGroups.Add(groupName, True, 1000, clientHandle, 0) Dim item As OPCItem = group.OPCItems.AddItem(itemId, 3) Dim itemCount As Integer = 1 Dim srvHandles As Array = Array.CreateInstance(GetType(Integer), itemCount) srvHandles.SetValue(item.ServerHandle, 0) Dim values As Array = Array.CreateInstance(GetType(Object), itemCount) values.SetValue(value, 0) Dim errors As Array = Array.CreateInstance(GetType(Integer), itemCount) ' NumItems 传 1,后面三个数组按位置对齐 group.SyncWrite(itemCount, srvHandles, values, errors) Return Convert.ToInt32(errors.GetValue(0)) = 0 Finally If group Is Not Nothing Then server.OPCGroups.Remove(group) End Try End Function写操作有三个容易翻车的地方。一是值的类型必须和设备数据类型一致,设备端定义的是 INT 型,你传一个字符串进去,服务器会直接返回类型不匹配错误。二是写操作对某些设备点是危险的,比如写转速、写温度设定值,建议在工控程序里加一道确认按钮,不要用定时器自动写。三是错误码必须记录,OPC 写失败的原因很多,值域超过上限、设备离线、服务器写保护都会返回错误,直接 Return False 后要把错误码写入日志,否则后面无从定位。
这个 DA 例子虽然是“最小”,但已经能把组、项、句柄这些核心概念带出来。它适合跑通链路,不适合直接上生产。生产的做法是把组和项的句柄缓存起来,不要每次读都 AddItem,这样调用频率高了以后才不会越来越卡。我在最后一章会专门讲缓存和重连的思路。
4. 最小可跑的 VB .Net OPC UA 通讯例子
4.1 连接 UA 服务器:端点发现、证书处理与会话保持
OPC UA 客户端在 .NET 生态下有多种实现,但代码脉络基本统一:加载应用配置,发现服务器端点,选择一条安全策略,创建会话。和 DA 相比,UA 不需要处理 DCOM,但多出一套“证书”的管理步骤。下面按主流 UA 客户端 SDK 的调用方式来写,类名和命名空间在现成 SDK 里几乎一致。不同的 SDK 在构造函数的参数上可能有细微差别,粘贴后先看智能感知对齐参数名就行了。
Imports Opc.Ua Imports Opc.Ua.Client Public Class OpcUaClientExample Private _session As Session Public Function Connect(endpointUrl As String) As Boolean ' 应用配置:UA 客户端需要一个 ApplicationName 和 ApplicationUri Dim config As New ApplicationConfiguration With { .ApplicationName = "VB.NET OPC UA Demo", .ApplicationUri = "urn:demo:vbnet:opcua", .SecurityConfiguration = New SecurityConfiguration With { .AutoAcceptUntrustedCertificates = True } } ' 发现端点:服务器可能暴露多个安全策略,这里取第一个“支持匿名 + 无安全”的端点 Dim discovery = New DiscoveryClient(endpointUrl) Dim endpoints As EndpointDescription() = discovery.GetEndpoints() discovery.Close() ' 在实际项目里,建议根据服务器要求选择 Basic256Sha256 策略 Dim endpoint As EndpointDescription = endpoints(0) ' 创建会话。最后一个参数是会话超时毫秒数,比如 60000 _session = Session.Create(config, endpoint, "vbnet-client", 60000, New UserIdentity(Nothing), Nothing) Return _session IsNot Nothing AndAlso _session.Connected End Function Public Sub KeepAlive() ' 每 5 秒发送一次心跳,避免中间的网关或设备把空闲会话断开 _session.KeepAliveInterval = 5000 End Sub End Class这段代码里最应该警惕的是 AutoAcceptUntrustedCertificates。开发环境打开它很省事,但到了生产环境一定要关掉,改成把服务器证书导入到 Windows 受信任的根证书存储里。否则你为了图方便默认接受所有证书,等于把 OPC UA 的安全机制直接拆掉,在合规审计时很难交代。Session.Create 的第五个参数是 UserIdentity,这里传 Nothing 代表匿名,大多数设备服务器默认开启匿名通道;如果设备设置了用户名密码,则需要传 New UserIdentity("用户名", "密码")。第六个参数是语言列表,按客户端系统语言自动匹配,没有特殊需求可以不传。
端点发现这里还有一个常见问题:有的服务器只监听本机地址,不回地上报端点。如果你在工控机上测试时用 127.0.0.1 能连,换到局域网 IP 就找不到端点,要么服务器改监听地址,要么检查防火墙是不是挡掉了 4840 端口的入站规则。现场最常见的现象是 Windows 防火墙弹窗没有点允许,后面节点列表一片空白。
4.2 读节点、写节点与订阅数据变化
UA 里节点的标识通常写成“ns=2;s=标识名”这样的字符串。ns 是命名空间索引,s 是字符串类型的标识。读取一个节点,只需构造 NodeId 再调用 ReadValue;写一个节点,则把目标属性打包成 WriteValue。下面把读写和订阅都串起来:
Public Function ReadNode(nodeIdString As String) As Object ' NodeId 可以由字符串直接解析,格式:ns=2;s=xxx Dim nodeId As New NodeId(nodeIdString) Dim value As DataValue = _session.ReadValue(nodeId) Return value.Value End Function Public Function WriteNode(nodeIdString As String, newValue As Object) As Boolean Dim nodeId As New NodeId(nodeIdString) Dim valueToWrite As New Variant(newValue) Dim writeValue As New WriteValue With { .NodeId = nodeId, .AttributeId = Attributes.Value, .Value = New DataValue(valueToWrite) } ' Write 方法接受一个 WriteValue 数组,可以一次写多个点 Return _session.Write(New WriteValue() {writeValue}) = StatusCodes.Good End Function Public Sub SubscribeNode(nodeIdString As String, groupName As String) Dim subscription As New Subscription(_session) With { .PublishingInterval = 500, .KeepAliveCount = 10 } Dim nodeId As New NodeId(nodeIdString) Dim monitor As New MonitoredItem With { .StartNodeId = nodeId, .AttributeId = Attributes.Value, .SamplingInterval = 250, .QueueSize = 5 } ' 数据变化时触发回调,VB.NET 里可以用 AddressOf 绑定到方法 AddHandler monitor.DataChange, AddressOf OnDataChange subscription.AddItem(monitor) _session.AddSubscription(subscription) subscription.Create() End Sub Private Sub OnDataChange(monitor As MonitoredItem, e As MonitoredItemNotificationEventArgs) Dim value As DataValue = DirectCast(e.NotificationValue, DataValue) Console.WriteLine("节点变化: " & monitor.StartNodeId.ToString() & " 值: " & value.Value) End Sub读写参数要区分清楚:ReadValue 是同步读,适合单点;Write 方法是批量写,它接收的参数是 WriteValue 数组,所以哪怕只写一个点也要构造数组。订阅这部分,PublishingInterval 是订阅的发布间隔,也就是服务器检查数据变化并向客户端推送的周期,500 毫秒是个偏保守但 CPU 占用不高的值;SamplingInterval 是采集周期,250 毫秒代表设备侧采样频率,两者不是一个概念。很多代码里把这两个值搞混,导致订阅的数据变化频率和预期完全不同。QueueSize 表示服务器端在客户端没有及时取数据时最多缓存几条,数值太小会丢数据,数值太大会增加内存占用。
订阅的优点是服务器只在数据变化时推送,比线程里 while 循环轮询省资源。但它也有代价:如果现场网络闪断,订阅会在会话重建后丢失,你必须在重连逻辑里重新创建订阅。这个我在避坑章节里会单独说。实际项目里不要每变化一次就创建一个订阅,最好是把需要监听的节点集中放在同一个订阅里,让服务器一次性管理。
5. OPC 通讯避坑与排查:高频问题记录
5.1 连接 DA 服务器报“拒绝访问”,DCOM 权限背锅
现象是 VB .NET 程序在开发机上连本机 OPC 服务器完全没有问题,部署到另一台电脑后,代码在 server.Connect 这一步抛异常,错误信息是“拒绝访问”甚至“服务器运行失败”。很多人的第一反应是代码写错,其实这是 DCOM 权限在拦你。
原因在于 OPC 服务器默认只允许特定用户调用 COM 接口,新电脑的登录账号没有授权。解决方法是把部署机器的运行账号加入 OPC 服务器的 DCOM 权限列表。具体操作是先运行 dcomcnfg,打开组件服务,再打开 OPC 服务器的属性,把“标识”设为交互式用户或指定一个固定账号,同时在“启动权限”和“访问权限”里给该账号加进允许列表。做完这两步基本就能连上。生产环境建议用独立服务账号,不要用管理员账号裸奔。
5.2 OPC 服务器列表一片空白,枚举服务没生效
现象是代码调用服务器枚举器,返回数为 0,但 OPC 服务器明明已经安装在同一台机器上。第一次遇到这个问题的开发者很容易怀疑服务器没装好,重装一遍还是一样。
原因通常有两个:一是 OPC Core Components 里的枚举组件没有注册,或者被安全软件清理掉;二是当前开发机没有读取注册表枚举项的权限。解决方法是重新安装对应版本的 OPC Core Components,然后用管理员身份注册;如果仍为空,就检查组件服务里的枚举服务是否处于停止状态。DA 的枚举只对注册表里的所有 ProgID 扫一遍,服务器只要注册了就不该看不到。做这一步之前先确认注册表里能找到服务器 CLSID,能避免你怀疑人生。
5.3 32 位和 64 位不匹配,COM 类型转换直接抛异常
现象是程序编译时一切正常,运行到读取点位时,返回的 values 数组里全是空值,或者直接在对象转换时抛 InvalidCastException。这个问题在 64 位工控机上特别常见。
原因是 COM 组件存在两套注册表分支。你的项目如果还是 AnyCPU,在 64 位系统下会自动以 64 位进程运行,但 OPC 服务器的自动化组件却注册在 32 位注册表里,两边就对不上了。解决方法是把 VB .NET 项目的目标平台强制改为 x86,重新编译后再看。如果现场服务器本身就是 64 位进程提供的 COM 服务,那反过来把项目设成 x64。判断依据是看 OPC 服务器安装目录里有没有 SysWOW64 字样,或者直接用注册表编辑器查看 CLSID 在哪个分支下存在。
5.4 UA 证书校验失败,报 BadSecurityChecksFailed
现象是 OPC UA 客户端连接时,服务器返回 BadSecurityChecksFailed,或者客户端回调里显示 Application Certificate Untrusted。开发环境下很多人会把它当成偶发网络故障,反复重试,浪费时间。
原因其实是客户端首次连接时的证书没有导入到服务器信任列表。OPC UA 的双向认证要求服务器信任客户端应用证书,反过来客户端也要信任服务器证书。解决方法是第一次连接前,把客户端生成的证书导出给服务器管理员,在服务器端添加为受信任客户端;同时把服务器证书抓下来,导入到客户端机器受信任人列表。如果你只是在开发环境验证逻辑,可以临时把 AutoAcceptUntrustedCertificates 设为 True,但上线前一定换回正常信任流程,否则后患无穷。
5.5 会话重连后订阅丢失,数据不再上报
现象是现场断网一段时间后,网络上其他业务恢复了,OPC UA 程序也显示已连接,但订阅的数据事件彻底不来了,只剩手动读节点还能返回数值。
原因是 OPC UA 的订阅是建立在一条 Session 上的,网络中断后 Session 被服务器回收,SDK 虽然能自动恢复连接,但订阅对象不会跟着自动重建。解决方法是把订阅创建逻辑封装成一个方法,在重连成功回调里,先清空原订阅,再根据本地保存的节点列表逐条重建。很多人没有保存订阅节点清单,重连时不知道恢复哪些点,就会留下这个坑。建议维护一个 List(Of String) 的订阅节点清单,每次订阅成功时刷新一次,重连时直接从这个清单重建。
这几条基本覆盖了现场遇过的七成以上问题。遇到连不上、读不到、写不进,先按现象对照这一章找原因,不要急着改业务代码。
6. 让 OPC 通讯代码能一直稳定跑:重连、日志与性能技巧
最后落到生产环境。OPC 通讯在项目里往往不是独立的,后面还挂着实时看板、批次报表、报警联动,通讯一旦不稳定,脏数据比没数据还讨厌。我现在给所有 OPC 客户端做三件事:断线自动重连、关键日志落盘、禁止高频临时建项。
先说重连。DA 和 UA 的断线判定不一样。DA 的 OPCServer 对象在断网后往往直接失效,必须重新 New 一个新对象;UA 的 Session 可以先调用一次 GetEndpoints 探测连接,再决定是否重建。通用做法是开一个定时器,间隔 5 秒检查一次连接状态,断开时先等待一个退避周期再重连。
Private Sub Timer_Tick(sender As Object, e As EventArgs) Handles Timer.Tick If _client.IsConnected Then _failCount = 0 Return End If ' 连续失败超过 3 次,把重试间隔拉长到 30 秒,避免服务器压力过大 _failCount += 1 Dim delaySeconds As Integer = 5 If _failCount > 3 Then delaySeconds = 30 If (DateTime.Now - _lastTry).TotalSeconds >= delaySeconds Then _lastTry = DateTime.Now _client.Connect(_serverUrl) If _client.IsConnected Then _failCount = 0 RebuildSubscriptionIfAny() End If End If End Sub其中 _failCount 是模块级变量,记录连续失败次数。第一次失败 5 秒后重试,连续失败 3 次以上就把间隔拉长到 30 秒,这是非常简单但可靠的退避方式。不要用 Thread.Sleep 写在 while 循环里,界面会卡死,建议用定时器。重连成功后还要记得重建 UA 订阅,否则会出现上一章第 5 种问题。
再说日志。OPC 通讯的故障往往是偶发的,最难查的就是“某个时间点值不对”。我给每个读写方法外面包了一层日志,把调用时间、节点 ID、返回值、错误码写入一个本地文件,按天滚动,单条内容控制在 200 字节以内。这层日志不需要额外装框架,直接在文本框里 AppendLine 后落文件就行,关键是别把值的内容打印成多行 JSON,否则现场排错时根本对不齐。
最后是性能。DA 那边即使封装了 ReadTag,也不要每次循环读值都 AddItem 再 Remove。组的本质是给服务器做批量缓存和订阅,如果你连续读同一个点都临时建一次项,服务器端就会反复做插入和释放,CPU 和内存一起飙升。我用 ConcurrentDictionary 把组和项的句柄缓存起来,键是 groupName + itemId,只在第一次访问时 AddItem,后续直接复用。UA 那边则优先用批量读,一次 ReadNode 只能读一个点,但 session.Read 可以一次读一组 NodeId,吞吐量差出一个量级。
还有一个习惯可以分享:所有 OPC 通讯程序的重点不是界面是否好看,而是程序退出时能否优雅断开。我早期做某图像处理 Demo 时,直接关窗体导致 COM 线程不能退出、进程僵在后台,后来定下规矩,FormClosing 事件里一定要先走 Disconnect 方法,再释放 COM 对象,最后再真正退出。这个习惯从那时保持到现在,救了我好几次。
如果你也是从“搜索引擎找到 VB .Net OPC 通讯例子,然后开始改造”这条路走过来的,希望这篇能帮你把重心放在代码之外的那部分上。协议选型、环境确认、重连和日志,这些才是现场稳定的关键。希望帮到你。
本文还有配套的精品资源,点击获取