
简介一套面向 C# 开发者的 OPC 通讯实例程序源码由工控老马开发用于通过 OPC 服务器连接 PLC 并读写数据自带精美实用界面适合新手及有一定经验的开发人员学习参考。资源共 30 个文件压缩包整体仅 1.19MB包含 Visual Studio 解决方案与工程文件.sln/.csproj、C# 源码.cs、可直接运行的 EXE、PDB 调试符号、界面截图 PNG、资源文件以及 Word 版说明文档结构清晰从源码阅读、编译调试到运行验证都能顺畅对照便于理解 OPC 客户端与服务器的通信交互。目前已有 848 人学习下载。读者不仅可以获得完整可运行的 OPC 客户端项目还能通过代码学习连接建立、标签读写、界面数据绑定等关键实现方法配套文档也方便快速上手同时通过调试符号可在异常时定位问题对初学 OPC 通讯的工程师和希望二次开发的开发者来说都是非常实用的参考资料。1. 从“PLC连不上”到“秒读点位”C# 通过 OPC 打通数据通道这件事我把它拆给你看车间里一台老设备触摸屏上数据正常可上位机系统就是读不到或者你刚接手数采项目要同时对接西门子和三菱两套 PLC厂商协议各不相同总不能每接一台设备就重写一套驱动。OPC 就是解决这类问题的关键——它把不同 PLC 的专有协议统一成一套标准接口C# 这边只需要按标准去连接、读点、写点。这篇笔记会从协议选型讲到 C# 实现代码再到我踩过的 DCOM、质量戳、重连这几个坑争取让看到的人少走弯路。2. OPC 协议与开发环境选型为什么说“先定协议再动手写代码”能少加三天班2.1 OPC DA 与 OPC UA 的本质区别一个是给老设备续命的方案一个是面向新项目的标准OPC 全称 OLE for Process Control早期版本叫 OPC DAData Access基于 Windows 的 COM/DCOM 技术专门解决工业现场“上位机读 PLC 数据”的问题。它的工作模式很简单现场装一个 OPC 服务器Server这个服务器负责跟 PLC 通过专有协议通信然后把你需要的点位Item暴露出来C# 程序作为客户端去访问服务器按“组Group 项Item”的方式读写数据。也就是说C# 不需要知道 PLC 是西门子还是三菱它只跟 OPC 服务器打交道。OPC DA 最大的问题是依赖 DCOM跨机器访问时要在 Windows 上层层配置权限一个“拒绝访问”能让人折腾半天。OPC UAUnified Architecture就是用来解决这些历史包袱的它基于 TCP/IP自带会话管理和加密机制跨平台、不需要 DCOM而且已经成为新项目的主流选择。但你真去到现场就会发现大量在役老系统的 OPC 服务器还是 DA 版本这就是现实——你得先搞清楚机房里有的是“米其林方案”还是“泡面方案”再看怎么写客户端。2.2 搭建 C# OPC 客户端最小开发环境目标框架、NuGet 包与一个能用的 OPC 服务器我用 C# 开发 OPC 客户端开发环境一般情况下是这样搭建的一台 Windows 10/11 开发机Visual Studio 2019/2022 Community 版本目标框架选 .NET Framework 4.7.2 或 .NET 6/8——选哪个取决于你用的是 OPC DA 还是 OPC UA。如果走 OPC UA直接用官方提供的OPCFoundation.NetCore.Opc.Ua这个 NuGet 包就能实现客户端它不依赖 COM编译成 x64 也没问题Windows 和 Linux 上都能跑这一点比 DA 领先不少。UA 的服务器也很多Kepware EX 支持 UA西门子 S7-1500 自带 UA 服务器还有一些开源实现像 open62541 也能用。如果是 OPC DA最常见的方式是通过Interop.OPCAutomation.dllOPC DA Auto 2.0 的 COM 互操作程序集来访问服务器。这个组件虽然老但是兼容性却非常好现场各种品牌 OPC 服务器基本都能连上。新建 C# 项目后右键引用添加 COM 组件找到 “OPC DA Auto 2.0”确定即可。有些项目是找不到这个 COM 组件的因为你系统里没有安装 OPC Core Components Redistributable去 OPC 基金会官网下载并安装一次就一劳永逸。2.3 搞定 OPC Server 的 DCOM 配置新手第一个“连不上”拦截点OPC DA 客户端要去连远程的 OPC 服务器如果不想每次都在代码里挣扎那就先把 DCOM 这个基础打牢不然服务器没配置好代码怎么写都是白搭。我一般会按下面的步骤来配置在服务器电脑上打开dcomcnfg展开“组件服务 → 计算机 → 我的电脑 → DCOM 配置”找到你的 OPC 服务器条目比如Kepware.OPC右键属性切换到“安全”选项卡。把“启动权限”“访问权限”“配置权限”都改为“自定义”然后添加你客户端运行时所使用的用户账户给上“允许启动”“允许访问”“允许配置”的权限。然后在“标识”选项卡选择“交互式用户”如果服务器以 Windows 服务方式运行就得选择“指定用户”并填入有权限的账户否则 DCOM 启动时没有权限。两台机器都做完相同的配置防火墙放行 TCP 135 端口以及 OPC 服务器动态端口范围这个比较麻烦更省事的做法是直接把 DCOM 相关的端口范围放行或者在测试阶段临时关闭防火墙。这些配置做完用服务器自带的客户端工具测一下连接通了再写 C# 代码。顺序一定是“先测通再编码”。3. C# 通过 OPC 读写 PLC 数据连接、读点、写点全流程代码拆解3.1 连接 OPC Server从引用 COM 组件到建立会话的代码逻辑连接 OPC DA 服务器的代码核心就三步声明对象、调用 Connect、判断状态。这里把关键的几行贴出来using OPCAutomation; OPCServer opcServer new OPCServer(); try { opcServer.Connect(Kepware.OPC, localhost); // ProgID 和机器名 if (opcServer.ServerState (int)OPCServerState.OPCRunning) { Console.WriteLine(OPC 连接正常); } } catch (Exception ex) { Console.WriteLine(连接失败: ex.Message); }Connect方法第一个参数是 ProgID这个值必须在服务器端注册表中存在。Kepware.OPC是 KEPServerEX 的 ProgID西门子的可能是Siemens.OpcDA.2不确定时用机器上的 OPC 客户端枚举工具去查或者看服务器安装目录下的配置文件。第二个参数是服务器所在机器的名称本地调试用localhost远程就用计算机名或 IP。注意有些环境用 IP 会触发 DCOM 校验失败做事稳妥一点优先用机器名。假如一定用 IP就在 hosts 文件里把机器名与 IP 做映射。3.2 读取 PLC 数据同步读、异步读与订阅模式的取舍连接建立后要读取 PLC 数据靠的是“组 项”模型。先添加一个组再往组里添加点位。点位名的格式通常是“通道名.设备名.标签名”具体命名规则要看 OPC 服务器端的配置。同步读数据代码OPCGroups groups opcServer.OPCGroups; OPCGroup group groups.Add(DataGroup); group.UpdateRate 500; // 500ms 刷新周期 // 添加点位 Channel1.Device1.Tag1 int clientHandle 0; object value null; object quality null; object timestamp null; group.OPCItems.AddItem(Channel1.Device1.Tag1, 1, out clientHandle, out value, out quality, out timestamp); // 同步读取从设备读OPCDevice不从缓存读OPCCache object[] values new object[1]; object[] errors new object[1]; group.SyncRead((short)OPCDataSource.OPCDevice, 1, out values, out errors);这里的SyncRead第一个参数很关键OPCDevice代表直接从 PLC 读取OPCCache读的是服务器的缓存数据。直接读设备更准但响应会慢一些读缓存速度快但可能拿到旧值。大多数实时监控场景我会选择直接读设备保留数据的新鲜度。当点位很多、刷新频率要求高时同步读循环轮询就有点吃不消对系统资源占用很明显这时候用订阅模式才是正路。订阅模式的代码看起来像这样group.UpdateRate 100; group.Active true; group.DataChange (int transactionID, int numItems, ref Array clientHandles, ref Array itemValues, ref Array qualities, ref Array timeStamps) { // 订阅事件里拿到的还是数组按客户端句柄去匹配点位 };订阅模式下服务器在一定时间间隔内检测点位是否有变化有变化就把数据推送过来C# 端通过事件接收。这种模式适合点位多、要求实时性高的场景。注意订阅事件是在一个 COM 回调线程里触发的事件处理内部尽量不做耗时操作只用队列做入队操作。3.3 写入 PLC 数据权限检查、数据类型匹配与常见失败现场写操作和读操作类似但要额外注意两项数据类型匹配与写入权限。// 添加要写入的点位 group.OPCItems.AddItem(Channel1.Device1.StartButton, 2, out clientHandle, out value, out quality, out timestamp); // 写入serverHandle 数组与 value 数组一一对应 short[] serverHandles new short[1]; serverHandles[0] (short)clientHandle; object[] newValues new object[1]; newValues[0] 1; // 写入值 object[] writeErrors new object[1]; group.SyncWrite(1, ref serverHandles, ref newValues, out writeErrors);这里最常见的问题是“我已经写了 1为什么 PLC 没动作”先别急着怪 OPC排查顺序应该是先检查 OPC 客户端里该点位是否允许写入很多服务器默认只读再确认 OPC 到 PLC 的通道是否有写入权限最后再回来看 C# 代码。如果服务器端用测试工具写都失败那就是服务器到 PLC 链路的问题跟你的 C# 代码没有关系。数据类型也要对上。OPC 点位在服务器端是有数据类型的你用一个 C# 的int去写一个服务器端定义成bool的标签会产生不可预料的错误或写入失败。先到服务器端确认标签类型再在代码里用统一的类型转换机制我一般在点位配置表里加上 DataType 字段。3.4 一个能跑的完整示例定时读取和按需写入的最小闭环常见做法是写一个简单的后台循环程序既能按周期读点位也能根据业务指令写点位。这里给一个简洁的框架public class OpcClientService { private OPCServer _server; private OPCGroup _group; private Dictionaryint, string _itemMap new Dictionaryint, string(); public void Connect(string progId, string machine) { _server new OPCServer(); _server.Connect(progId, machine); _group _server.OPCGroups.Add(MainGroup); _group.UpdateRate 200; _group.Active true; // 订阅数据变化 _group.DataChange OnDataChange; } private void OnDataChange(int transactionID, int numItems, ref Array clientHandles, ref Array itemValues, ref Array qualities, ref Array timeStamps) { // 这里只做入队不做业务处理 for (int i 1; i numItems; i) { int handle (int)clientHandles.GetValue(i); object value itemValues.GetValue(i); int q (int)qualities.GetValue(i); Console.WriteLine($Handle:{handle}, Value:{value}, Quality:{q}); } } public void ReadItem(string itemPath) { int handle 0; object value null, quality null, timestamp null; _group.OPCItems.AddItem(itemPath, _itemMap.Count 1, out handle, out value, out quality, out timestamp); // 同步读取一次并打印 object[] values new object[1]; object[] errors new object[1]; _group.SyncRead(1, 1, out values, out errors); Console.WriteLine($读取 {itemPath} 的值: {values.GetValue(0)}); } }这种基于事件回调的模型是 C# 上位机最常见的写法工业现场数据采集程序基本都长这样。但因为回调执行在 OPC 线程里绝不能把数据库写入或 UI 刷新写在里面否则系统卡顿只是时间问题。4. OPC 通信实战避坑指南DCOM、质量戳、重连风暴的现场排查记录4.1 DCOM 权限配置引发的“拒绝访问”问题现象程序在开发机器上运行正常部署到现场工控机后一执行Connect就抛出System.Runtime.InteropServices.COMException (0x80070005): 拒绝访问。原因0x80070005 是标准 Windows 权限错误几乎可以断定是 DCOM 配置没有把当前运行账户加进去。有时候明明把 Everyone 都加进去了还是报错原因在于服务器和客户端不在同一台机器上两台机器都要配置不能只在服务器端配。解决在服务器和客户端两台机器上都执行dcomcnfg把 OPC 服务器条目的安全权限改为自定义并添加当前用户的允许权限。“标识”选项卡选“交互式用户”。配置完成后重启 OPC 服务再运行程序验证。另外要特别留意Windows 更新有时会把 DCOM 配置重置回默认值现场运行稳定的机器尽量别做系统更新。4.2 读到的值显示为旧数据但质量戳已变 Bad现象现场 PLC 断电或者网线断开后C# 程序没有报错数据显示区域停在一个旧数值上看起来像是“卡住了”这其实不是卡住而是服务器还保持着最后的值但质量戳已经变为 Bad。原因OPC 数据模型除了 Value 之外还有 Quality 字段。设备断线时OPC 服务器不会自动把 Value 清空而是把 Quality 置为 Bad。如果代码里只取 Value 不判断 Quality就会拿到“僵尸值”。解决任何点位读取都同时取 Value 与 Quality。质量戳大于等于 192 才认为是有效数据以下的都按异常处理// 质量码 192 以上表示 Good if (quality 192) { // 使用 value } else { // 标记该点位不可信不参与业务计算 }这个习惯是我吃了亏之后才养成的。现在但凡上现场第一步的查验就是看 Quality 与 Timestamp不然生产报表里混进几个虚假值查都查不出来。4.3 订阅回调里做了业务处理数据开始丢点现象开启订阅模式后程序跑一段时间发现日志里数据点的数量明显减少而且 UI 越来越卡内存占用居高不下。原因在DataChange事件里直接做了数据库写入、日志记录、UI 更新等耗时操作导致回调线程执行时间过长OPC 服务器推送过来的数据因为客户端“没空接”而被丢弃。订阅模式的推送频率由UpdateRate控制比如 100ms 一次如果你的回调花了 300ms那中间的数据自然就丢了。解决回调里只做一件简单的事——把数据放入线程安全的队列然后立刻返回。另起一个后台消费线程来处理入队的数据private ConcurrentQueue(int handle, object value, int quality) _queue new ConcurrentQueue(int handle, object value, int quality)(); // 回调中 private void OnDataChange(...) { for (int i 1; i numItems; i) { _queue.Enqueue(((int)clientHandles.GetValue(i), itemValues.GetValue(i), (int)qualities.GetValue(i))); } } // 消费线程 private void ProcessQueue() { while (true) { if (_queue.TryDequeue(out var item)) { // 在这里做业务处理 } Thread.Sleep(10); } }队列模型是 OPC 客户端处理高频率数据流的标准姿势即保证了事件响应的实时性也避免回调线程被业务逻辑阻塞。4.4 OPC Server 假死与自动重连指数退避避免“重连风暴”现象现场因为网络波动或 OPC 服务器重启客户端和服务器之间的连接断开程序里后续的所有读写都异常。即使服务器恢复了客户端也不会自动恢复连接必须重启程序才行。原因OPC 连接没有内置自动重连机制OPCServer 对象在连接断开后处于一种不确定状态继续调用读写方法会一直报错。我见过有些人写“每 5 秒重连”的定时器结果当服务器假死恢复的瞬间多台客户端同时发起重连请求服务器又被打挂好家伙直接来个二次事故。解决重连逻辑要加“指数退避”第一次失败等 1 秒第二次等 2 秒第三次等 4 秒等待时间逐渐加长直至最大上限 30 秒。某次服务器恢复之后客户端不会像定时器那样齐刷刷地冲上去而是错峰重连把风险降低不少。public async void StartReconnectLoop() { int delaySeconds 1; while (true) { try { _server.Connect(_progId, _machineAllName); break; } catch { await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); delaySeconds Math.Min(delaySeconds * 2, 30); } } delaySeconds 1; // 成功后重置 }4.5 OPC DA 的 32 位/64 位纠结为什么程序发布后在部分机器上装不起来现象程序开发时一切正常发布后在另一台机器上运行时加载Interop.OPCAutomation.dll时报BadImageFormatException。原因OPC DA 的 COM 组件有位数之分很多服务器端的 OPC 核心组件只有 32 位版本。如果你的 C# 程序以 x64 模式编译在 64 位系统中就不能加载 32 位的 COM 组件于是出现位数不匹配的异常。解决最省事的办法是把项目平台目标固定为 x86因为绝大多数厂家的 OPC DA 组件都是 32 位。如果你的 OPC 服务器是 64 位那项目也改成 x64总之“哪有碗饭就端到哪”。我的一般做法是先到目标机器上装好 OPC 服务器再用任务管理器看服务器进程是否带 *32 标记以此判断位数。4.6 点位多但刷新慢UpdateRate 设得太小反而是负担现象很多人为了让数据更“及时”把UpdateRate设为 10ms结果点位数量又多CPU 飙得很高服务器和客户端一起卡顿。原因UpdateRate 决定的是服务器以多短的时间间隔去扫描各组内点位的变化。点位多了以后10ms 一轮的扫描压力非常大而且这会导致事件回调极其频繁队列积压越来越严重从数据上看反而更“迟滞”了。解决根据现场实时性要求来设置一般设备状态数据用 100ms 到 500ms 就足够了模拟量数据实时监控用 100ms 也很宽裕。真正的实时控制不应该走 OPC 这条链路OPC 适合数据采集和上位机监控不适合纳秒级的闭环控制。5. 把 OPC 客户端做成“交付也不慌”的三个工程化习惯第一把点位配置外置到 JSON 文件里。业务代码里不出现硬编码的点位路径和数据类型一个点位映射表在启动时加载。这样到了现场应付点位表调整改配置文件即可不用重新编译发布。点位映射表至少包含四个字段点位路径、点位标识Handle、数据类型、读写属性。代码如下public class OpcPointConfig { public string ItemPath { get; set; } public int Handle { get; set; } public string DataType { get; set; } public bool IsWritable { get; set; } }启动服务时循环读取配置并AddItem到组里。这个习惯能帮你在调试阶段快速定位问题是点位不存在还是类型不对还是在代码里敲错了字符串。第二做一个独立的“现场诊断模式”。这个模式下的程序界面不是业务界面而是一个纯点位值列表显示每个点位的 Value、Quality 与 Timestamp。现场工程师反馈“数据不快”“数据不准”时你先开这个窗口看视觉效果Quality 是不是持续 GoodTimestamp 是不是一直在更新这一看就能把是链路问题还是业务逻辑问题理清。我上现场基本靠这张“点位心电图”说话比代码调试还高效。第三提交 OPC 客户端之前做一次连续 72 小时运行验证。验证期间记录连接的在线时长、重连次数、订阅队列积压数量。验证结束后重点看队列积压数量是否持续增长——如果在积压说明消费速度跟不上推送速度先优化消费逻辑而不是去调小 UpdateRate。这个习惯能在项目验收前发现潜在的稳定性问题而不是等交付到现场之后被它反噬。行走 OPC 开发这么多年最深的教训是OPC 的问题通常不是 C# 代码的问题而是环境的问题——DCOM 权限、服务器选型、质量戳判断、重连策略每一个都比语法更耗时间。顺序也别弄反先配环境、再用官方工具测通最后才写代码。希望这些经验能帮你在工业数据采集这条路上少走弯路。本文还有配套的精品资源点击获取