C#上位机接入OPC UA实战:连接、读写、订阅与断线重连全解析
2026/9/7 11:07:37 网站建设 项目流程

简介:面向工业自动化与.NET开发者的OPC UA C#客户端示例包,围绕C#与PLC通信、数据采集这一核心场景,帮助开发者快速理解OPC UA协议中的服务调用、节点浏览与订阅机制。压缩包共135个文件,其中55个C#源码文件构成主要示例,辅以项目工程文件(sln/csproj)、配置与资源文件、少量DLL和可执行程序,整体体积仅1.61MB,目录结构清晰,便于按模块阅读与复用。已有3584人学习下载。示例覆盖了客户端初始化、服务器连接与认证、浏览节点定位数据源、创建订阅接收变化通知、读写PLC变量、异常处理及安全断开连接等完整流程,并演示了async/await异步模型在实时数据采集中的用法。通过研读源码,可以掌握UA-.NETStandard等开源库的典型调用方式,理解OPC UA对象模型和信息交换逻辑,适合希望在.NET项目中集成OPC UA通信能力、提升工业软件开发效率的工程师参考。 做上位机这些年,我接手过的项目里十个有八个绕不开设备数据采集。早些年各家设备各说各话,西门子走S7,三菱走MC,罗克韦尔走CIP,一个上位机里塞满各种通讯驱动,光维护这些驱动就能耗掉不少精力。后来逐渐统一到OPC UA这条路上,一个C#客户端通吃各种设备,代码量少了一大截。今天这篇就从一个能直接运行的OPC UA C#示例说起,把连接、读、写、订阅、断线重连、证书处理这些最常用的功能全部跑通。适合正在做C#上位机开发、要对接PLC或MES的工程师,也适合刚接触OPC UA、想知道从哪下手的同学。

1. 项目思路拆解:C#接入OPC UA到底在解决什么问题

1.1 核心需求:统一工业通讯层

先说我实际遇到过的一个典型场景。一条产线上有PLC、有机器人、有视觉相机,还有几台老设备,每台设备的通讯方式都不一样,有的是Modbus TCP,有的是自定义TCP报文,有的干脆给一个厂家封好的DLL。以前的做法是每台设备写一个驱动类,上位机里装一堆驱动,还要处理各种断连、重连、超时异常。后来客户要求上MES,所有设备数据要统一采集,这时候OPC UA的优势就体现出来了——它在语义层定义了统一的地址空间和数据模型,现场的PLC、传感器、相机只要都暴露出OPC UA Server,上位机只需要写一个Client就能全部搞定。

从技术角度看,OPC UA把数据怎么描述、怎么安全传输、怎么通知变化这三件事都标准化了。C#所在的.NET生态又天然适合Windows上位机开发,两者搭配能省掉大量从零造轮子的工作。很多时候我们纠结“用Modbus还是Socket”或者“怎么跟VisionMaster通讯”,本质都是在找一个稳定、好维护的通讯层,而OPC UA恰好就是这个答案。

1.2 方案对比:为什么不是Modbus TCP或裸Socket

这里给个直观的对比,可以按这个思路选型:

方案数据模型部署与安全适用场景
Modbus TCP简单寄存器地址无加密,几乎无安全小型PLC、传感器快速采集
裸Socket/TCP完全自定义全部自己做已有固定协议的老设备
OPC DACOM/DCOMWindows域环境,部署麻烦老项目兼容
OPC UA面向对象节点树证书、加密、审计跨平台、MES对接、新项目首选

我个人的选择标准是:如果只是从一台PLC读几十个寄存器,协议改动不大,Modbus TCP够用;但如果设备来源杂、后续要上MES或者数据要在多个系统间共享,就直接上OPC UA,省得返工。热词里有人问“海康相机VisionMaster与C#上位机通讯用什么协议比较好”,如果只是视觉软件和上位机之间传结果,用SDK或TCP都行;但要让PLC联动执行分拣或报警,用OPC UA把检测结果写成节点,各方统一消费同一棵树上的数据,效率会高很多。

1.3 为什么用C#而不是C++或Java

C#代码可读性好,开发效率高,跟Windows下的调试工具配合得也顺。拿C++比,省心很多,不用操心指针、内存泄漏;拿Java比,Windows GUI、串口网口编程、跟原生DLL交互都更顺手。这两年.NET一直在更新,.NET 6/8下写控制台程序、Windows服务、WinForm/WPF开发都很快。工业上位机应用层开发如果不涉及底层驱动,C#几乎是最合适的选择。我自己从C++转C#之后,同类项目开发周期差不多缩短了三分之一,尤其是处理字符串、JSON、界面绑定这些杂活时,C#的体验要好太多。

2. 手写代码前,先把这几个概念搞明白

2.1 地址空间:服务器侧的信息树

OPC UA服务器不是用IP加寄存器地址来定位数据的,它维护了一棵“信息树”。树上每个东西都叫节点,节点下面可以挂变量、方法、对象这些内容。要读数据,就得先知道目标节点的NodeId,常见格式是"ns=2;s=Tag1"这种,ns是命名空间索引,s是字符串标识。很多人在读取时报BadNodeIdUnknown,十有八九是NodeId写错了或者命名空间不对。

所以我强烈建议:正式写代码前,先用Prosys OPC UA Browser这类工具连到服务器上看一看树结构,确认节点的NodeId和数据类型后再动手。这比在代码里反复猜、反复试错高效得多。Prosys OPC UA Browser是免费的,连接后左侧就是节点树,点开就能看到每个节点的属性、值、数据类型,还可以临时写值测试服务器是否允许写入。

2.2 通讯服务:会话、读写、订阅

OPC UA客户端和服务器之间先建立一个会话,可以理解成登录;登录后可以执行浏览、读取、写入、调用方法这些服务。如果数据需要持续监测,再创建一个订阅,订阅里挂上监视项,服务器会在采样周期内检测变量变化并自动推送给客户端,不用客户端高频轮询。

我用一个仓库比喻来理解:服务器就是一个仓库,会话是进门通行证,读就是看货架上的标签,写就是把标签换掉,订阅则是雇了个员工,货架一变他就跑过来告诉你。这个模型兼顾了实时性和带宽,比Modbus那种轮询方式优雅得多。轮询是客户端反复问“变了吗?变了吗?”,订阅是服务器主动说“变了,新值是xxx”,现场点位一多,差别就非常明显。

2.3 UA-.NETStandard库与环境准备

C#端最成熟的OPC UA库是官方生态的UA-.NETStandard,在NuGet上的包名是OPCFoundation.NetStandard.Opc.Ua.Client,主要用来写客户端。另外还有一个OPCFoundation.NetStandard.Opc.Ua.Core,可以用于搭建服务器端。市面上第三方库也有很成熟的,但商业授权麻烦,我推荐直接用官方库,免费开源,功能覆盖日常开发完全够。

开发环境建议这样准备:

  • Visual Studio 2022或VS Code
  • .NET 6或.NET 8 SDK
  • NuGet包:OPCFoundation.NetStandard.Opc.Ua.Client
  • 模拟服务器:Prosys OPC UA Simulation Server,方便本地调试
  • 辅助工具:Prosys OPC UA Browser,浏览地址空间

如果没有现成的PLC设备,用模拟服务器就能跑通全部示例代码。Prosys的模拟服务器自带一批标签节点,比如Tag1、Tag2,数据类型和读写属性都能配置,非常适合学习阶段使用。

3. 实操:把一个OPC UA C#客户端从零跑起来

3.1 最小连接代码

先创建一个控制台项目并安装NuGet包:

dotnet new console -n OpcUaClientDemo cd OpcUaClientDemo dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client --version 1.5.374.522

然后准备一个Config.xml配置文件,放在项目根目录并设置复制到输出目录。UA-.NETStandard需要一个ApplicationConfiguration来描述客户端自身的证书和安全配置。下面的模板可以直接抄:

<?xml version="1.0" encoding="utf-8"?> <ApplicationConfiguration xmlns="http://opcfoundation.org/UA/2008/02/Types.xsd"> <ApplicationName>OpcUaClientDemo</ApplicationName> <ApplicationUri>urn:localhost:OpcUaClientDemo</ApplicationUri> <ApplicationType>Client</ApplicationType> <SecurityConfiguration> <ApplicationCertificate> <StoreType>Directory</StoreType> <StorePath>pki/own</StorePath> <SubjectName>CN=OpcUaClientDemo</SubjectName> </ApplicationCertificate> <TrustedPeerCertificates> <StoreType>Directory</StoreType> <StorePath>pki/trustedPeer</StorePath> </TrustedPeerCertificates> </SecurityConfiguration> <TransportConfigurations /> <TransportQuotas> <MaxMessageSize>4194304</MaxMessageSize> </TransportQuotas> <ClientConfiguration> <DefaultSessionTimeout>60000</DefaultSessionTimeout> </ClientConfiguration> </ApplicationConfiguration>

注意:pki路径这里要用正斜杠而不是反斜杠,Windows下容易在这踩坑。

接下来写连接代码:

using Opc.Ua; using Opc.Ua.Configuration; using Opc.Ua.Client; var application = new ApplicationInstance { ApplicationName = "OpcUaClientDemo", ApplicationType = ApplicationType.Client }; var config = await application.LoadApplicationConfiguration("Config.xml", false); bool certOk = await application.CheckApplicationInstanceCertificate(false, CertificateFactory.DefaultLifeTime); var endpointUrl = "opc.tcp://127.0.0.1:4840"; var endpointDescription = CoreClientUtils.SelectEndpoint(config, endpointUrl, true); var endpointConfiguration = EndpointConfiguration.Create(config); var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); using var session = await Session.Create( config, endpoint, false, "CSharpClientSession", 60000, new UserIdentity(new AnonymousIdentityToken()), null); Console.WriteLine("连接成功"); Console.WriteLine($"服务器: {session.Endpoint.EndpointUrl}");

解释一下关键参数。CheckApplicationInstanceCertificate是确认客户端证书存在,第一次运行会自动生成到pki/own目录。SelectEndpoint会解析服务器地址并筛选安全策略。Session.Create里最后一个null是事件回调,用来监听KeepAlive,后面断线重连会用到。这里用的匿名身份,生产环境建议用用户名密码或证书认证。

3.2 浏览节点并读取变量

连接成功后,先浏览一下服务器上的节点结构。核心是用Browse服务:

var browseDescription = new BrowseDescription { NodeId = ObjectIds.ObjectsFolder, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, NodeClassMask = (uint)NodeClass.Variable | (uint)NodeClass.Object, ResultMask = (uint)BrowseResultMask.All }; await session.Browse( null, null, 0, new BrowseDescriptionCollection { browseDescription }, out var browseResults, out _); foreach (var reference in browseResults[0].References) { Console.WriteLine($"{reference.DisplayName} -> {reference.NodeId}"); }

浏览结果会显示节点名和对应的NodeId,把要监控的节点记下来,然后读取变量值:

var nodeToRead = new ReadValueId { NodeId = new NodeId("ns=2;s=Tag1"), AttributeId = Attributes.Value }; await session.Read( null, 0, TimestampsToReturn.Both, new ReadValueIdCollection { nodeToRead }, out var results, out var diagnostics); DataValue dataValue = results[0].Value; Console.WriteLine($"Tag1 当前值: {dataValue.Value}");

读取时建议把多个点位放在一个ReadValueIdCollection里批量读,不要一次读一个。OPC UA的Read服务本身支持批量,把同一批次的所有点位放在一个请求里,能显著减少网络往返,点位多的时候性能差距非常明显。

3.3 写入变量

写值的核心是Write服务,写入值要包装成DataValue,同时要带上状态码和时间戳:

var nodesToWrite = new WriteValueCollection { new WriteValue { NodeId = new NodeId("ns=2;s=Tag2"), AttributeId = Attributes.Value, Value = new DataValue { Value = 88.6, StatusCode = StatusCodes.Good, SourceTimestamp = DateTime.UtcNow } } }; await session.Write(null, nodesToWrite, out var writeResults, out _); if (writeResults[0].Code == StatusCodes.Good) { Console.WriteLine("写入成功"); } else { Console.WriteLine($"写入失败: {writeResults[0].Code}"); }

写入时注意两点。第一,目标节点必须允许写,很多服务器会把部分变量设成只读,写的时候会返回BadWriteNotSupported。第二,数据类型要匹配,服务器节点是Double类型就不要传int,否则会被拒绝。建议写之前先看节点属性里的DataType定义。

3.4 用订阅模式接收数据变化

轮询读写是基本用法,但现场数据量大时高频轮询既不实时也浪费带宽。正确姿势是订阅模式:

var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 1000, KeepAliveCount = 5, LifetimeCount = 10, PublishingEnabled = true, MaxNotificationsPerPublish = 100 }; session.AddSubscription(subscription); subscription.Create(); var monitoredItem = new MonitoredItem(subscription.DefaultItem) { StartNodeId = new NodeId("ns=2;s=Tag1"), SamplingInterval = 100, QueueSize = 10, DiscardOldest = true }; monitoredItem.Notification += (item, args) => { if (args.NotificationValue is MonitoredItemNotification notification) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {item.StartNodeId}: {notification.Value.Value}"); } }; subscription.AddItem(monitoredItem); subscription.ApplyChanges(); Console.WriteLine("按回车退出"); Console.ReadLine();

很多新手会混淆两个时间参数:PublishingInterval是服务器的发布周期,也就是服务器多久汇总一次通知发给客户端;SamplingInterval是采样周期,也就是多久检测一次变量是否变化。1000毫秒发布、100毫秒采样,是现场比较常用的组合。QueueSize是通知队列大小,DiscardOldest=true表示队列满了优先丢弃旧数据、保留最新值,这样即使偶发网络抖动,也能保证客户端拿到的是最新状态。

4. 现场实战:断线重连、安全策略和性能优化

4.1 断线重连逻辑

工业现场网络必然会抖动,PLC也可能重启,OPC UA服务端会短暂断开,生产级客户端必须有重连机制。思路是注册Session的KeepAlive事件,检测到通讯异常或会话失效后,通过SessionReconnectHandler尝试重连:

private static SessionReconnectHandler _reconnectHandler; private static void Session_KeepAlive(Session session, KeepAliveEventArgs e) { if (e.ServiceResult.Code == StatusCodes.BadCommunicationError || e.ServiceResult.Code == StatusCodes.BadSessionIdInvalid) { Console.WriteLine($"连接异常: {e.ServiceResult},5秒后重连..."); if (_reconnectHandler == null) { _reconnectHandler = new SessionReconnectHandler(); _reconnectHandler.BeginReconnect(session, 5000, ReconnectHandler_Complete); } } } private static void ReconnectHandler_Complete(object? sender, EventArgs e) { Console.WriteLine("重连完成"); _reconnectHandler?.Dispose(); _reconnectHandler = null; }

注意:重连成功后订阅可能已经失效,必须重新创建订阅并挂载MonitoredItem。我最开始写重连逻辑时漏掉了这步,结果连接恢复了但数据不再推送,查了很久才发现是订阅没有重建。

4.2 安全策略与证书管理

OPC UA支持的安全策略有None、Basic256Sha256、Aes128_Sha256_RsaOaep等。None最常用但明文传输,生产环境不推荐。Basic256Sha256是现在工业设备默认支持得比较多的,安全性和兼容性平衡得不错。

如果启用安全连接,服务器证书需要加入本地可信列表。第一次连接报BadCertificateUntrusted,基本就是证书还没做信任配置。临时调试时可以在ApplicationConfiguration里把AutoAcceptUntrustedCertificates设为true,但千万别在生产环境这么干,安全策略形同虚设。

用户名密码认证的写法也很简单,把Session.Create里的UserIdentity换掉就行:

var userIdentity = new UserIdentity("admin", "password");

像西门子S7-1500这类设备,配置OPC UA Server时可以直接设用户名密码,这个UserIdentity就是对应的认证凭证。

4.3 性能优化与日志定位

点位多的时候,批量读、批量写、一个订阅挂多个MonitoredItem,这三招能解决大部分性能问题。Read和Write天然支持批量,只要不是特殊业务需求,不建议逐条调用。订阅也一样,把同刷新频率的点位放在同一个Subscription里,复用同一个发布周期,既省资源又容易管理。

日志方面,UA-.NETStandard底层有日志接口,调试时可以打开:

Utils.SetTraceLog(new ConsoleTraceListener(), TraceMasks.Error | TraceMasks.Information);

证书握手问题、底层通讯错误都能在日志里看到。生产环境建议把日志写到文件,按天滚动,现场出问题时定位会轻松很多。

4.4 常见问题速查表

现象可能原因处理办法
BadSecurityModeRejected客户端与服务器安全策略不匹配SelectEndpoint时指定可用的安全策略
BadCertificateUntrusted服务器证书未受信任把服务器证书加入TrustedPeerCertificate目录
BadNodeIdUnknownNodeId写错或命名空间不对用Prosys Browser查看真实NodeId
BadSessionIdInvalid会话已失效走重连逻辑重新建立Session
写入失败节点只读或类型不匹配确认节点写权限、DataType
订阅没有数据采样/发布间隔过大或订阅失效调整PublishingInterval、SamplingInterval,重连后重建订阅

这些是我实际调试中遇到频率最高的问题,基本占了日常排障的90%以上。

5. OPC UA在上位机生态里的延伸玩法

5.1 与PLC和MES的交互

最常规的组合是PLC作为OPC UA Server,C#上位机作为Client采集数据并下发控制命令。再往上,MES系统通过上位机提供的接口或数据库中间表取数,整个链路数据格式统一,新增设备只需要改节点配置表,上位机逻辑基本不用动。这种架构的好处是设备层变动被隔离在OPC UA地址空间里,上层系统感知不到具体设备差异。

5.2 与机器视觉、扫码枪的数据协同

热词里有人问“海康相机VisionMaster与C#上位机通讯使用什么协议比较好”。视觉软件的SDK可以直接在C#里调用,但如果你要的不只是图像结果,而是要产线PLC根据检测结果做分拣或报警,更合理的架构是:C#上位机作为视觉系统和PLC之间的枢纽,视觉软件通过SDK或网络协议把检测结果交给上位机,上位机再用OPC UA把结果写到PLC的节点上。

扫码枪同理,读到的条码通过串口或网口到上位机,上位机用OPC UA把条码写入服务器节点,MES和追溯系统直接从同一棵信息树里读。我一直觉得,OPC UA在上位机生态里不是某一个设备驱动,而是整条产线数据的公共总线。理解了这一点,很多协议选型的纠结自然就有了答案。

这套代码我在工控机上跑了大半年,连过西门子S7-1500、倍福PLC还有几个国产设备的数据点,稳定性和可维护性都超出预期。如果同事问我OPC UA C#从哪下手,我一般就建议先把连接、读、写、订阅这四件事跑通,再补上断线重连,就已经能覆盖大部分项目需求。后续想深入的话,可以研究服务器端开发,或者看看PubSub模式,那是另一套玩法,等有空再写写。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询