简介:OPC UA C# 示例是一份面向工业自动化开发者的实战代码包,重点演示如何使用C#构建OPC UA客户端,实现与PLC的安全通信与实时数据采集,适合需要快速上手UA客户端开发的工程师学习。压缩包共135个文件,以cs源码为主,辅以Visual Studio解决方案与项目配置、resx资源文件、png示意图及少量dll/exe程序集,整体尺寸仅1.61MB,便于下载和本地编译运行。资源已吸引3649人学习,内容覆盖客户端初始化、服务器连接、节点浏览、数据订阅、读写操作及异常处理等关键环节,代码中包含清晰的类与方法封装,并演示async/await异步编程模型,帮助开发者写出非阻塞、低延迟的实时采集逻辑。通过研读这些示例,读者可以快速掌握OPC UA核心概念与OPCFoundation.NetStandard.Opc.Ua等库的使用方法,并将其迁移到实际工业项目中,提升设备互联与数据采集的开发效率。
1. OPC UA C# 示例到底在解决什么:设备接入与上位机统一数据入口
车间设备要接到上位机系统,最难受的往往不是画界面,而是面对一堆互不兼容的协议:PLC、仪表、视觉系统各说各话。OPC UA C# 示例正是这类需求的起点——OPC UA 是工业界公认的数据互操作标准,C# 工程师又能借助官方协议栈用几十行代码起一个 Server 或 Client,把设备数据变成统一地址空间里的节点。这个方向适合做设备数据采集、C# 上位机、MES 对接的人。一个反直觉的结论是:OPC UA 规范厚得像本书,但 C# 侧的协议栈封装已经相当成熟,真正卡住进度的往往不是协议本身,而是节点地址、证书信任和变量类型这几个细节。下面按选库、建 Server、写 Client、排错到交付的顺序走一遍,照着做就能把示例改成自己的可用代码。
2. 选对 C# 库和基础概念:UA-.NETStandard 与三条必读规则
2.1 为什么默认选 OPC Foundation 的官方 C# 库而不是旧 SDK
OPC UA 从来不是一个单一协议,而是一整个家族:传输层、数据建模、安全策略、订阅机制都在里面。早期很多设备厂商提供的是基于 .NET Framework 的老 SDK,有的只认 Windows,有的接口还残留着 COM 味道,接入现代工程时很别扭。现在做 OPC UA C# 示例,我第一选择是 OPC Foundation 维护的 UA-.NETStandard 协议栈,NuGet 上对应的包名是 OPCFoundation.NetStandard.Opc.Ua,它用同一套 API 同时覆盖 Client 和 Server,能跑在 .NET Framework 4.6.1 以及 .NET 6/8 上,工控机里的老 Windows 和新 Linux 网关都照顾得到。
很多人问能不能自己实现一个轻量版,或者用 HTTP 转发 JSON 代替。我的看法是:OPC UA 的握手、安全通道、证书校验、订阅状态机这些底层机制非常繁琐,认真实现一遍至少是几个月的投入,而且容易在边界条件上翻车。UA-.NETStandard 虽然代码量不小,但协议细节基本封好,你只需要关心节点怎么建模、数据从哪来。选库时还要注意包内部构成:Opc.Ua.Core 提供基础类型,Opc.Ua.Server 支撑服务端框架,Opc.Ua.Client 封装客户端会话。工程里引用最上层的包即可,依赖会自动带下来,不需要手动逐个添加。
2.2 必须理解的三件事:Endpoint、NodeId、NamespaceIndex
这三件事是所有示例代码的钥匙,不理解它们,代码在你手里就是一个黑匣子,换个设备就无从下手。
Endpoint。一个 OPC UA Server 暴露的是端点地址,通常写作 opc.tcp://192.168.0.10:4840 这种形式。它不是一个普通 TCP 端口,而是由 EndpointDescription 描述的完整服务端点,里面带着传输协议、安全策略和证书信息。客户端连接时要把这三样一起匹配,只改 IP 不管安全策略,连接就会失败。
NodeId。地址空间里每个节点都用 NodeId 唯一标识,典型写法是 ns=2;s=CurrentTemperature。ns 表示 namespace index,s= 表示字符串类型的标识。读代码时不要把 ns=2 当成固定值,它是相对当前 Server 的 NamespaceTable 而言的。
NamespaceIndex。这是最容易出问题的地方。同一个设备在不同固件版本里 namespace 顺序可能变化,所以正式工程里应该先读取服务端的 namespace 表,再动态拼 NodeId,而不是手写死。后面避坑章节我会给出专门的处理函数。把这三个概念对应到 C# 里,就是 EndpointDescription 和 NodeId 两个类型,前者用来建会话,后者用来读写和订阅。
2.3 最小工程搭建:一条命令把依赖拉齐
常见做法是创建一个控制台项目,再添加 NuGet 包。命令行方式如下:
dotnet new console -n OpcUaSample cd OpcUaSample dotnet add package OPCFoundation.NetStandard.Opc.Ua逻辑说明:第一条命令创建 C# 控制台项目,第二条进入目录,第三条把官方 OPC UA 库加入项目依赖。生成后的 csproj 里会多一个 PackageReference,在 Visual Studio 里打开 NuGet 管理器搜同一个包名也能完成同样操作,选稳定版即可,不要装预览版。
安装完成后,在代码文件顶部补上四个 using 是最省事的起手式:
using Opc.Ua; using Opc.Ua.Configuration; using Opc.Ua.Client; using Opc.Ua.Server;逻辑说明:Opc.Ua 提供 NodeId、DataValue、Variant 等基础类型;Configuration 负责应用配置与证书;Client 和 Server 分别对应客户端会话与服务端框架,写哪端就保留哪端,全写上也不冲突。控制台程序第一次运行时并不需要手写完整配置文件,ApplicationInstance 会在 silent 模式下自动生成默认配置,这部分在下一章启动 Server 时再展开。到这里工程依赖就算拉齐了,可以开始写真正的示例。
3. 跑通 Server 端示例:从零拉起一个 OPC UA 服务的步骤
3.1 继承 StandardServer 搭服务端骨架
OPC UA 服务端不是简单地监听一个 TCP 端口,它要处理会话建立、订阅调度、安全通道、节点管理。UA-.NETStandard 把这些逻辑封装在 StandardServer 基类里,常见做法是写一个子类,在 OnServerStarting 里挂上自己的节点管理器。
using Opc.Ua; using Opc.Ua.Server; public class SampleServer : StandardServer { private SampleNodeManager _nodeManager; protected override void OnServerStarting(ApplicationConfiguration configuration) { // 启动节点管理器,把自定义变量挂进地址空间 _nodeManager = new SampleNodeManager(this, configuration); _nodeManager.Start(); base.OnServerStarting(configuration); } protected override void OnServerStopped() { _nodeManager?.Stop(); base.OnServerStopped(); } }逻辑说明:StandardServer 的启动流程会先加载配置、初始化安全通道、开始监听,然后回调 OnServerStarting 给子类挂载业务逻辑的机会。SampleNodeManager 是自定义的节点管理器,它才真正决定客户端能看到哪些变量;服务器停止时如果不释放节点管理器,订阅回调和句柄资源都会泄。
3.2 用 NodeBuilder 把变量挂进地址空间
服务端示例的核心是让客户端能读到你的数据。我一般用 CustomNodeManager 配合 NodeBuilder 来建节点,比手写 NodeState 对象清晰很多,也方便后续增加事件和方法的挂载点。
public class SampleNodeManager : CustomNodeManager { public SampleNodeManager(IServerInternal server, ApplicationConfiguration config) : base(server, config) { // 声明本管理器拥有的命名空间 URI SetNamespaces(new[] { "http://yourcompany.com/opcua/sample" }); } public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> references) { base.CreateAddressSpace(references); var builder = new NodeBuilder { Name = "CurrentTemperature", NodeType = NodeType.Variable, DataType = DataTypeIds.Double, AccessLevel = AccessLevels.CurrentRead, // 只读节点,调试公开数据够用 Value = new Variant(25.0) // 初始值 }; var nodeId = AddNode(builder, references); Console.WriteLine($"创建节点:{nodeId}"); } }逻辑说明:SetNamespaces 是关键一步,它把一个 URI 映射成服务端内部的 namespace index,客户端最终看到的 ns=2 就是由这里的映射顺序决定的。CreateAddressSpace 在整个服务启动阶段被调用一次,NodeBuilder 描述节点外观,AddNode 把节点以及它与根目录的父子关系写进地址空间。
参数说明:DataType 要使用 OPC UA 标准类型 ID,比如 DataTypeIds.Double,不要直接写 C# 的 double 字符串;AccessLevel 是位标志,读用 CurrentRead,需要测试写入功能时再改成 CurrentReadOrWrite;Value 是初值,真正做项目时应该接 PLC 变量或内存变量,并通过 Updated 事件把新值推进去。这个片段是完整结构里的核心环节,想一次拿到可编译版本,建议直接以官方 UA-.NETStandard 仓库里的 Quickstarts 为工程模板,再把 NodeManager 替换成你自己的逻辑。
3.3 启动入口与三项启动参数
服务端控制台入口长这样,它负责加载配置、检查证书,然后启动服务器实例。
static async Task Main() { var app = new ApplicationInstance { ApplicationName = "SampleOpcServer", ApplicationType = ApplicationType.Server, }; // silent: true 表示配置文件缺失时自动生成默认配置 await app.LoadApplicationConfiguration("server.config.xml", silent: true); await app.CheckApplicationInstanceCertificates(false); var server = new SampleServer(); await server.Start(app.ApplicationConfiguration); Console.WriteLine("OPC UA Server 已启动,按 Enter 退出"); Console.ReadLine(); await server.Stop(); }逻辑说明:ApplicationName 会写进应用证书和描述信息,ApplicationType 告诉协议栈当前进程是服务端。LoadApplicationConfiguration 负责读配置,silent 为 true 时如果没有配置文件,库会自动生成一份。CheckApplicationInstanceCertificates 用来检查或创建应用证书,示例环境传 false 能减少干扰,现场环境建议改成 true 强制校验。
常见做法是把端口和安全策略固定在配置 XML 里,方便部署时按工控机环境修改:
<ServerConfiguration> <BaseAddresses> <ua:String xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd">opc.tcp://0.0.0.0:4840</ua:String> </BaseAddresses> <SecurityPolicies> <ServerSecurityPolicy> <SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#None</SecurityPolicyUri> </ServerSecurityPolicy> </SecurityPolicies> </ServerConfiguration>参数说明:BaseAddresses 是监听地址,0.0.0.0:4840 表示对所有网卡监听,产线只跑内网时也可以换成具体 IP;SecurityPolicies 是一个列表,建议保留至少一项 Basic256Sha256 用于正式交接。调试期用 None 策略最省事,但不能带到交付环境。Windows 防火墙默认会拦 4840 端口,客户端连不上时先放行 TCP 4840,再排查证书问题,这个顺序能省下不少无用功。
4. 写透 Client 端示例:读取、写入与订阅的参数级拆解
4.1 建立会话:Session.Create 的关键参数
客户端示例第一步是连接服务端并建立会话。会话是 OPC UA 的逻辑连接,一个客户端可以长时间复用同一个 Session,不要频繁创建销毁。
using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; var app = new ApplicationInstance { ApplicationName = "OpcUaSampleClient", ApplicationType = ApplicationType.Client, }; await app.LoadApplicationConfiguration("client.config.xml", silent: true); await app.CheckApplicationInstanceCertificates(false); var endpointUrl = "opc.tcp://127.0.0.1:4840"; var endpoint = new ConfiguredEndpoint(null, new EndpointDescription(new Uri(endpointUrl))); var session = await Session.Create( app, endpoint, false, // updateBeforeConnect:连接前是否刷新端点信息 "SampleSession", // sessionName:多客户端调试时区分来源 60000, // sessionTimeout:会话超时毫秒数 new UserIdentity(), // 默认匿名身份,正式环境换用户名密码 null);逻辑说明:ConfiguredEndpoint 包装了端点地址,Session.Create 内部会完成 GetEndpoints、安全策略协商、创建会话三步。示例中省略了显式证书配置,协议栈会用默认方式处理,适合本地跑通。匿名身份只用于内网调试。
参数说明:sessionName 会出现在服务端会话列表里,上位机同时连多台设备时,用这个字段能快速定位是哪台在占资源;sessionTimeout 建议 30000 到 120000,太短会在做长时间批量操作时被服务端踢掉;UserIdentity 换成 UserNameIdentity 时,需要再用构造函数传入用户名密码,工厂环境通常要求这样。
4.2 读值和写值:AttributeId 与 Variant 类型
会话建立后最常用的操作是读取节点当前值。代码很短,但判断返回值是必修课。
var nodeId = new NodeId("ns=2;s=CurrentTemperature"); DataValue value = session.ReadValue(nodeId); if (StatusCode.Good == value.StatusCode.Code) { Console.WriteLine($"温度:{value.Value}"); } else { Console.WriteLine($"读取失败:{value.StatusCode}"); }逻辑说明:ReadValue 是便捷方法,等价于构造 ReadValueId 再调用 Read。DataValue 除了 Value 属性,还带着 StatusCode 和 SourceTimestamp。判断 StatusCode 是必须步骤,节点不存在时 Value 是 null,只看 Value 容易把故障当成正常数据。打印时 Value 能直接转字符串,做类型判断时再用 value.Value.TypeInfo.BuiltInType。
写入的逻辑复杂度更高一点,因为要明确写哪个属性、写什么类型:
var writeNodeId = new NodeId("ns=2;s=SetSpeed"); var writeValue = new WriteValue { NodeId = writeNodeId, AttributeId = Attributes.Value, Value = new DataValue(new Variant(1200.5)), // 类型必须与服务端一致 }; var results = session.Write( new WriteValueCollection { writeValue }, out var writeResults, out var diagnosticInfos); if (writeResults[0].Code == StatusCodes.Good) { Console.WriteLine("写入成功"); } else { Console.WriteLine($"写入失败:{writeResults[0]}"); Console.WriteLine($"诊断信息:{diagnosticInfos[0]}"); }逻辑说明:WriteValueCollection 支持一次写多个节点,writeResults 与传入顺序一一对应。AttributeId 一般就是 Attributes.Value,这个参数容易漏写。Variant 的装箱类型必须和服务端声明一致,服务端定义 Float 你写 Double,协议栈不会自动转换,而是直接回一个 BadTypeMismatch。
参数说明:诊断信息数组在失败时非常有用,正式项目里建议打日志时把 diagnosticInfos 一起输出。写入前先用 UAExpert 或 Browse 功能确认节点 AccessLevel 可写,很多设备变量是只读的,直接写只能拿到 BadAccessDenied。
4.3 订阅数据变化:两个 Interval 的配合
如果只是周期轮询,用循环加 Thread.Sleep 也能跑,但 OPC UA 订阅的价值在于服务端按采样间隔采集、数据变化时才推送,网络开销小很多。
var subscription = new Subscription(session, 1000) // 第二参数为发布间隔,单位毫秒 { DisplayName = "TempSubscription", KeepAliveCount = 5 }; var monitoredItem = new MonitoredItem { StartNodeId = new NodeId("ns=2;s=CurrentTemperature"), SamplingInterval = 500, // 采样间隔,单位毫秒 QueueSize = 10, // 队列容量 DiscardOldest = true // 队列满时丢弃最旧数据 }; monitoredItem.Notification += OnItemNotification; subscription.AddItem(monitoredItem); session.AddSubscription(subscription); subscription.Create();private static void OnItemNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e) { foreach (var notification in e.NotificationValue.MonitoredItems) { Console.WriteLine($"{item.StartNodeId} = {notification.Value.WrappedValue.Value}"); } }逻辑说明:Subscription 是服务端的发布对象,MonitoredItem 是要监视的单个节点。PublishingInterval 是服务端把一批变化推送给客户端的节拍,SamplingInterval 是服务端读取底层数据源的时间间隔。两个值不必相等,但都不能小于服务端配置的下限,否则会被协议栈自动放大。
参数说明:QueueSize 表示服务端为这个监视项缓存多少个采样值;DiscardOldest 为 true 时队列满就丢最旧数据,适合实时画面类应用;如果每个数据都不能丢,就把 QueueSize 调大并把 DiscardOldest 设为 false,代价是客户端处理不过来时内存占用上升。订阅创建后,控制台程序要用 Console.ReadLine 挂住主线程,WinForms 则要把 session 和 subscription 存成类字段,防止被垃圾回收后回调静默失效。
5. OPC UA C# 示例排错避坑:证书、命名空间与类型的典型事故
5.1 连接报 BadCertificateUntrusted:信任链没建立
现象:Session.Create 抛出 ServiceResultException,状态码是 BadCertificateUntrusted,有时伴随 BadSecurityModeRejected。
原因:OPC UA 默认做双向证书校验,客户端不信任服务端证书,或者两端的 SecurityPolicy 不在同一个安全水平。UA-.NETStandard 的默认配置把对方证书当作未受信任对象,这是有意为之的安全设计,不是 bug。
解决:最常见做法是交换证书。服务端证书生成在自己的证书目录里,客户端第一次连接后,把对方证书导入本机受信任的证书存储区。调试期可以临时把客户端配置文件的 AutoAcceptUntrustedCertificates 设为 true,但这只适合完全隔离的测试网络。如果改完证书仍然报 SecurityModeRejected,就检查两端的 SecurityPolicyUri 列表,让客户端选中的策略实际存在于服务端列表中。
5.2 读节点报 BadNodeIdUnknown:ns=2 不是万能钥匙
现象:服务端连接成功,但读取任何 ns=2 的节点都返回 BadNodeIdUnknown,而用 UAExpert 浏览时同一个节点显示正常。
原因:NamespaceIndex 对不上。UAExpert 显示的 ns=2 是它连接后动态解析的结果,服务端固件升级、地址空间重新排序都可能改变 index,你把 ns=2 写死在代码里,换一台设备就失效。
解决:不要手拼 NodeId。建立会话后先读一次服务端的 NamespaceTable,再根据 URI 动态拼接:
string uri = "http://yourcompany.com/opcua/sample"; int nsIndex = session.NamespaceUris.GetIndexOrAppend(uri); var nodeId = new NodeId($"ns={nsIndex};s=CurrentTemperature"); Console.WriteLine($"动态命名空间索引:{nsIndex}");逻辑说明:GetIndexOrAppend 在 URI 已存在时直接返回对应 index,不存在时追加到客户端的本地 namespace 表,从而保证构造出的 NodeId 能在当前服务端得到解析。这是我每接一台新设备都要做的第一步,也是避免 BadNodeIdUnknown 最根本的解法。
5.3 写入报 BadTypeMismatch 或 BadAccessDenied
现象:Write 调用返回的结果码不是 Good,常见两类。第一类是 BadTypeMismatch,第二类是 BadAccessDenied。
原因一:类型不匹配。服务端变量类型是 Float,客户端用 Variant(1200.5) 写入,C# 里的浮点字面量默认是 Double,协议栈发现 Variant 类型与服务端定义不一致,直接拒绝。解决:先读一次该节点,从返回的 DataValue 里取出类型,然后按原类型拼新的 Variant。原因二:没有写权限。节点 AccessLevel 是 CurrentRead,代码却尝试写入;或者服务端按用户角色做了限制,匿名用户只能读。解决:用 UAExpert 查看节点属性里的 AccessLevel,确认可写后再动代码;需要写权限时,让设备厂家开放操作账户。
5.4 订阅触发一次后不再回调
现象:订阅创建成功,第一次数据变化回调正常,之后完全静默。手动用 UAExpert 看,数据明明一直在变。
原因:先检查两个 Interval。服务端规定最小发布周期是 1000ms,客户端把 SamplingInterval 设成 100ms,协议栈会把它放大到服务端允许的下限,表现就是延迟变大而不是彻底断掉。真正静默的常见原因是节点本身没有数据变化事件,只有值变化才推送。
解决:调试期把 PublishingInterval 和 SamplingInterval 都设成 1000ms,先把链路确认通。然后在回调里打时间戳,观察两次回调的间隔。如果确认节点在变化但还是没有回调,就用 UAExpert 的 DataChange 订阅做对照实验,看问题是出在客户端代码还是服务端数据源。
5.5 长时间运行后会话被服务端断开
现象:程序跑几小时后,再调用 Read 报 BadSessionIdInvalid 或 BadSessionClosed,只能重启上位机恢复。
原因:SessionTimeout 到期,或者 KeepAlive 机制判定客户端已经失联。常见错误是把订阅的 KeepAliveCount 设得过大,乘以 PublishingInterval 后判活时间很长,网络短暂闪断后要等很久才触发重连。
解决:为 Session 挂 KeepAlive 事件,在检测到 KeepAliveStopped 或状态异常时调用 session.Reconnect()。注意 Reconnect 成功后旧订阅会自动重建,但 MonitoredItem 的事件处理器要重新挂载,这一步最容易漏。如果你发现掉线后必须 new 一个 Session 才能恢复,说明旧会话的订阅全部丢失了,这通常是 session 对象被局部变量释放,或者重连逻辑写在了错误位置。现场长期运行时,我习惯把 Session 做成单例组件,并固定使用同一个实例执行读写。
提示:以上五条基本覆盖了 OPC UA C# 示例从调试到上线的常见事故。如果你遇到的报错不在这五类里,先翻诊断信息里的 SymbolicId,再反查服务端日志。
6. 从示例到可交付:UAExpert 验证与安全策略落地的习惯
6.1 用 UAExpert 做黑匣子验证
写完示例别急着接设备,先本地起 Server,再用 UAExpert 连接把它当黑匣子测一遍。UAExpert 是 OPC Foundation 生态里常用的调试客户端,能浏览地址空间、查看节点属性、创建订阅观察数据变化。我一般验证三个点:节点树是否完整、AccessLevel 是否符合预期、订阅是否按设定周期推送。它还能直接显示服务端解析后的 namespace index,和代码里打印出来的对照,能在五分钟内确认 5.2 节说的命名空间问题是否存在。
6.2 交付前确认的关键运行参数
表里的参数是现场起步值,没有特殊需求不要盲目调小:
| 参数 | 推荐初值 | 说明 |
|---|---|---|
| SessionTimeout | 60000 ms | 长稳运行调到 120000 |
| OperationTimeout | 120000 ms | 批量操作超时,设太短会误报 |
| PublishingInterval | 1000 ms | 数据展示够用,追求实时再降 |
| SamplingInterval | 500 到 1000 ms | 服务端有下限,设太小会被自动放大 |
SecurityPolicy 至少要保留 Basic256Sha256 一项,客户端和服务端保持一致;证书目录做备份,换工控机时能直接恢复信任关系。调试环境允许 None 策略,但不能出现在交付清单里。
6.3 我的交付习惯
第一次把示例搬到产线时,我踩的就是订阅掉线这个坑,当时以为重连就是重新 new 一个 Session,结果新会话的订阅全部丢失,数据整整丢了半个多小时。现在我的流程固定成三步:先在本地用 UAExpert 验证节点和订阅;再用模拟服务跑 8 小时稳定测试;最后才接真实设备。证书和命名空间这两个坑,几乎每次对接第三方设备都会碰到一次,所以 NodeId 动态拼接和证书交换这两个工具函数,我会放进公共组件里复用。OPC UA C# 示例只是起点,把这些底层逻辑沉淀成组件,后续接设备的速度会快很多。希望帮到你。
本文还有配套的精品资源,点击获取