简介:这是一套基于C#的OPC UA客户端源码,目标是解决西门子机床等设备在非标准认证、加密策略与私有数据模型下的连接难题,同时兼容KepServer等常见OPC UA服务器,适合工业通信开发者和自动化集成人员参考。压缩包共2000个文件、约38.89MB,以300余个cs核心源文件为主体,配合xml配置、h/cpp原生依赖、htm接口文档以及sln/csproj工程文件,并附带可运行的SampleClient示例与UA-.NET-Legacy基础框架,便于快速定位、编译和二次移植。已有531人学习。整套源码覆盖会话管理、订阅发布、安全策略等OPC UA关键机制,并通过示例展示从服务器发现、节点读写到订阅通知的完整流程;对于需要适配西门子机床私有命名空间、跨品牌OPC UA服务器并兼顾加密通讯与后期维护的开发者,是一份具备较高工程价值的参考实现。
1. 先聊清楚:为什么“都是OPC UA”还会连不上
先说一个我常被问到的问题:为什么我用OPC UA客户端连KepServer一下子就通,换了西门子的机床就报错,翻来覆去研究半天也不通?很多刚接触C#上位机开发的朋友,以为OPC UA是一套“装好就能通”的协议,但实际上它不是一个“产品”,而是一整套规范。OPC UA全称是OPC Unified Architecture,它把数据访问、历史数据、报警、方法调用这些能力抽象成了统一的服务接口,同时又允许不同厂家在信息模型上自由发挥。打个比方,OPC UA像是大家约定好普通话,但每个厂家的“词典”和“口音”不一样。
所以,一套C# OPC UA客户端源码,如果想要做到“既能连西门子机床的OPC UA服务器,又能连KepServer这类通用网关软件”,本质上不是把代码写得多么花哨,而是要把它写得足够“标准”——协议栈标准、配置灵活、异常处理宽容。这篇文章我就把我实际用过的一套源码结构、踩过的坑、以及为什么这样设计的原因,完整梳理一遍。它不是官方Demo的照搬,而是贴合车间现场、贴合C#上位机开发实际节奏的一套方案,适合正在做机床数据采集、产线设备监控或者MES对接的工程师参考。
这套代码能满足哪些实际需求?连接西门子数控系统(比如Sinumerik 840D sl)的OPC UA服务器,读取主轴转速、进给倍率、当前程序名等实时数据;连接安装了KepServer的工控机,把PLC、仪表、变频器的数据统一送上来;甚至因为OPC UA是跨平台、跨厂商的标准,同一份代码换个配置,就能连上其他支持OPC UA的服务器。后续无论做数据展示、报警推送还是数据库落盘,都在这套基础上叠加。
2. 选型与库的选择:为什么推荐OPCFoundation的UA-.NETStandard
2.1 市面上的C# OPC UA库怎么选
C#体系中做OPC UA客户端,主流选择其实就是两种:一种是OPCFoundation官方的UA-.NETStandard库,另一种是商业库或者国内封装的开源库。我的建议非常直接——优先用OPCFoundation官方库,因为这是OPC基金会自己维护的,协议版本的跟进速度最快,而且底层兼容.NET Framework 4.6.2以上和.NET Core/.NET 5+,也就是说不管是老旧的工控机Windows 7系统还是新的Linux工控机,都能跑。
可能有人问,为什么不用某些“开箱即用”的第三方库?我也试过几个,确实上手快,但做到后面会发现问题很尴尬——有些库对OPC UA的复杂类型(比如结构体、数组、扩展对象)支持不完整,遇到西门子机床返回的特定数据节点,解析不出来;有些库对安全策略支持不全,配置了Basic256Sha256之后,握手总是失败。说实话,做工业数据采集,稳定性比开发速度重要得多,协议的底层你做不了主,那就得选一个真正把协议做到位的库。
2.2 工程结构和依赖
我实际使用的工程结构也很简单,核心就是一个类库项目,再加上一个测试用的WinForm或控制台项目。主工程引用的NuGet包有这几个:
OPCFoundation.NetStandard.Opc.Ua.Core OPCFoundation.NetStandard.Opc.Ua.Client OPCFoundation.NetStandard.Opc.Ua.Configuration OPCFoundation.NetStandard.Opc.Ua.Security.Certificates至少需要Core和Client两个包,自动会带上基础依赖。这里要提醒一下,NuGet包的版本建议保持统一,不要Core用旧版、Client用新版,版本混用会出现一些非常诡异的“对象不支持指定方法”的异常。我一般直接锁定到同一个版本号,比如1.4.372。
除了包引用,项目中还必须有ApplicationCertificate这个环节,因为OPC UA客户端在连接服务器之前,往往需要生成或者加载自己的应用证书。代码里这一步可以自动完成,但目录结构要提前规划好:
Configuration/ Opc.Ua.Client.Config.xml // 客户端全局配置 pki/ own/certs/ // 客户端证书 trusted/ // 信任的服务器证书 rejected/ // 不信任的证书(调试时查看)这个pki目录是OPC UA安全模型里的核心目录,很多“连不上”的问题,最终都可以在rejected目录里找到线索。
2.3 ApplicationConfiguration:所有连接的前置条件
基于UA-.NETStandard的客户端,第一步永远是配置ApplicationConfiguration。很多人在这里随便写写,结果后面报各种证书和端点错误。我建议把配置项拆成三个必填部分:
var config = new ApplicationConfiguration { ApplicationName = "MyOpcUaClient", ApplicationUri = "urn:mycompany:myopcuaclient", ApplicationType = ApplicationType.Client, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.X509Store, StorePath = "CurrentUser\\My", SubjectName = "CN=MyOpcUaClient, C=CN, S=Shanghai" }, TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = "Configuration\\pki\\trusted" }, RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = "Configuration\\pki\\rejected" } } };ApplicationUri这个值很多新手不理解,其实它就是客户端自己的“身份证号”,最好用固定格式,不要每次运行随机生成。服务器的信任列表会记录这个Uri,你改了它,服务器那边可能又要重新信任一次。SecurityConfiguration里指定了证书存储位置:客户端证书放在Windows证书库里,信任的服务器证书放在pki目录里。每当服务器证书变更,代码第一次连接会提示“未信任”,去pki/trusted里把证书拉进来,或者让TrustedPeerCertificates指向一个共享网络目录,这样多台客户端可以共用一份信任列表。
配置完成后调用await config.CertificateValidator.UpdateCertificateAsync()初始化证书验证器,再调用config.CertificateValidator.CertificateValidation事件做日志记录。这一步不是走流程,它能帮你快速定位证书信任问题。
3. 源码的核心骨架:连接、浏览、读写、订阅
3.1 创建会话:连接这一步最考验细节
创建会话是整个流程里最经典的“看似简单实则磨人”的环节。一个最小化的连接代码如下:
var endpoint = new Uri("opc.tcp://192.168.1.100:4840"); var selectedEndpoint = CoreClientUtils.SelectEndpoint(config, endpoint.ToString(), useSecurity: true); var session = await Session.Create( config, selectedEndpoint, updateBeforeConnect: false, checkDomain: false, sessionName: "MySession", sessionTimeout: 60000, identity: new UserIdentity(new AnonymousIdentityToken()), preferredLocales: new[] { "zh-CN", "en-US" } );这里有几个对工业现场特别重要的参数。useSecurity: true表示启用加密和签名,但如果对方是老的服务器、只支持None安全策略,就得把useSecurity改成false,或者用ConfiguredEndpoint手动指定SecurityMode。我一般在配置文件里留一个开关,让现场人员不用改代码就能切换安全模式。
checkDomain: false是很多人的救星。OPC UA的安全模型里,服务器证书的域名或IP必须和连接地址匹配,但车间现场的计算机名经常乱改,或者服务器配置时用了主机名、连接时用IP,这种情况下握手会直接失败。我后面会专门讲这个“计算机名不再与OPC UA配置的计算机名称匹配”的问题,这里先埋个伏笔。
UserIdentity决定身份认证方式。KepServer和西门子机床默认都允许匿名,但如果企业安全策略要求用户名密码,就换成new UserIdentity("user", "password")。还要注意,Session.Create返回的是一个高层次的会话对象,它内部自动维护了通道、安全令牌的续订,不需要自己CloseAsync一个旧的再开新的,这一点比老式的OPC DA舒服得多。
3.2 读取与写入:看清节点ID的本质
连接上之后,读数据用ReadValueAsync:
var nodeId = new NodeId("ns=7;s=Channel1.Device1.Tag1"); var value = await session.ReadValueAsync(nodeId); double result = (double)value.Value;这里最核心的概念就是NodeId。OPC UA不像PLC那样直接用地址(DB100.DBD12),而是用“命名空间+标识”组合出来的节点路径。你看到ns=7,就是命名空间索引为7,s=后面的字符串是服务器内部定义的标识。KepServer的节点通常形如ns=2;s=Channel1.Device1.Tag1,而西门子机床的节点往往是ns=3;s=Sinumerik/...这种层级结构,也有的会是数字ID:
var nodeId = new NodeId("ns=3;i=1002");想搞清楚这些节点ID,最可靠的办法是先用UA Expert或者本库自带的Session.Browse浏览一遍服务器节点树,而不是去猜。这也是我代码里必带浏览功能的原因——车间设备经常换工艺,节点路径对不上时,现场工程师用客户端自带的树形浏览一眼就能找到新路径,根本不用回办公室改代码。
写入的代码也类似:
var writeValue = new WriteValue { NodeId = new NodeId("ns=2;s=Channel1.Device1.WriteTag"), AttributeId = Attributes.Value, Value = new DataValue(new Variant(123)) }; var result = await session.WriteAsync(null, new[] { writeValue });注意写入前要确认这个节点允许写,并且数据类型要和服务器端一致。一个非常常见的坑是:服务器端变量是Int16,你写成Int32,OPC UA虽然会自动转换,但某些严格的服务器会报BadTypeMismatch。所以保险起见,写入之前先读一次拿到原始值类型,再用Convert.ChangeType转成对应类型。
3.3 订阅数据变化:上位机刷新的正确姿势
做上位机界面的人都知道,轮询读几百个变量会导致CPU和网络占用飙升,更科学的方案是订阅。OPC UA的订阅模型核心是两个对象:Subscription和MonitoredItem。订阅创建后,服务器主动推送数据变化,客户端不用一直问。
var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 1000, KeepAliveCount = 10, LifetimeCount = 30 }; await session.AddSubscription(subscription); subscription.Create(); var item = new MonitoredItem { StartNodeId = new NodeId("ns=2;s=Channel1.Device1.Temperature"), AttributeId = Attributes.Value, SamplingInterval = 1000, QueueSize = 10, DiscardOldest = true }; item.Notification += (MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs args) => { var notification = args.NotificationValue as MonitoredItemNotification; var value = notification.Value; // 这里用Invoke回到UI线程更新界面 }; subscription.AddItem(item); subscription.ApplyChanges();PublishingInterval是服务器按这个频率推送数据,SamplingInterval是服务器按这个频率去采样底层设备,两者可以不同。KepServer这些网关软件本质是把背后PLC的数据通过OPC UA映射出来,所以采样间隔一般是100ms到1s之间比较合理,太短了会给PLC带来额外通讯负担,产线设备偶尔会因此报通讯超时。
订阅回调是在后台线程执行的,直接操作WinForm的Label会报跨线程异常,必须用Invoke或者Progress<T>机制转到UI线程。我以前在车间排查一个“界面卡死重启”的问题,最后发现是回调里做了数据库写入,导致订阅线程被阻塞,改成丢到独立Queue里慢慢写就正常了。
4. 同时适配西门子机床和KepServer的差异处理
4.1 端点发现:先把地址和端口逻辑理顺
无论是西门子机床的OPC UA服务器还是KepServer,它们都遵循同一个规则:端点URL基本是opc.tcp://主机名或IP:端口。差别只在默认端口上。
KepServer的默认OPC UA端口一般是49320,西门子的OPC UA服务器通常是4840。但实际车间环境中,客户改端口的情况非常多。所以我的客户端源码里,连接地址做成可配置项,界面上留输入框,配置文件中保存上次连接信息,启动时自动填上。
有些设备支持多点播报(Discovery),也就是在网络上广播自己的存在。C#里可以直接用DiscoveryClient.DiscoverServers来扫描局域网内的服务器列表,然后把列表显示在界面上,点击即可连接。这玩意在现场很好用——你不知道设备的IP和端口时,扫一下就能看到服务器名。
4.2 安全策略与证书信任:两边要求不一样
这是我踩坑最多的地方。KepServer默认为了让用户快速上手,经常直接用“匿名+无加密”的策略,也就是SecurityMode为None。但西门子机床的OPC UA服务器往往出于数据安全考虑,要求使用Basic256Sha256签名加密,并且在第一次连接时校验客户端证书。
针对这种差异,我在创建Session之前,会先从服务器端点的安全策略列表里挑一个最合适的:
foreach (var ep in selectedEndpoint.EndpointDescription.UserIdentityTokens) { Console.WriteLine(ep.TokenType); } foreach (var secPolicy in selectedEndpoint.EndpointDescription.SecurityPolicies) { Console.WriteLine(secPolicy); }如果服务器同时支持多种策略,我优先选择Basic256Sha256,因为它安全性最高。但如果测试下来证书互信一直搞不定,再退回None。工业现场“先连通、再安全”是务实的选择,只要设备在内网隔离的车间环境里,风险是可控的。
证书互信的流程一定不要忽略:首次连接时,客户端会从服务器拉取证书并检查是否在受信任列表里。代码中,如果发现证书不受信任,你可以把它保存到pki/rejected目录并打印提示,然后人工把这个证书移动到trusted目录。还有一种更省事的办法:在CertificateValidator的CertificateValidation事件里直接接受所有证书,但仅限于现场调试用。
4.3 命名空间和节点路径的适配
西门子机床的OPC UA服务器和KepServer在信息模型上的差异,主要体现在命名空间数量和节点层次上。KepServer可以在配置里自定义Channel和Device名称,所以NodeId相对固定,只要KepServer配置不变,节点路径基本稳定。而西门子机床的OPC UA服务器(比如Sinumerik系统里集成的UA Server)会有很深的层次结构,很多数据藏在Objects -> Sinumerik -> ProgramState -> Program这种层级下面,不同系统版本甚至节点路径名称都有小差异。
我的解决思路是:不在代码里写死节点路径,而是给每个采集点维护一个“变量表”,存JSON或者数据库里。变量表字段包含变量名、NodeId、数据类型、读写属性。客户端启动时加载这张表,连接服务器后先做一次节点验证,验证失败的变量做个标记并在界面标红,但不会导致整个连接中断。这样做的好处显而易见——换了一台不同版本的西门子机床,不需要改代码,只需要修正变量表里的NodeId即可。
5. 实测翻车记录:比功能更值钱的排错经验
5.1 证书主机名不匹配的经典故障
标题里有一句“计算机名不再与OPC UA配置的计算机名称匹配”,这是很多人在连西门子设备时遇到过的一个具体报错。报错背后的逻辑是这样的:服务器端配置的证书里写了一份“允许连接的计算机名称列表”,当客户端发起连接的时候,服务器会拿着客户端的机器名去比对,发现对不上就拒绝握手。车间里工程师为了方便,经常把工控机的计算机名从“WIN7-PC”改成“PLC-HMI-01”,但服务器端信任关系还停留在旧名字上,于是就这么卡死了。
我在代码里解决这个问题的方式是:在创建会话时设置checkDomain: false,绕过了域名检查。这个参数名看起来不起眼,却是连西门子机床时最关键的开关。如果客户的安全策略不允许关闭域名检查,就得在西门子服务器的管理界面里把新的计算机名加入白名单,这个操作必须在服务器侧完成,客户端改不动。
5.2 广播找不到服务器,但IP能ping通
另一个高频现场问题:客户端扫描不到OPC UA服务器,但明明IP是通的。多半是UDP广播被防火墙拦了,或者不在同一网段。OPC UA的Discovery用到了UDP 4840端口,而实际连接用的是TCP 4840端口,两个方向都要放行。现场处理办法是telnet一下IP端口看通不通:
telnet 192.168.1.100 4840如果telnet能通,说明TCP没问题。如果扫描不到,重点检查Windows防火墙有没有放行UDP。还有一种情况是服务器所在工控机装了多个网卡,Discovery广播默认从某个非业务网卡发出,客户端就收不到了。实在不行就别用广播了,手动输入端点URL最靠谱。
5.3 读取频率太高,服务器不堪重负
KepServer背后往往会拖带好几台PLC或者仪表设备,OPC UA服务器作为网关本身性能也有限。之前有个项目,我在上位机里写了个遍历所有节点的循环,每100ms读一次全量数据,结果KepServer直接卡死,PLC也开始报通讯故障。后来改成按变量变化频率分开订阅:高速变化的主轴转速用200ms采样间隔,温度压力这类慢变量用1s或者2s,读取线程全部停掉,服务器负载瞬间就降下来了。
还有一个细节,订阅的QueueSize如果设置成1,表示只保留最新值,适合转速这种快速变量。如果要做历史曲线,QueueSize可以设大一点,比如50,这样断网重连后还能补回一小段数据。但注意QueueSize越大内存开销越大,全部变量都设50会吃掉不少内存。
6. 从“连上”到“用好”:这套源码还能扩展什么
6.1 报警与事件采集
OPC UA除了数据读写,还有Alarm&Condition模型。KepServer和西门子设备都支持把报警作为事件节点暴露出来。在订阅基础上,可以监听事件通知:
var eventItem = new MonitoredItem { StartNodeId = new NodeId("ns=2;s=Alarms"), AttributeId = Attributes.EventNotifier, MonitoringMode = MonitoringMode.Reporting };有了事件订阅,上位机就可以实时弹出设备故障报警,而不需要轮询检查状态字。这个功能排在扩展项第一位,因为车间最关心的永远是设备停了没有,为什么会停。
6.2 MQTT转发与数据库落盘
很多系统要求数据既要进界面,又要推到远程平台。OPC UA客户端读到的数据,经过格式转换后丢给MQTT客户端发布出去,或者用定时批处理写进SQL Server/MySQL。这里我的经验是队列解耦——OPC UA订阅回调只负责把数据压入ConcurrentQueue,后台单独线程消费队列写数据库或发MQTT。这样订阅线程永远不会被IO阻塞,数据不会丢,界面也不会卡。
6.3 与视觉、扫码枪等设备联动
在做产线自动化的时候,C#上位机往往还要接海康相机、扫码枪等设备。OPC UA客户端采集的数据可以作为联动条件,比如读到的PLC信号触发相机拍照,扫码枪读到条码后写到OPC UA服务器里某个节点,让PLC知道当前加工的是哪个工件。这种多设备联动的架构里,OPC UA客户端相当于一个“数据中台”,把各种数据统一汇入到一个事件驱动框架里,整体逻辑会清晰很多。
上面这些方向,其实都是建立在本文这套“连接、浏览、读写、订阅”源码基础之上的。基础打好,上层怎么加功能都不会乱,这也正是我做这套源码时最看重的事情——不是堆功能,而是把连接层做得足够扎实、足够宽容。以后再遇到什么国产机床、横河仪表、罗克韦尔设备,只要对方支持OPC UA,用这份代码改改配置就能上,省下来的时间,大可以花在真正有用的业务逻辑上。
本文还有配套的精品资源,点击获取