简介:C# OPC UA客户端源码,是一个面向工业通讯开发者的可直接运行参考工程,既可用于连接西门子机床自带OPC UA服务器,也兼容Kepware KepServer等第三方OPC UA服务,重点弥补了网上大多源码不支持西门子用户名密码或加密认证的短板。包内共2000个文件,以C#源码(cs)与工程文件(sln、csproj)为主,同时包含配置文件(xml、settings)、说明文档(txt、md、rtf)及若干C/C++辅助模块,压缩包大小38.89MB,整体结构清晰,便于二次开发。源码基于OPC UA .NET框架,涵盖安全策略协商、会话管理、命名空间适配等核心逻辑,并包含大量示例客户端与测试页面(htm/js),可帮助开发者快速理解从连接到读写订阅的完整流程。目前已有535人学习,适合有C#基础、正在做设备数据采集或工业互联集成的开发者参考。
1. 为什么一套 C# 的 OPC UA 客户端源码值得自己搭
车间里同时有两类设备:西门子机床用 OPC UA 上报运行状态和主轴数据,另一套系统靠 KepServer 把各种自定义标签汇到一块。通用客户端连起来容易,但生产环境要做数据过滤、异常重连、接 MES,最后还是得自己掌握一套 C# 的 OPC UA 客户端源码。这篇不讲 OPC UA 协议怎么从零实现,而是讲怎么把源码用起来:连接类怎么封装、节点 ID 怎么写、西门子机床和 KepServer 两边的差异在哪、哪些坑现场真的会翻车。
适合的读者是两类人:一类是刚接手设备采集项目、需要用 C# 把数据从 OPC UA 服务器读出来的开发;另一类是已经能连上单个服务器,但换一个设备厂商就频繁出问题的实施工程师。把这两边都打通,客户端才算真正落地,而不只是能连上演示环境。
2. 正式写代码前:客户端对象模型,以及西门子与 KepServer 的连接差异
2.1 客户端三件套:ApplicationConfiguration、EndpointDescription、Session
C# 做 OPC UA 客户端,绕不开三个核心对象。第一个是ApplicationConfiguration,它描述的是“我这个客户端是谁”,包括客户端自己的应用名、证书、信任列表、传输配额、超时时间。第二个是EndpointDescription,它描述的是“我要连的服务器是谁”,包含服务器地址、端口、支持的安全策略、服务器证书。第三个才是Session,连接建立后的实际会话对象,读写浏览都靠它。
很多初学者把精力放在写读写方法上,结果第一个连不上的报错全出在证书和 ApplicationConfiguration 上。我在排错时基本遵循这样的顺序:先看配置,再看端点,最后才看节点。因为 Session 没建立,后面所有节点操作都是空谈。
这里有个容易混淆的点:OPC UA 不是 HTTP 那种“发请求拿响应”的模型。Session 建立后,客户端和服务器之间维持一条长连接,服务器会主动发心跳,订阅数据也是服务器往客户端推。所以后面的代码里我会特别强调订阅的生命周期,连接断了不重连,数据流就静默消失,这是采集程序最常见的隐患。
2.2 都是 OPC UA,西门子机床与 KepServer 的差异表
很多人以为“都是 OPC UA,换个 URL 就完事”,这个想法在现场会碰壁。协议栈是同一个,但服务器的实现侧重点完全不一样。我把两边的差异整理成一张表,写代码前先对着确认一遍。
| 对比项 | 西门子机床侧常见情况 | KepServer 侧常见情况 |
|---|---|---|
| 默认端口 | 通常是 4840 | 常见 49320,具体看 Kepware 配置 |
| 安全策略 | 常启用 Basic256Sha256,证书交换严格 | 默认可能开放 None,也支持 Basic256Sha256 |
| 用户认证 | 匿名或机床配置的操作员账户 | 可设 Windows 账户或 OPC UA 用户名密码 |
| 节点 ID 风格 | 字符串节点 ID 多,命名空间数字大 | 通道.设备.标签结构,整数或字符串节点 ID 都有 |
| 命名空间 | 由机床固件定义,通常是西门子特有 URI | 由 Kepware 工程配置定义,项目间不通用 |
| 浏览行为 | 节点树带设备模型语义,层次较深 | 基本是“通道→设备→标签”三层,扁平直观 |
这张表不是让你死记,而是提醒一件事:节点 ID 里的ns=数字不是全局唯一标识,它只是“这个服务器内命名空间的序号”。同一个节点,换个服务器可能ns=2变成ns=5,但对应的 NamespaceUri 不变。所以写代码时,不要写死ns数字,后面我会给一个专门的处理方法。
还有一个现场容易踩的差异:西门子机床的 OPC UA 服务器往往和 PLC 程序强绑定,固件版本变了,节点树可能整体挪位置;KepServer 则完全由工程师自己建点,标签层次相对稳定。这决定了你的客户端代码必须支持“先浏览再读取”,否则固件一升级,写死的节点 ID 就全废了。
2.3 SDK 怎么选:官方网站协议栈而不是第三方打包
C# 做 OPC UA 客户端,主流的方案是直接用 OPC 基金会官方维护的 .NET 协议栈。NuGet 里的官方包名一般是OPCFoundation.Opc.Ua.Client和OPCFoundation.Opc.Ua.Configuration,老一点的项目里看到OPCFoundation.NetStandard.Opc.Ua也一样,那是在 .NET Standard 时代的分装。
选择官方协议栈而不是第三方封装,原因很实在。OPC UA 的复杂度不在收发数据,而在证书管理、安全策略协商、订阅生命周期、服务器诊断这些细节上,官方栈把这些协议层的东西全实现了,你只需要关注配置和业务,不需要把协议坑重新踩一遍。第三方封装包看起来上手快,但很多只实现了匿名连接和 None 安全策略,一旦换成西门子这种证书校验严格的服务器,封装层就会变成黑匣子,出了问题根本没法排查。
另外要注意版本差异。官方栈 1.4.x 和 4.x 的 API 有变化,老的客户端里用的是Session.Create,新一代用了Session.CreateAsync,订阅的构造方式也不完全一样。我会在下面代码按新写法来,同时在注释里说明老版本怎么改。拿到的源码如果 API 对不上,先看它引用的包版本,再去对比对应版本的方法签名,比乱改代码快得多。
3. 一个能通吃的 C# OPC UA 客户端源码:连接、浏览、读写怎么组织
3.1 最小解决方案和证书目录规划
我先给一个最小工程结构。用控制台项目演示足够,实际接入 MES 时把连接类原样搬进服务或 Windows 服务就行。
dotnet new console -n OpcUaTool cd OpcUaTool dotnet add package OPCFoundation.Opc.Ua.Client dotnet add package OPCFoundation.Opc.Ua.Configuration dotnet build这里有个容易被忽略的规划:证书目录。OPC UA 客户端第一次启动时会生成自己的应用证书,同时要维护信任的服务器证书列表。我一般会在运行目录下建三个目录:
pki/client # 客户端自己的证书和私钥 pki/trusted # 信任的服务器证书 pki/issuer # 颁发者 CA 证书证书目录先规划好的好处是:换服务器时不用把证书散落在系统证书库里,项目整体拷贝就能迁移。目录路径在ApplicationConfiguration.SecurityConfiguration里配置,后面代码会写。注意,证书目录一定要用绝对路径或者基于AppContext.BaseDirectory拼接,别用Directory.GetCurrentDirectory(),因为服务方式启动时当前目录经常不是程序目录,这是后期最容易翻车的一个细节。
3.2 Session 创建代码:一条连接完成两端握手
连接是 OPC UA 客户端最核心的一步。下面代码把客户端初始化、端点选择、会话建立三个动作封装好,后续不管连西门子还是连 KepServer,调同一个方法就行。
// OpcUaConnector.cs using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; public class OpcUaConnector { private ApplicationConfiguration _appConfig; // 客户端身份初始化:生成证书、准备信任目录 public async Task InitializeAsync(string clientName) { var app = new ApplicationConfiguration { ApplicationName = clientName, ApplicationUri = $"urn:{Dns.GetHostName()}:{clientName}", ApplicationType = ApplicationType.Client, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = "pki/client", SubjectName = $"CN={clientName}" }, TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "pki/trusted" }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "pki/issuer" }, // 生产环境必须保持 false,否则等于放弃证书校验 AutoAcceptUntrustedCertificates = false }, TransportQuotas = new TransportQuotas { OperationTimeout = 30000, MaxStringLength = 1024 * 1024, MaxByteStringLength = 4 * 1024 * 1024 }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } }; // 这一步会读取或生成本地客户端证书 await app.InitializeAsync(); _appConfig = app; } // 选择端点并建立会话,用户名密码为空时走匿名 public async Task<Session> ConnectAsync( string endpointUrl, string userName = null, string password = null, bool useSecurity = true) { // 获取服务器端点描述,useSecurity=false 时绕过安全策略 EndpointDescription endpoint = CoreClientUtils.SelectEndpoint( _appConfig, endpointUrl, useSecurity); // 匿名身份和用户名密码身份是 OPC UA 的两种常规方式 var identity = string.IsNullOrEmpty(userName) ? new UserIdentity(new AnonymousIdentityToken()) : new UserIdentity(userName, password); var session = await Session.CreateAsync( _appConfig, endpoint, updateBeforeConnect: true, checkDomain: false, _appConfig.ApplicationName, 60000, identity); return session; } }这段代码里最关键的是SelectEndpoint这一步。它会去服务器那询问支持的端点列表,然后按客户端配置挑一个匹配的安全策略。useSecurity参数在开发和现场排查时很有用,设成 false 能快速验证网络通不通、节点路径对不对,但生产环境必须设回 true,否则等于裸奔。
Session.CreateAsync里的updateBeforeConnect: true是要求客户端在连接前先更新端点信息,这个参数在服务器证书轮换后特别重要。如果服务器证书刚换过,不更新端点,连接就会报证书不匹配。checkDomain: false表示不校验服务器地址域名和证书里的域名是否一致,西门子机床很多用 IP 访问,域名对不上很正常,这个开关一定要开着。
3.3 浏览与读取:节点 ID 的“字符串格式”是两边统一的关键
连接建立后,第二件要做的事就是把节点 ID 搞清楚。OPC UA 节点 ID 在代码里就是一个字符串,比如ns=2;s=Channel1.Device1.Tag1,KepServer 里常见的也就是这种格式。西门子机床那边则常出现ns=3;s=...这种较长字符串 ID。不管是哪边,读取前都要先解析成NodeId对象。
// 节点读取与浏览工具 public async Task<DataValue> ReadValueAsync(Session session, string nodeId) { var id = new NodeId(nodeId); // 解析 "ns=2;s=Channel1.Device1.Tag1" return await session.ReadValueAsync(id, CancellationToken.None); } // 列出指定节点下所有子节点,连接后先“摸树” public List<ReferenceDescription> BrowseChildren(Session session, string nodeId) { var browser = new Browser(session) { BrowseDirection = BrowseDirection.Forward, NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable), Recursive = false }; return browser.Browse(new NodeId(nodeId)); }NodeId的字符串解析格式是ns=<命名空间序号>;<类型>=<值>,类型有s字符串、i整数、gGUID、b字节串几种。两边服务器最常用的是s和i。KepServer 里如果标签建的是整形 NodeId,你会看到ns=2;i=12345;西门子那边则大量使用s类型。
这里要特别提醒:解析时不要把ns数字写死在配置里。同一个 KepServer 工程,如果删掉某个设备再重建,命名空间的顺序可能变化,原来的ns=2就变成ns=3。我在项目里的做法是连接后先拿服务器的Namespaces列表,再按 NamespaceUri 去匹配,而不是按数字。源码里如果只按ns数字写死,换一次工程配置就全盘崩溃,这类问题特别隐蔽。
读取返回值是一个DataValue,里面包含Value和StatusCode。写采集逻辑时,先判断StatusCode是不是Good,再取Value。很多新手只看值不看状态,结果读到的是BadWaitingForInitialData这类中间状态,还以为设备数据就是那样。
3.4 写入与订阅:采集程序真正每天在用的两个功能
读取适合低频查询,生产采集主要靠订阅。西门子机床和 KepServer 都支持订阅,只是节点 ID 不同,订阅代码可以完全复用。我先把写入方法给出,再给订阅的骨架。
// 写入单个节点值,用于下发指令或修改参数 public async Task WriteValueAsync(Session session, string nodeId, object value) { var writeValue = new WriteValue { NodeId = new NodeId(nodeId), AttributeId = Attributes.Value, Value = new DataValue(new Variant(value)) }; var response = session.Write( requestHeader: null, nodesToWrite: new WriteValueCollection { writeValue }, diagnostics: out var diagnostics); // 必须逐条检查返回状态,服务器不保证所有节点都写成功 if (response.Results[0].Code != StatusCodes.Good) { throw new Exception($"写入失败,状态码:{response.Results[0]}"); } }写入的坑在于类型匹配。OPC UA 是强类型协议,你写一个 C# 的int,服务器节点类型是Int16,很多服务器会拒绝。所以写入前最好先读取一次当前节点,拿到它的数据类型,再转换后写入。西门子机床的某些参数对写入类型尤其严格,这一点在第四章会再提。
订阅的代码则分四步:创建订阅、加到会话、添加监控项、触发采集。发布间隔PublishingInterval是服务器下发数据的周期,采样间隔SamplingInterval是服务器读取底层数据源的周期,两者可以不同。
// 创建订阅并添加监控项 var subscription = new Subscription(session) { PublishingInterval = 500, // 服务器每 500ms 推送一次数据 LifetimeCount = 60, KeepAliveCount = 10, DisplayName = "MachineData" }; session.AddSubscription(subscription); subscription.Create(); var item = new MonitoredItem { StartNodeId = new NodeId("ns=3;s=Machine/Spindle/Speed"), SamplingInterval = 250, // 底层数据源每 250ms 采集一次 AttributeId = Attributes.Value }; item.Notification += OnDataChanged; subscription.AddItem(item); subscription.ApplyChanges();LifetimeCount和KeepAliveCount这两个参数容易被忽视。简单理解:发布间隔乘上KeepAliveCount是“多久没有数据就算连接可疑”,发布间隔乘上LifetimeCount是“多久没有真实数据就把订阅销毁”。现场如果网络时不时抖动,把KeepAliveCount调大到 20 以上,能避免订阅被服务器误杀。我见过不少项目把这两个值设成默认,结果晚上网络闪断一次,订阅就悄无声息没了,第二天早上一看数据缺了一整段。
4. 对接西门子机床 OPC UA:先确认参数,再浏览节点树
4.1 西门子侧要确认的 4 个开关
接西门子机床前,不要急着写代码,先确认机床侧几项配置。我在现场一般按下面这张表一个个问过去,少一项就可能在握手阶段卡住。
| 确认项 | 常见位置 | 不对齐的后果 |
|---|---|---|
| OPC UA 服务是否激活 | 机床 PLC 程序或驱动配置里启用 | 端口扫不到,连接直接拒 |
| 防火墙放行端口 | 机床侧或网络安全策略 | TCP 能通但握手无响应 |
| 安全策略与证书模式 | 机床 OPC UA 配置页 | BadSecurityChecksFailed |
| 用户名密码或匿名开关 | 机床用户管理 | 连接后权限不足,浏览不到节点 |
西门子机床的 OPC UA 服务器不是默认开着的,很多需要在 PLC 程序里调用相应指令激活服务,或者通过机床面板的通信设置打开。这一步没做,后面代码再对也连不上。判断方法很简单:先用网络工具测端口通不通,通了再看握手报错。
4.2 用一次浏览代替写死的访问路径
西门子机床的节点树不像 KepServer 那么扁平,它有设备模型语义,层次比较深,而且不同固件版本可能有变化。所以接西门子机床,我强烈建议先写一个浏览工具,把节点树打印出来,确认节点真实路径后再固化配置。
调用第一节的BrowseChildren,从标准 Objects 文件夹开始,递归打印出节点名和 NodeId。这个动作在工程上叫“摸树”,摸完树你才会知道这台机床实际暴露了哪些数据点,而不是拿着别的项目的节点配置直接照搬。
// 递归浏览节点树,打印节点显示名和 NodeId public void PrintTree(Session session, string nodeId, int depth) { var children = BrowseChildren(session, nodeId); foreach (var child in children) { var indent = new string(' ', depth * 2); Console.WriteLine($"{indent}{child.DisplayName} -> {child.NodeId}"); // 只对 Object 和 Variable 节点继续下探,避免遍历到非数据节点 if (child.NodeClass == NodeClass.Object || child.NodeClass == NodeClass.Variable) { PrintTree(session, child.NodeId.ToString(), depth + 1); } } }这里有个细节:ReferenceDescription.NodeId可能是 ExpandedNodeId,打印出来的是完整格式,带 NamespaceUri。转成字符串后可以直接传给ReadValueAsync,但要注意,有些服务器的节点 ID 在浏览时返回的是http://...开头而不是ns=数字。这时候不要慌,这是 ExpandedNodeId 的正常表现,NodeId构造器可以直接解析这个格式。
现场接西门子机床时,我遇到过两次节点树和文档不一致的情况:一次是固件升级后某个数据点被挪到别的分支下,一次是机床厂商启用了“仅显示已启用数据点”选项,没有使能的数据点直接不出现。这两种情况都只能靠浏览发现,所以“先浏览再写死”这个习惯必须养成。节点文档只能当参考,不能当真理。
4.3 安全策略与匿名:哪种现场该用哪个
西门子机床的安全策略配置,在调试期和稳定运行期应该区别对待。调试期为了快速验证节点路径,我会用useSecurity: false先连一次,确认节点能读到,再把安全策略打开做正式连接。这不是偷懒,是为了把“网络问题”和“证书问题”两个变量拆开,避免同时排查。
正式运行期,建议至少启用Basic256Sha256,证书交换模式按服务器要求配置。如果机床侧配置成了签名证书模式,而客户端代码用的是全匿名,连接会在Session.CreateAsync阶段直接失败。这时候不要慌,把机床导出或者下载的服务器证书放进pki/trusted目录,重启客户端程序让它重新读取证书列表,再连一次。
还有一个必须区分清楚的概念:匿名 ≠ 不安全。OPC UA 的“匿名”只是不认证用户身份,传输层的签名和加密仍然可以正常工作。所以如果机床要求必须匿名,安全策略照样可以用Basic256Sha256。这一点别搞混了,很多实施人员一看到匿名就自动把useSecurity设为 false,结果证书问题变成连接失败,还找不到原因。
5. C# OPC UA 客户端最容易翻车的 5 个坑:现象、原因、解决
5.1 连西门子报 BadSecurityChecksFailed
现象:客户端连接西门子机床 OPC UA,握手阶段直接抛BadSecurityChecksFailed。
原因:这个错 90% 是证书校验不过。要么客户端证书没被机床信任,要么机床服务器证书没有放到客户端信任列表,要么机床侧更换过证书但客户端还在用旧缓存的端点信息。
解决:先关掉AutoAcceptUntrustedCertificates,程序首次连接时会生成客户端证书,把这个证书导入到机床信任列表;再把机床侧的服务器证书导出放到pki/trusted目录,重启客户端。如果之前已经用旧的端点信息连过,删掉本地证书缓存,重新跑一次SelectEndpoint。这里几乎没有别的玄学,证书放对了就能过。
5.2 连上 KepServer 但浏览不到自定义标签
现象:客户端能建立会话,浏览对象节点也正常,但就是看不到 KEPware 里新建的通道和标签。
原因:KepServer 的 OPC UA 浏览根目录不只是标准的 Objects 文件夹。Kepware 工程里的通道设备标签挂在它自己的命名空间下,如果浏览起点在Objects标准位置,或者没有展开服务器对象节点的下一层,自定义标签就被忽略了。
解决:连接后先浏览Objects下的所有子节点,找到 Kepware 的服务器对象节点,从那里展开。还有一种情况是 Kepware 的 OPC UA 服务没有勾选“允许浏览”,需要在 Kepware 配置里把 UA 服务端启用并设置端口。端口不对也会表现为能连上但节点为空,因为连到了一个空壳服务。
5.3 读取报 BadNodeIdUnknown,节点明明存在
现象:用手册上的节点 ID 读取,服务器返回BadNodeIdUnknown。
原因:节点 ID 里的ns数字不对,或者节点 ID 格式和服务器实际不匹配。比如服务器用的是字符串节点 IDs=Spindle.Speed,你按整数i=1000去读,自然找不到。
解决:不要凭文档猜,用BrowseChildren把节点树打出来,直接复制工具打印出的 NodeId 字符串来用。同时检查节点 ID 里的命名空间序号是否匹配当前服务器。我给别人做支持时,遇到这类问题从来不看手册,只看浏览结果,节点写错的情况百分之百能在这一步被发现。
5.4 证书目录是相对路径,换个运行目录就全断了
现象:程序在本机跑得好好的,部署到服务器或者做成 Windows 服务后,证书校验全失败,连接也不稳定。
原因:证书路径用了Directory.GetCurrentDirectory()或相对路径拼接,进程启动时的工作目录是系统服务目录或某个固定目录,和开发时的目录不一致,证书目录找不到了。客户端程序没找到自己的证书,就会现场临时生成一个,服务器侧信任列表里又没有这个新证书,连接直接失败。
解决:证书路径统一用AppContext.BaseDirectory拼接,或者把pki目录放在程序集所在目录外部的固定路径。部署后第一件事就是检查pki目录下有没有生成证书文件,没有就说明路径配错了。
5.5 订阅跑了两小时突然不再回调
现象:订阅一开始数据正常推送,几小时后回调函数不再触发,程序没有报错。
原因:最常见的是服务器端会话超时,或者中间网络设备把空闲连接回收了。OPC UA 会话有超时时间,订阅的KeepAliveCount太小,客户端没有及时发出 keepalive,服务器就把订阅释放了。
解决:把KeepAliveCount调到 10 以上,同时监听Session.KeepAlive事件,在事件里判断连接状态并主动重连。下一章我会给出一个具体的重连方案。还有一个容易忽视的点:不要在回调函数里做耗时操作,比如写数据库、发 HTTP 请求,回调阻塞会拖累订阅处理线程,时间一长客户端和服务器之间确认消息超时,订阅就会失效。这个坑我踩过两次,后来统一改成回调只入队,后台线程负责写库。
6. 从“能连上”到“能长期采集”:重连、超时和验证方法
6.1 用 KeepAlive 和 Reconnect 把客户端变得有韧性
连上很简单,难的是跑一个季度不丢数据。我一般会在 Session 建立后立刻挂上 KeepAlive 事件,把自动重连的逻辑放在事件回调里。这里给一个现场验证过的思路。
// 连接建立后挂上 KeepAlive 监听 session.KeepAlive += Session_KeepAlive; private static async void Session_KeepAlive(Session session, KeepAliveEventArgs e) { // e.Status 不是 Good 说明连接出现异常,需要重连 if (e.Status != null && e.Status.Code != StatusCodes.Good) { try { // 先尝试会话级重连,失败再重新建立完整连接 await session.ReconnectAsync(); } catch (Exception) { // 走到这里说明会话已经不可救药,只能重建 } } }重连逻辑要注意两点。第一,重连不是简单重新CreateAsync,Session对象上保留了大量订阅信息,直接重建会话会把所有订阅丢掉。正确做法是优先调用ReconnectAsync,让服务器恢复原有订阅关系。第二,如果重连失败,要在异常里做完整的SelectEndpoint到Session.CreateAsync流程,但这时候订阅要重新创建。所以采集对象里建议维护一个“订阅清单”,重连后按清单重建,而不是把监控项散落在各个业务代码里。
6.2 交付前用模拟服务器和跟踪日志做验收
长期采集的验证方法,比代码本身更重要。我习惯在交付前做三轮验证。
第一轮,用本地的 OPC UA 模拟服务器,配合这套客户端源码连上,按 100ms 订阅间隔跑 24 小时,检查有没有掉线、有没有间隙性丢点。模拟服务器能自由控制故障,比直接拿真机床做破坏性测试划算得多。第二轮,用真实服务器把两端都跑通,重点验证证书目录和命名空间匹配。第三轮,打开官方库的跟踪日志,把ApplicationConfiguration里的TraceConfiguration指向日志文件,连续跑一晚上再分析日志。日志里会明确记录每一次重新连接、每一处证书验证失败,这就是长期运行的黑匣子,比靠运维盯数据靠谱。
这套代码的最终能力边界,在于它的重连策略和订阅重建机制是否完整。只看“能连上”,任何客户端源码都合格;能连续跑一个月不出问题,才算真正可用。我在给其他人交付这类源码时,最后三条硬性要求分别是:证书目录必须统一、节点 ID 只能来自浏览结果、重连后订阅必须自动恢复。这三条做到了,再接任何 OPC UA 服务器风险都不大。
希望这篇 C# 的 OPC UA 客户端源码落地笔记能帮到你,尤其是那些正准备把西门子机床和 KepServer 两套数据源接到同一个采集程序里的同行。
本文还有配套的精品资源,点击获取