
简介OPC UA 官方服务器与客户端程序源码包由SIEMENS公司与OPC基金会联合出品内置服务器端与客户端两套完整示例工程适合刚接触OPC UA协议的新手快速建立整体认识也适合有一定经验的开发人员参考官方实现用于工业互联场景下的通信开发与排障。压缩包共含14个文件整体大小约100.84MB其中多个zip为Visual Studio工程源码与配套例程PDF为官方使用手册及API说明EXE、MSI与MSM为工具或SDK安装组件HTM与TXT提供索引、清单等辅助信息类型较全、目录结构清楚可按需取用。目前已有1024人学习下载读者可借此获得服务器/客户端完整源码、开发文档和SDK组件并能直接编译运行示例尤其可结合SIMATIC S7-1500等PLC设备学习OPC UA地址空间、会话管理与数据读写等典型功能为后续二次开发提供扎实参考。1. 把官方这份 OPC UA 服务器/客户端源码当作协议实现来读比当作示例跑更有价值标题里的OPC UA 官方服务器 客户端程序源码.zip它不是那种解压以后点个 exe 就能跑的发布包而是一份把服务器端和客户端同时摊开给你看的源代码。真正让这份压缩包值回时间的地方不在编译通过那一刻而在于当你在产线上被PLC 点表怎么变成 OPC UA 节点客户端断线以后订阅怎么恢复这类问题卡住时能直接从源码里找到答案而不是去论坛里猜。它适合三类人从 Modbus、OPC DA 迁过来的设备工程师写数据采集与上层应用对接的软件工程师以及实验室里要用真实 OPC UA 协议做整套系统验证的在校学生。2. 先读源码骨架再动手编译服务器端、客户端和配置文件的三角关系2.1 服务器端入口Main、Application 与 NodeManager 的调用链这类源码包的服务器端工程长得都差不多一个 Main 函数负责启动一个 ApplicationInstance 负责读配置和证书一个继承自 StandardServer 的类负责装配 NodeManager。我第一次打开这种工程时习惯先找 Main看它调了 LoadApplicationConfiguration再往下看它 Start 了哪个 Server 子类五分钟就能把启动链路走通后面再去读业务代码就不容易迷路。Server 子类里边能看到 CreateServerProperties、OnServerStarted 这些重写入口但真正跟设备数据打交道的是 NodeManager。官方源码包里几乎都会带一个 CustomNodeManager 的子类CreateAddressSpace 方法就是往地址空间里注册对象节点、变量节点、方法节点的位置。你要把 PLC 点表映射成 OPC UA 节点改的就是这一块。// 服务器启动骨架官方 .NET 示例里的通用写法 using Opc.Ua; using Opc.Ua.Server; class MachineServer : StandardServer { protected override MasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { var nodeManagers new ListINodeManager { new MachineNodeManager(server, configuration) // 自定义命名空间管理器 }; return new MasterNodeManager(server, configuration, nodeManagers); } }这段代码的关键是 CreateMasterNodeManager它是服务器端装配地址空间的入口。MachineNodeManager 负责管理你自定义的那部分地址空间如果它没有被返回进 MasterNodeManager你后面注册的任何节点都不会出现在任何 OPC UA 客户端里。注意构造参数里的 server 和 configuration 两个对象它们后面要一路传给节点管理器很多初始化错误都出在这两个对象为 null。2.2 客户端源码的阅读顺序会话、端点与订阅客户端工程打开以后先看它怎么连服务器再看 Session.Create最后看 MonitoredItem。OPC UA 客户端的完整使用流程就是建端点、建会话、建订阅三步每一步失败时断点该下在哪个方法心里先有个数。官方示例的客户端工程里一般会有一个封装好的连接辅助类把 Session 创建、证书校验、重连逻辑都收进去了。我建议先找一套不带封装的裸调用用例来读这样服务端的报错会明确对应到协议的某一层。我自己平时会用 UAExpert 这种 Windows 上的 OPC UA 图形客户端做对照验证它能看到地址空间树、订阅参数和证书状态自己写的客户端能不能连、能不能读、能不能订阅拿它当参照物最省事。提示源码包里那份客户端程序的业务逻辑通常很薄它的主要价值是演示官方 SDK 的标准调用姿势。要应对现场环境你八成得在上面改造。2.3 30 分钟跑起来还原依赖、生成证书、双方握手不要一拿到源码就在 Visual Studio 里按 F5先把 NuGet 还原跑通。OPC UA 的 .NET 官方 SDK 依赖原生 OpenSSL 库缺了它会在启动时直接报找不到本地方法。我一般先还原加编译dotnet restore OPCUAServerAndClient.sln dotnet build OPCUAServerAndClient.sln -c Releasebuild 之前确认本机装了 .NET SDK官方 SDK 现在一般要求 .NET 6 及以上如果是部署到工业主机只装运行时也行。restore 会把 OPCFoundation.NetStandard.Opc.Ua.* 这一组 NuGet 包拉齐如果这几个包版本不一致在这个阶段就能看到版本冲突警告。第一次跑之前先打开服务器工程的配置文件确认端点端口是不是 4840协议是不是 opc.tcp。客户端工程里的连接地址必须和服务器完全一致端口、路径、多余的空格都不能差。注意很多示例用的是 localhost如果你要连的是另一台机器得把主机名换成实际 IP。改完端口和地址以后先启动服务器进程再启动客户端进程。客户端日志里出现 Connected 或者读到了 ServerStatus 节点说明握手成功。如果日志里出现证书相关的报错往下看第 5 章的排查。3. 改服务器端源码把 PLC 点表映射成 OPC UA 节点的三步落地方法3.1 在 CreateAddressSpace 里注册自定义变量节点官方示例的地址空间里有两个命名空间第一个是 OPC UA 内置的第二个是示例自己的。你要把现场数据挂到自定义命名空间里不要在默认命名空间上乱挂点否则后面用 UAExpert 筛数据时一片混乱第三方的 SCADA、MES 系统也找不到你的数据。using Opc.Ua; public class MachineNodeManager : CustomNodeManager { public MachineNodeManager(IServerInternal server, ApplicationConfiguration config) : base(server, config) { // 自定义命名空间 URI对应客户端里看到的 urn:factory:machine NamespaceUris new string[] { urn:factory:machine }; } public override void CreateAddressSpace( IDictionaryNodeId, IListIReference externalReferences) { // 把设备上的一个模拟量映射成 OPC UA 的变量节点 var tempState new BaseDataVariableState(null, new NodeId(MachineTemp, NamespaceIndex)) { DataType DataTypes.Double, ValueRank ValueRanks.Scalar, DisplayName new LocalizedText(MachineTemp), AccessLevel AccessLevels.CurrentRead | AccessLevels.CurrentWrite, UserAccessLevel AccessLevels.CurrentRead | AccessLevels.CurrentWrite }; AddPredefinedNode(null, tempState); } }BaseDataVariableState 是服务器端最常用的变量状态对象地址空间里的每个变量节点都对应一个这样的状态对象。构造函数里的 NodeId 由字符串标识符和 NamespaceIndex 组成NamespaceIndex 是 NamespaceUris 里的索引经过编译期自动换算出来的内置命名空间占了 0自定义命名空间通常从 2 开始。DataTypes.Double 表示这个变量是双精度浮点ValueRank 决定它是标量还是数组。AccessLevel 这一组参数容易看漏。客户端想下写指令必须在 AccessLevel 和 UserAccessLevel 里都加 CurrentWrite只读权限下客户端调用 Write 会拿到 Bad_NotWritable。NodeId 我建议直接用 PLC 点表里的点名当字符串标识符数据从哪来、节点叫什么两边一一对应导出变量清单的时候不用再猜。给一个我常用的映射规则方便你抄PLC 地址OPC UA NodeId数据类型说明DB1.DBD4ns2;sMachineTempDouble主轴温度模拟量DB1.DBX0.0ns2;sStartButtonBoolean启动按钮开关量DB1.DBD8ns2;sFeedSpeedDouble进给速度带死区过滤3.2 让变量值活起来轮询 PLC、死区判断与变更通知状态对象只是一块内存值不会自己变。常规做法是服务器程序里开一个后台任务周期去读 PLC 的寄存器或接口把读到的值写进状态对象的 Value 属性再触发变更通知。这个触发机制是 OPC UA 订阅模型的核心也是新手最容易卡住的地方。using Opc.Ua; private BaseDataVariableState _machineTemp; public async Task UpdateLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { double raw ReadPlcRegister(DB1.DBD4); // 读 PLC 的 32 位浮点 double current _machineTemp.ValueAsdouble(); // 当前服务器缓存值 if (Math.Abs(raw - current) 0.1) // 死区 0.1过滤电气噪声 { _machineTemp.Value raw; _machineTemp.ClearChangeMasks(_server, true); // 通知订阅引擎值变了 } await Task.Delay(500, ct); // 500ms 轮询一次 } }写进 Property 本身不会立刻把变化推给客户端。订阅引擎在下一个发布周期会去比对地址空间的监控项值变了才把数据变更通知打包进响应。ClearChangeMasks 是触发订阅通知的关键调用不加这句客户端那边永远看不到这个节点的数据。死区判断这块是血泪经验。连续变化的模拟量如果不加过滤每个轮询周期都会触发一次通知订阅队列写满、存储打爆、CPU 拉高。轮询间隔 500ms、死区取量程的 0.1%是现场调优比较稳的起点信号波动大的地方再把死区放大到 0.5%。另外要注意模拟量在通信层做一次滤波很常见但滤波和协议死区是两个东西别把它们混在同一个环节里做否则调试时很难判断是哪一层把数据吞了。3.3 方法节点让客户端下命令而不是写寄存器设备交互不只是读写寄存器还要启停、复位、切换配方。这类动作建议在地址空间里暴露成 Method 节点而不是让客户端去写一个命令寄存器。OPC UA 方法调用有请求-响应语义客户端调一次就知道服务器到底执行成功没有。var resetMethod new MethodState(null, new NodeId(ResetMachine, NamespaceIndex)) { DisplayName new LocalizedText(ResetMachine) }; resetMethod.OnCall async (context, method, inputArgs) { bool ok await PlcResetAsync(); // 实际下发的 PLC 复位命令 return new object[] { ok }; // 返回值映射到方法输出参数 }; AddPredefinedNode(null, resetMethod);OnCall 就是方法被客户端调用时服务器端执行的委托inputArgs 对应方法定义的输入参数返回的 object[] 结果一一映射到方法输出参数。这样客户端拿到 MethodId 以后直接调用 session.Call整个动作就是一次标准方法调用成功与否通过状态码回传不会出现变量写进去了但设备没反应的暧昧状态。SINUMERIK 这类数控系统的 OPC UA 接口就是这么暴露的它的客户端 SDK 也是按标准 OPC UA 方法调用来访问 NC 变量。设备侧会说话的不是只有西门子但凡是走 OPC UA 的控制器地址空间里无非就是变量节点加方法节点这两样东西看懂一套就通了大半。4. 改客户端源码会话、订阅、断线重连三块核心逻辑怎么改4.1 Read 与 Subscription轮询和订阅怎么选官方客户端示例里最基础的读法是一次性读几个节点适合画面初始化、设备点检这类只取一次的场景。只要涉及趋势曲线、状态变化监控、报警离散采样必须切到订阅。订阅是服务器主动推客户端不用守着采样周期省带宽延迟也低代码里两种调用方式都有看懂对比才能选对。取数方式网络开销数据实时性典型用途Read每次一条请求取决于调用频率画面初始化、变量表导出ReadMultiple一次请求多个节点取决于调用频率点检、批量导入Subscription/MonitoredItem只在变化或周期推送毫秒级曲线、报警、状态变化把示例客户端分开验证的思路也适用生产项目读操作不建订阅点检逻辑只订阅长周期稳定信号用更大发布间隔。别把全部节点塞进同一个订阅里后面 CPU 和网络一紧张排查时不好分清是谁在拖后腿。4.2 订阅参数设置SamplingInterval、PublishingInterval 与队列这组参数是官方示例里最容易绕晕的地方。SamplingInterval 是服务器检查变量有无变化的频率PublishingInterval 是服务器把一批变化打包发给客户端的周期QueueSize 是每个监控项在服务器端暂存的通知条数。新手上路照抄默认值通常没问题但现场性能问题几乎都出在这几个参数的组合上。using Opc.Ua; // 订阅对象发布周期 1000ms即服务端每 1 秒向客户端推一次数据 var subscription new Subscription(session, 1000); session.AddSubscription(subscription); var monitored new MonitoredItem(subscription, nodeId, AttributeId.Value) { SamplingInterval 200, // 服务器每 200ms 采样一次该变量 QueueSize 5, // 客户端暂未消费时最多缓冲 5 条 Filter new DataChangeFilter { Trigger DataChangeTrigger.StatusValue, Deadband 0.2 // 绝对值死区过滤高频抖动 } }; monitored.Notification OnDataChanged; subscription.AddItem(monitored);MonitoredItem 挂到 subscription 上才真正在服务器端登记。客户端不在线时服务器会按 QueueSize 暂存通知恢复后再补推队列设为 0 表示无限队列但内存会跟着涨一般不建议。Notification 事件里拿到的是 MonitoredItemNotification 对象里面带值、状态码和时间戳解析时这三个字段都要处理别只看值。参数上PublishingInterval 别照抄 1000ms先想清楚业务需要多快看到数据。现场数据源本身就是秒级更新那发布周期设 500ms 纯粹是烧 CPU报警要求 200ms 内上画面再往 100ms 压同时把死区打开让无意义的变化死在服务器端。4.3 断线重连与订阅恢复官方源码里的经典方案网络一抖客户端和服务器的会话就断了。官方客户端示例的重连逻辑一般是客户端在 KeepAlive 长时间收不到响应时判定连接不可用创建 SessionReconnectHandler 去重连。重连成功以后 Session 对象还在但订阅和监控项往往已经失效必须在重连回调里重建。如果不重建你会看到连接没断但永远收不到新数据的诡异情况。using Opc.Ua; using Opc.Ua.Client; private void StartReconnect() { var reconnectHandler new SessionReconnectHandler(); reconnectHandler.BeginReconnect(_session, 15000, out _); // 15 秒内尝试重连 reconnectHandler.Status (s, e) { if (e.Status SessionReconnectHandler.ReconnectStatus.Reconnected) { _ Task.Run(RebuildSubscription); // 重连成功后重建订阅 } }; } private void RebuildSubscription() { _subscription new Subscription(_session, 1000); _session.AddSubscription(_subscription); foreach (var nodeId in _monitorNodeList) { // 重新登记每一个监控项 var item new MonitoredItem(_subscription, nodeId, AttributeId.Value) { SamplingInterval 200, QueueSize 5 }; item.Notification OnDataChanged; _subscription.AddItem(item); } _subscription.ApplyChanges(); // 提交本会话内的订阅修改 }BeginReconnect 的第二个参数是重连超时毫秒数重连状态通过 Status 事件回调。必须判断 Reconnected 而不是 Started否则会在握手刚开始时就抢建订阅。ApplyChanges 是提交会话内所有订阅修改的方法官方客户端也建议对重建操作加锁避免在 Publish 回调过程中改对象导致状态错乱。重连超时时间不能拍脑袋设太短。工业控制器或 OPC UA 网关重启通常要 2 到 5 秒如果超时设 3 秒每次都来不及完成握手就宣告失败程序陷入重连-失败-再重连的循环。我给现场一般设 15 到 30 秒同时把会话超时拉长到 60 秒让服务器端保留会话等待恢复的时间足够长重连以后原会话还在订阅重建的负担就小了。5. 避坑指南官方源码跑不起来或连不稳的 5 个排查现场5.1 现象UAExpert 能连上自写客户端连不上第一次启动服务器时会生成自签证书UAExpert 连接会弹个信任对话框你把证书导入受信任列表就通了。但你自己的客户端程序往往没有这个交互连接时直接报 Bad_SecurityChecksFailed 或 Bad_CertificateUntrusted。原因就是客户端证书链没进服务器的信任存储。解决分两步测试环境图省事可以在服务器启动后手动把客户端证书导入信任列表正式环境把客户端证书导出成 DER 文件加入服务器的受信任证书目录然后在服务端配置里指定 CertificateValidator 去加载。注意别把自签证书生成到临时目录否则服务器每次重启都生成新证书信任关系就断了。5.2 现象断线重连成功订阅数据一直为空很多人重连代码写得没问题KeepAlive 也恢复但信号就是不来。原因在协议层面OPC UA 会话恢复只能还原会话对象不能保证地址空间里的 MonitoredItem 还活着。服务器端在会话中断期间可能已经清理了订阅资源尤其是订阅空闲超时被释放的情况。解决方法是按 4.3 的代码在 Reconnected 回调里重建订阅和监控项。这套重建逻辑要写成可重入函数每次重连都执行宁可变量列表重复注册也不要漏订阅。我看到不少现场是把重建代码写在别的地方只在首次启动执行一次断线以后自然就空了。5.3 现象发布间隔设到 100ms服务器 CPU 被拉满典型的翻车现场把服务器发布周期调到 100ms 期望更快看到数据结果客户端没快多少服务器 CPU 却先满了。原因是 OPC UA 的 Publish 请求本身就是长轮询100ms 发布意味着每秒 10 次打包、传输、解包同时采样周期 200ms 产生的变化全都要排队进入发布周期发布间隔太短反而浪费。解决方式是把发布周期设成采样周期的 2 到 5 倍让每个包能包含更多变化点减少网络来回次数。先量出变量实际的变化频率再定这两个参数别做拍脑袋优化。如果现场要求毫秒级报警优先在 DataChangeFilter 里设置死区过滤而不是把发布周期往死里压。5.4 现象服务器日志报证书时间错误握手被拒OPC UA 证书里带有效区间服务器与客户端系统时钟差几个小时以上握手会被证书时间校验直接拒绝。这个问题在虚拟机和容器环境里特别隐蔽代码完全没动换个部署环境就接不上。解决方式是把两侧系统时间对准再启动服务。工业内网一般都有时间服务器兜底跨局域网的边缘节点也要保证时间同步机器重启后再校验一次。证书明明在有效期内但握手失败十有八九就是时钟偏差用 UAExpert 连一下看报错信息就能确认。5.5 现象编译期 SDK 版本冲突调用方法对不上官方源码包的依赖如果升级不彻底编译阶段会在 Session.Create 或 MonitoredItem 构造函数上报方法不存在或类型不匹配。OPC UA .NET SDK 的几个程序集是共用一个版本号的只升了 Client 没升 Server或者反过来就会出现这种问题。解决方式是先把 OPCFoundation.NetStandard.Opc.Ua 相关包全部回退到同一个基线版本再整体升到同一新版本不要一个一个零散升级。用 dotnet list package --outdated 看一下依赖树升完以后先跑官方测试客户端验证一遍基础读写再去跑自己的业务逻辑。注意避坑第一条里提到的证书信任在 Linux 部署时尤其容易踩。Windows 下弹框点确定就行Linux 下没有弹框得自己把证书放进信任目录并确保目录权限正确否则服务器日志里会反复出现证书加载失败。6. 进阶验证用官方客户端源码反测服务器端的三种压测手法代码跑通以后用 UAExpert 手工点点看不出压力。真正把服务器推向极限的是拿这份客户端源码写三个小工具并发压力、订阅风暴、异常报文。并发压力就是开十几个 Session 同时握手观察服务器的响应时间和最大会话数官方服务器默认支持上百个会话但数据层往往先扛不住。订阅风暴是让一个 Session 建几十个 MonitoredItem订阅同一个高频变化节点观察发布周期和队列是否溢出。异常报文最敏感构造一个不存在的 NodeId 发起 Read或者用错误的安全策略尝试激活会话正常的服务器应该在 500ms 内返回 Bad_NodeIdUnknown 或 Bad_SecurityPolicyRejected如果迟迟无响应多半是服务器端有阻塞操作占了线程。用客户端源码做压测有个好处它跟你部署的服务器用同一套 SDK问题定位清晰不会被第三方工具自身干扰。我见过有人把服务器测成 100% CPU最后定位到是一个自定义 NodeManager 在发布周期里每次都去读 PLC 寄存器而不是只在变化时读。把死区逻辑加在服务器端发布周期回到 500msCPU 立刻下来客户端数据曲线依然平滑。压测时记录三个数值PublishResponse 的往返时间、每个监控项的通知产生频率、服务器线程池占用。官方示例客户端里有一个 TimeSinceLastKeepAlive 指标是判断会话健康的快速手段我的习惯是把它加进告警持续超过 5 秒就发一条警告。这套重构的坑是我在边缘网关上调发布间隔时踩出来的把参数改到极端值、又不看守护进程资源翻车是迟早的事。先统一时间再谈发布频率先恢复订阅再谈队列深度顺序反了问题会像洋葱一样一层套一层。希望这些经验帮到你。本文还有配套的精品资源点击获取