
C# 通过 OPC DA 协议实现数据的同步与异步读取及局域网访问 OPC Server去年我接手了一个老车间数字化改造的项目核心工作是用 C# 把车间里几台西门子 PLC 的数据汇总到我们自己开发的上位机平台上。一开始我以为是老一套的 Modbus TCP 或 S7 通信到了现场才发现设备侧那台 OPC Server 是十多年前部署的只支持 OPC DA不支持 OPC UA。更麻烦的是现场工程师不愿意把服务器升级成 UA 网关我只能老老实实地用 C# 去对接一个老掉牙的 COM 组件把 OPC DA 里的实时数据同步、异步地读出来还是在局域网环境下远程访问。这篇文章就是那次整改的完整记录包括方案选型、DCOM 配置、同步和异步两种读取模式的实现细节以及我在现场踩过的坑。如果你正准备用 C# 搞上位机、又恰好绕不开 OPC DA这篇应该能帮你节省不少时间。1. OPC DA 的通信机理为什么老协议至今还在工控一线服役先花点时间把 OPC DA 本身讲清楚。很多刚接触工控开发的 C# 工程师会把 OPC DA 和 OPC UA 混为一谈实际这是两个年代、两套架构的东西。搞清楚机理后面配置 DCOM、写代码才会明白自己到底在操作什么。1.1 三层对象模型Server、Group、Item 的关系OPC DA 的通信模型是典型的三层结构OPC Server 负责与底层设备PLC、DCS、仪表等通信对外暴露数据访问接口OPC Group 是客户端在 Server 内部创建的“数据分组”容器用来组织和管理一组相关的数据点OPC Item 是真正对应设备变量的最小数据单元关联的是设备侧的寄存器地址、数据格式、读写属性等信息。你可以把 Server 想象成一个物业公司Group 是这栋楼里的楼层而 Item 就是每个房间里的水表电表。客户端读取数据时不是直接去“敲门”要水表读数而是告诉物业Server“我要看 3 层 5 号房的水表Item”Server 再去设备侧取数返回给客户端。这个设计有个非常重要的好处客户端不需要关心设备底层的通信协议是 S7、Modbus 还是 Profibus所有协议转换都由 OPC Server 完成。这也是为什么 OPC DA 能在工控领域流传二十多年——它对上层应用的抽象是成功的只是底层依赖了 COM/DCOM 这个老旧的 Windows 组件技术。1.2 同步读取与异步读取的本质差异OPC DA 的读取方式在设计之初就定义了两种同步读取和异步读取。同步读取Sync Read是客户端调用读取方法后线程阻塞在那里直到 Server 返回数据或超时。就像你去窗口办事站着排队等柜台把材料递出来期间什么也干不了。它的优点是逻辑极其简单适合点位少、读取频率要求不高的场景。异步读取Async Read则不同客户端调用读取方法后立即返回线程该干嘛干嘛等 Server 准备好数据后通过回调事件把结果通知给客户端。相当于你点了外卖手机下单后继续干活骑手送到时电话通知你取餐。这里有个容易混淆的点OPC DA 的异步读取能力是由 Server 端实现的客户端要依赖回调接口才能拿到结果。如果 Server 本身实现不完整或支持的接口版本太旧异步读取可能会失效。这也是我在实际项目里发现的一个大坑后面第 4 章会展开说。1.3 C# 接入 OPC DA 的四种主流方案横向对比C# 访问 OPC DA 没有官方“正宗”的库所有方案本质上都是对 COM 组件的互操作包装。我在调研阶段对比了常见的四种路线直接说结论和适用场景。方案底层机制优点缺点适用场景Interop.OPCAutomation.dllOPC DA Auto调用 OPC 基金会提供的自动化接口OPC Automation Interface兼容性最好几乎所有 OPC DA Server 都支持年代久远文档少类型转换需小心对接老设备、老组态软件时优先选它OPC 基金会官方公布的.NET API基于 COM 互操作的封装接口如 OpcDaNet.dll逻辑更面向对象提供同步/异步封装类底层同样依赖 DCOM版本较老的 Server 偶发兼容问题新项目、Server 比较标准时用第三方商业库如 Kepware、Matrikon 提供的客户端 SDK厂商对 OPC DA 的二次封装接口友好、支持异步回调和断线重连封装收费或闭源License 受限于特定产品公司预算充足、希望降低开发成本时用自写 COM 互操作代码直接调用 IOPCServer、IOPCItemMgt 等 COM 接口完全可控性能最优开发量大需要对 COM 有深入了解需要极致性能或特殊定制时我最后选的是 Interop.OPCAutomation.dll。原因是现场那台老 OPC Server 的 DCOM 配置和接口实现都很“随性”用官方标准 API 反而容易碰壁自动化接口的兼容性最稳妥。代价是代码里会出现一些 COM 对象释放的“脏活”这个我会在排错章节里讲。2. 局域网访问 OPC Server 的第一步DCOM 配置和防火墙放行OPC DA 基于 COM/DCOM 技术局域网访问时最核心的问题是跨机器调用。很多人写 C# OPC 代码时在 Server 本机测试一切正常一旦客户端放到另一台电脑上就出现“找不到 OPC Server”“访问被拒绝”或“连接超时”其实 90% 都不是代码问题而是 DCOM 权限和防火墙没配好。2.1 做 DCOM 权限设置前必须确认的四个网络基础项在进入组件服务控制台之前先确认四个容易被忽略的基础项。这些没弄对后面配再多权限都白搭。第一客户端电脑和 OPC Server 电脑必须在同一个局域网并且能互相 ping 通。这个看似废话但我在现场真遇到过物理网线接错、两台机器在不同 VLAN 的情况。第二两台电脑的 Windows 网络位置必须设为“专用网络”而不是“公用网络”。公用网络会默认开启 Windows 防火墙的最高级别过滤DCOM 动态端口会被拦得一干二净。第三最好保证两台电脑属于同一工作组或者处于同一域环境中。点对点工作组环境下需要建立相同的本地账户凭证用户名密码一致否则跨机器 COM 认证会失败。第四关闭“简单文件共享”。控制面板——网络和共享中心——高级共享设置里有个“文件共享连接”选项一定要设为“使用 128 位加密帮助保护文件共享连接”或者干脆把密码保护的共享关掉。这个选项会影响系统对网络凭据的传递方式。2.2 组件服务控制台里的逐项 DCOM 权限配置确认完基础网络项后在 OPC Server 电脑上运行dcomcnfg进入“组件服务——计算机——我的电脑——DCOM 配置”。在这里找到你的 OPC Server 对应的 COM 注册项。不同厂商的 Server 显示名称不同一般是产品名或 OPC 相关的 ProgID。右键打开属性需要配置的关键项有四个。第一是“常规”选项卡。确认“身份验证级别”为“默认值”或“无”。“无”代表不校验调用方的安全级别在纯内网环境可以减少很多故障点。注意这里的“无”不是不安全而是把安全校验交给了更上层的访问权限控制。第二是“位置”选项卡。勾选“在这台计算机上运行应用程序”这是局域网访问的基本前提。有些 Server 组件默认允许在“指定位置”运行路径一变就可能找不到。第三是“安全”选项卡。这里是最关键的。“启动和激活权限”和“访问权限”都应该选择“自定义”然后点击“编辑”按钮把“Everyone”或者“ANONYMOUS LOGON”加进去并勾选“本地启动”“远程启动”“本地访问”“远程访问”四个权限。如果你用的是域环境还要把域用户组加进去。注意把 Everyone 加入权限并不意味着随便谁都能控制你的设备因为还受限于 Windows 账户和防火墙规则。但在纯内网、无敏感数据的场景下这是最稳妥的配置方式。第四是“标识”选项卡。这里建议选“交互式用户”。如果你用“指定用户”且密码过期或错误Server 启动会失败。但要注意“交互式用户”意味着 OPC Server 只有在有人登录该电脑时才能正常运行。如果现场那台机器设置了开机无登录自动运行就要选“指定用户”并填一个永不失效的服务账户或者干脆把 OPC Server 注册成 Windows 服务。2.3 防火墙规则和动态端口范围OPC DA 的 DCOM 通信最经典的坑是只开放了 135 端口却不放行动态端口范围。DCOM 的初始连接走 TCP 135但真正的数据传输会由系统随机分配 1024 之后的动态端口。静态只开 135客户端能看到 OPC Server 列表但一旦建立数据连接就会超时。解决方法有两种。第一种在 OPC Server 电脑的防火墙高级设置中新增入站规则允许程序C\Windows\System32\dcomcnfg.exe和 OPC Server 的可执行文件通信。这种“按程序放行”的方式最省心系统会自动处理动态端口。第二种限制 DCOM 动态端口范围然后在防火墙中放行这个范围。运行dcomcnfg在“我的电脑”属性——COM 设置里可以限定动态端口范围比如 49152-65535这是 Windows 系统的保留动态端口段。然后在防火墙中新增 TCP 入站规则放行该端口段。我推荐先试试第一种“按程序放行”因为它对系统原有端口的干扰最小。如果 OPC Server 是组态软件的内置服务程序路径配置正确后一般就能解决。2.4 快速验证 DCOM 权限是否配好的测试方法配完上面这些如何快速判断 DCOM 通不通不需要急着自己写 C# 代码Windows 自带一个 OPC 客户端测试工具。在客户端电脑上安装“OPC Core Components”或者任意带 OPC DA Client 的工具比如 Matrikon OPC Explorer 的免费版直接在服务器列表里枚举。如果在 Matrikon 里能看到目标 OPC Server、能浏览到 Item 列表、能实时读取数据那基本可以断定 DCOM 权限已经没问题接下来就可以安心写代码了。如果枚举不到 Server先检查刚才说的端口和权限如果能看到 Server 但浏览不到 Item多半是权限不够如果能浏览到 Item 但 DCOM 配置正确代码侧的问题就可以独立排查了。这个思路能帮你快速隔离现场问题不至于在代码和网络配置之间反复横跳。3. 同步读取与异步读取的实现代码背后的编程理念与线程模型到了写代码的环节。这一章我会先用 Interop.OPCAutomation.dll 把同步读取和异步读取的完整套路写出来然后对比两种模式的适用场景最后给出断线重连和自动恢复的通用方案。3.1 环境引用和连接 OPC Server 的基础代码先说你需要在工程里引用什么。在 Visual Studio 中右键“引用——添加引用”选择 COM 选项卡找到“OPC Automation 2.0”或“OPC DA Automation Wrapper 2.02”引用后系统会生成Interop.OPCAutomation.dll。连接 OPC Server 的代码模式是固定的using OPCAutomation; // 创建 OPC 服务器对象的自动化实例 var opcServer new OPCServer(); // 连接名称可能是 ProgID如 KEPware.KEPServerEX.V6或机器名 opcServer.Connect(KEPware.KEPServerEX.V6, 192.168.1.100); // 创建组参数是组名和是否激活 OPCGroup opcGroup opcServer.OPCGroups.Add(MyGroup); opcGroup.UpdateRate 100; // 更新周期单位毫秒 opcGroup.IsActive true; opcGroup.IsSubscribed true;Connect方法的第二个参数是远程计算机名或 IP。如果 DCOM 配置正确这里就能连上。连接成功后OPCGroups.Add创建组UpdateRate决定 Server 向客户端推送数据的周期这个值不要设得太小否则 Server 会积累大量数据积压导致网络拥堵和 CPU 飙升。3.2 同步读取的两种调用方式和适用边界同步读取最直接的方式是通过OPCItem.Value.Value获取// 添加一个 ItemDevice1.Value 是设备侧变量的 OPC 标识 OPCItem item opcGroup.OPCItems.AddItem(Device1.Value, 0); item.IsActive true; // 从 Server 拉取实时值 object value item.Value.Value; object quality item.Value.Quality; object timestamp item.Value.TimeStamp;item.Value.Value拿到的数据在到达时已经是当前值每次访问都会触发一次随性、无需等回的请求。但注意这个方式是“随性读取”并不保证你拿到的是“最新”的数据因为 Server 内部有缓存机制可能返回的是上一次刷新周期的缓存值。如果需要确保每次都从设备侧直接采集可以使用OPCGroup.SyncRead方法// 定义要读取的 Item 句柄数组 int[] handleList new int[] { itemHandle1, itemHandle2, itemHandle3 }; object values null; object qualities null; object timestamps null; opcGroup.SyncRead( (short)OPCDataSource.OPCDevice, // 从设备直接读取跳过 Server 缓存 3, // 读取个数 handleList, out values, out qualities, out timestamps );OPCDataSource.OPCDevice这个枚举值很关键它强制 Server 直接访问设备寄存器而非返回缓存值。代价是高延迟和高负载同步读取时如果有 10 个 itemServer 会按顺序逐个向设备发起请求耗时是单个请求的叠加。3.3 异步读取回调陷阱和线程上下文切换异步读取的代码量和复杂度比同步高得多。先看基本流程// 声明一个变量用于接收异步读取回调结果 private Array asyncValues; private Array asyncQualities; private Array asyncTimestamps; private int requestID 0; // 需要订阅异步读取完成的回调事件 opcGroup.AsyncReadComplete OnAsyncReadComplete; private void Button_AsyncRead_Click(object sender, EventArgs e) { int[] handleList new int[] { itemHandle1, itemHandle2, itemHandle3 }; opcGroup.AsyncRead(handleList.Length, handleList, out requestID); } private void OnAsyncReadComplete(int transactionID, int numItems, Array clientHandles, Array values, Array qualities, Array timestamps) { // 注意这个回调运行在 COM/OLE 线程池线程中不能直接更新 UI asyncValues values; asyncQualities qualities; asyncTimestamps timestamps; // 如果需要更新界面用 Invoke 切回 UI 线程 // this.Invoke(new Action(() UpdateUI(asyncValues))); }这里有几个必须注意的点。第一回调和主线程的关系是工控开发者的第一道坎。OnAsyncReadComplete运行的不是你创建组的那个线程而是 OLE/COM 的线程池线程。如果你在这个回调里直接操作 UI 控件会抛出跨线程异常。即使使用 WinForms 不抛异常运行时也会间歇性出现诡异行为。标准做法是封装一个委托用Invoke或BeginInvoke回到主线程。第二异步读取大量点位时回调频率会非常高。如果你订阅的是 100 个点ISubscribed true 且 UpdateRate 100msServer 每秒最多会触发 10 次回调每次回调携带 100 个点的数据。如果回调里还写了数据库数据库的压力会非常大。稳妥的做法是回调里只暂存数据由后台线程定时批量抓取处理。第三异步读取依赖 Server 对自动化接口的异步实现。老一代 Server比如某些 2005 年以前的组态软件内置 OPC 服务对AsyncReadComplete回调的触发并不稳定甚至根本不会触发。我在现场就遇到过一台 Server 能同步读取、能订阅但就是异步回调丢失的情况。判断方法是观察回调触发次数和时间间隔如果不规律或根本没有就只能退妥协用线程同步读取模拟异步效果。3.4 同步与异步的选型原则和折中方案用一张表总结两种模式的选型逻辑场景推荐方式原因点位少50、读取频率低1s同步读取逻辑简单便于排错线程开销小点位多200、采集频率高500ms异步读取避免阻塞回调批量返回数据需要严格按固定周期采集异步订阅IsSubscribedtrueServer 主动推送时间戳更整齐与设备交互型操作如写入参数同步写入需要确认执行结果读取量不大但 UI 不能卡后台线程 同步读取用代码模拟异步兼容性最广很多人误会“异步一定比同步好”实际并不绝对。异步回调的线程模型复杂回调风暴和数据延迟不可控在中小型项目中我经常用后台独立线程 主线程 Invoke 的“假异步”模式用起来反而比原生异步稳定得多。3.5 断线重连和数据恢复机制工地现场的 OPC Server 不可能永远稳定网络闪断、设备重启、Server 服务崩溃时有发生。C# 客户端必须有自动重连机制否则现场每跑一天都要人工重启程序。标准做法是双重检测定时任务检查连接状态 异常触发重连。private void CheckConnectionTimer_Elapsed(object? sender, ElapsedEventArgs e) { try { // 如果组已经激活认为连接还在 if (opcGroup.IsActive) return; ReconnectToServer(); } catch { ReconnectToServer(); } } private void ReconnectToServer() { try { opcServer.Disconnect(); opcServer.Connect(progID, serverIP); opcGroup RecreateGroup(); SubscribeToGroupEvents(); } catch (Exception ex) { // 记录日志等待下一次重试 Logger.Error(OPC 重连失败, ex); } }重连后要重新创建组和 Item之前的组不能直接复用。每次重建后记得重新订阅回调事件否则你的异步读取不会恢复。现场实测下来断网点时间如果超过 30 秒重连后还需要额外等待 3-5 秒让 Server 恢复设备缓存直接读值很可能拿到质量位为 Bad 的数据。4. 实测踩坑记录从 Access Violation 到局域网找不到 OPC Server这一章记录我在这个项目中踩过的最凶险的几个坑每个坑都附上完整的排查链路。这些内容常规文档里基本不会写但现场遇上了就是硬骨头。4.1 C# 调用 COM 组件出现 AccessViolationException (c0000005)这是用 C# 写 OPC DA 最常见的致命异常进程直接崩溃损失惨重。Access Violation内存访问违规的根因几乎都是 COM 对象的生命周期管理出了问题。COM 对象和 .NET 托管对象的回收机制完全不同。.NET 的垃圾回收器不知道 COM 对象什么时候被释放如果 COM 对象被 GC 提前回收而底层 DLL 还在使用这个对象接口就会触发访问违规。最典型的错误代码是// 错误示例用完似乎应该释放但这里存在隐患 OPCItem item opcGroup.OPCItems.AddItem(Device1.Value, 0); item.Value.Value.ToString(); Marshal.ReleaseComObject(item); // 过早释放如果此后 opcGroup 内部还有该 item 的引用再次读取就会导致 c0000005。正确的释放模式必须遵循“释放前确认无引用释放后等待挂起线程”的规范private void SafeReleaseCOMObject(object obj) { if (obj null) return; try { Marshal.FinalReleaseComObject(obj); } catch { // 对象可能已被释放忽略 } finally { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } }还有一个很容易被忽略的坑OPCServer对象在未 Disconnect 前直接置为 null或者让它在未释放的情况下被打包进 GC会导致进程崩溃。正确顺序是先Disconnect再释放 Group再释放 Server。4.2 局域网枚举不到 OPC Server但 Server 本机能正常读取这个问题的排查链路非常典型。如果你用 OPC 客户端工具在远程枚举不到 Server几乎可以确定是 DCOM 或防火墙问题但具体是哪一层需要逐个排除。第一步在 Server 电脑上用命令netstat -ano | findstr 135确认 135 端口正在监听。第二步在客户端电脑上测试 TCP 连接用 PowerShell 执行Test-NetConnection 192.168.1.100 -Port 135如果显示 TcpTestSucceeded True说明 TCP 通道是通的问题很可能出在 DCOM 认证权限。第三步远程客户端用dcomcnfg——组件服务——“计算机”——“我的电脑”——“COM 安全”里查看“访问权限”和“启动权限”确认其中包含ANONYMOUS LOGON和Everyone。注意这里有两个层面一个是“我的电脑”上的全局 DCOM 安全一个是具体 OPC Server 组件属性里的安全设置。全局设置和组件级设置的服务顺序是先全局后组件最终权限是取交集。所以两边都要配。第四步确认客户端和 Server 的用户账户信息。如果 Server 电脑上设了强密码但客户端电脑的本地账户密码为空或不同DCOM 的 NTLM 认证就会失败。现场最简单的处理方案在两台电脑上分别建立同名同密码的管理员账户并保证该用户被加入 DCOM 的访问权限中。我遇到的真实情况比这更隐蔽服务器的 Windows 防火墙是关的但设备隔离策略单独拦了动态端口。最后在防火墙中新建入站规则放行 OPC Server 程序的动态端口才解决。4.3 同步读取偶发超时或阻塞线程卡死同步读取如果出现偶发超时一般是几十秒到几分钟没有返回原因有两种一是 Server 底层的设备通信超时比如 PLC 掉站二是客户端读取阻塞了。这里有一个关键配置OPCGroup 有一个Timeout属性单位毫秒这是 Server 返回数据的最长等待时间。默认值可能很大数分钟现场需要根据业务调整opcGroup.Timeout 3000; // 3 秒无响应视为超时另一个阻塞点是 COM 和线程的关系。如果你在 UI 线程直接调SyncRead一旦 Server 繁忙或者设备掉站UI 线程会彻底卡死窗口无响应。规避方案很简单所有 OPC 通信全部放后台线程必要时用Task.Run后台线程超时后主动丢弃不回主线程等待。我用的是双线程模型主界面线程只管展示采集线程负责通信。采集线程内部用ManualResetEvent控制收到一个数据间隔就塞入队列主界面定时从队列取数。这样即使 OPC 线程卡住 10 秒界面 UI 依然流畅现场操作员不会骂娘。4.4 数据质量 Quality 总是 Bad但 Server 是正常的这个坑比较隐蔽。同步读取返回了值但 Quality 是 Bad说明 OPC Server 认为该点当前质量不可靠。常见原因有三个。第一个是 Server 端配置问题设备驱动没有激活或者驱动变量未映射。在 Server 管理界面里能看到该点的连接状态一般会显示为“非活动”。第二个是 Group 的IsSubscribed和IsActive设置冲突。有些 Server 对“订阅模式”的数据点质量要求和“主动读模式”不同。我遇到的情况是 IsSubscribed true 时部分点返回 Bad改为 false 后同步读取质量恢复正常。第三个原因比较阴险Server 本机编程的账户和启动 OPC Server 服务的账户不一致导致 Server 进程无权限从设备驱动读取数据。在组件服务的“标识”里把运行账户改成“系统账户”或管理员账户即可解决。4.5 异步回调风暴导致内存暴涨当点位多、UpdateRate 小、回调里处理不及时时内存会持续增长。原因很简单Callback 触发频率高于消费频率数据积压在线程池队列里。优化手段是“限流”或“批量聚合”。我最后采用了折中方案把 UpdateRate 从 100ms 调整到 250ms并在回调里不处理业务逻辑只把原始数据塞进并发队列由采集线程批量写数据库。这样回调处理耗时不到 1 毫秒队列积压基本为零系统稳定运行了几个月。5. 写在最后一些可能对你项目有用的经验回顾整个项目我觉得最值得记住的经验有三条。第一条OPC DA 项目的第一优先永远是现场环境的可达性验证而不是代码逻辑。DCOM 配置不过关代码写得再漂亮也是白搭。请先在本机验证 Server 可用再在远程验证连得上最后才开始写业务代码。第二条COM 对象的生命周期管理是 C# OPC DA 的命门。该用 FinalReleaseComObject 的地方不能手软但也不要过度释放。每次释放前问自己一句这个对象还有没有别的地方在引用第三条如果你能说服现场工程师升级尽量升级到 OPC UA。OPC UA 不依赖 DCOM加密认证方式也现代化得多。如果实在没法升级就把 DCOM 配置流程固化成文档放进实施手册里以后现场换人维护时能少走很多弯路。最后分享一个小技巧调试 OPC DA 项目时在客户端电脑下载一个免费的 Matrikon OPC Explorer用它做“最高权威裁判”。它连不上说明配置问题它能连上而你的程序连不上说明代码问题。这个工具我用了将近十年帮我在无数个现场快速解决了环境争议强烈建议你入行上位机开发就常备一个。