公司动态
基于C#与.NET Core的OPC UA 1.03客户端开发实战指南
简介在工业自动化与物联网领域数据采集是连接物理设备与信息系统的关键技术。其核心原理在于通过标准化的通信协议实现设备数据的可靠、安全读取与传输。OPC UA作为一种跨平台、支持语义信息模型的工业通信标准其技术价值在于解决了传统OPC DA在安全性、跨平台性等方面的局限为构建统一的数据底座提供了基础。在应用场景上它广泛服务于制造执行系统、设备监控和工业互联网平台的数据集成。本文聚焦于使用C#语言和.NET Core框架开发一个兼容OPC UA 1.03规范的客户端详细阐述了从连接管理、数据订阅到跨平台部署的完整工程实践为开发者实现高性能、易集成的工业数据采集方案提供参考。1. 项目概述从OPC DA到OPC UA的工业数据采集演进在工业自动化领域数据是驱动一切决策和优化的血液。十几年前当我们谈论车间设备数据上云、上系统时绕不开一个老牌协议OPC DA。它基于微软的COM/DCOM技术在Windows平台上确实风光过一阵子但跨平台、跨防火墙、安全性等问题让它成了IT和OT部门之间永恒的“扯皮”点。我记得早年做MES制造执行系统集成时为了配置一个DCOM能在客户现场和他们的IT人员耗上整整两天各种权限、安全策略、端口调试起来极其痛苦。后来OPC基金会推出了OPC UA它就像一场及时雨彻底改变了游戏规则。它不再依赖特定的操作系统内置了完善的安全模型采用面向服务的架构并且最关键的是它定义了一套统一的信息模型框架让数据不仅“能读到”还能“读懂”——也就是带上了语义信息。我手头这个项目标题“opc_C#OPCUA_.netcore_opcua客户端_覆盖1.03版_OPCUA”虽然看起来像是一串关键词的堆砌但它精准地指向了一个非常具体且实用的技术组合使用C#语言基于.NET Core平台开发一个兼容OPC UA 1.03版本的客户端程序。这里的“覆盖1.03版”是关键它意味着这个客户端需要实现OPC UA Part 4: Services 1.03规范中定义的核心服务集。为什么是1.03因为这是目前工业界应用最广泛、最稳定的一个版本后续的1.04、1.05主要增加了一些边缘场景的服务对于绝大多数数据采集和监控场景1.03已经完全够用且生态支持最成熟。这个项目的核心价值就是为开发者提供一个跨平台、高性能、易集成的OPC UA客户端工具或库。无论是想从一台西门子S7-1500 PLC、一台发那科机器人控制器还是一个第三方的OPC UA服务器如KEPServerEX中读取温度、压力、设备状态还是向它们写入控制参数都可以通过这个客户端轻松实现。它尤其适合那些正在将传统Windows桌面应用迁移到Linux服务器或Docker容器中的团队也适合开发新一代基于微服务架构的工业互联网平台。2. 技术选型与架构设计思路2.1 为什么是C# .NET Core在工业软件领域C#一直占据着重要地位这得益于其与Windows平台和传统OPC DA基于COM的历史渊源。但选择.NET Core而非传统的.NET Framework是本次项目架构的基石主要基于以下几点考量真正的跨平台能力.NET Core可以运行在Windows、Linux、macOS上。这意味着你的数据采集客户端可以部署在成本更低的Linux服务器上或者打包成Docker镜像在云原生环境中弹性伸缩。这是传统.NET Framework无法做到的。性能与现代化.NET Core的运行时和垃圾回收器经过了深度优化在高并发IO场景如同时连接多个OPC UA服务器、订阅大量数据点下性能通常优于.NET Framework。同时它对异步编程async/await的支持更为彻底和高效这对于需要处理大量网络通信的客户端至关重要。未来的可持续性.NET Core是.NET 5/6/7/8的基石代表了微软.NET技术的未来方向。基于它进行开发意味着你的代码库能平滑地迁移到最新的.NET版本享受最新的语言特性和运行时优化。2.2 OPC UA客户端库的选择在C#中实现OPC UA客户端我们通常不会从零开始实现复杂的二进制编码、安全通道、服务请求而是选择一个成熟的第三方库。主流选择有两个OPC Foundation官方库这是最权威、最标准的实现。它完全遵循OPC UA规范功能最全但相应地API相对底层学习曲线较陡且文档以规范为主新手上手需要更多时间。第三方开源库如OPC-UA-Client社区中有一些基于官方库封装或独立实现的库它们往往提供了更友好、更简洁的API。例如有些库用起来就像调用一个普通的RESTful API客户端一样简单。在本项目中我选择基于OPC Foundation的官方.NET Standard库进行构建。原因如下可靠性与兼容性官方库保证了与任何符合规范的OPC UA服务器的最大兼容性尤其是在处理复杂数据类型、历史访问、事件等高级功能时风险最低。功能完整性要“覆盖1.03版”官方库是功能覆盖最全的。长期支持有OPC基金会背书维护和更新更有保障。注意官方库的NuGet包名称通常是Opc.Ua.Client和Opc.Ua.Core。你需要确保引用的是支持.NET Standard或.NET Core的版本。2.3 客户端架构设计一个健壮的OPC UA客户端不应只是一个简单的连接、读值、断开的脚本。它需要考虑到异常处理、重连机制、会话管理、订阅性能等。我设计的核心架构分层如下通信层基于官方库的Session对象负责与服务器建立安全通道、创建会话。这一层需要封装连接的生命周期管理包括自动重连、心跳维持。数据模型层OPC UA的核心是信息模型。这一层需要定义如何浏览服务器地址空间、解析节点属性如NodeId,DisplayName,DataType。一个好的做法是设计一个Node实体类将OPC UA的节点信息转化为业务中易于理解的对象。服务层这是业务逻辑的核心。提供Read、Write、Browse、Subscribe等原子操作的方法。Subscribe订阅是高频数据采集的关键它需要高效地管理MonitoredItem监控项和数据处理回调。应用层/配置层提供灵活的配置方式如JSON配置文件定义要连接的服务器端点、要订阅的节点列表、安全策略等。这一层使客户端能够适应不同的部署环境。3. 核心功能实现与代码解析3.1 项目初始化与依赖配置首先创建一个新的.NET Core控制台应用或类库项目。使用NuGet包管理器控制台安装必要的包Install-Package Opc.Ua.Client Install-Package Opc.Ua.Core Install-Package Microsoft.Extensions.Configuration.Json Install-Package Microsoft.Extensions.DependencyInjection Install-Package Microsoft.Extensions.Logging.Console使用Microsoft.Extensions系列包是为了引入依赖注入和配置管理这是构建可维护、可测试的现代.NET应用的最佳实践。创建一个appsettings.json文件来管理配置{ OpcUaClient: { ServerUrl: opc.tcp://localhost:4840, SecurityPolicy: Basic256Sha256, MessageSecurityMode: SignAndEncrypt, Username: , Password: , NodesToSubscribe: [ ns2;sMachine/ProductionRate, ns2;sMachine/Temperature, ns2;sLine/Status ], PublishingInterval: 1000, SamplingInterval: 500 } }3.2 建立安全连接与会话管理连接OPC UA服务器不仅仅是TCP握手还需要协商安全策略、交换证书、创建用户身份令牌。以下是核心连接代码的封装using Opc.Ua; using Opc.Ua.Client; public class OpcUaSessionManager { private ApplicationConfiguration _appConfig; private Session _session; private readonly ILoggerOpcUaSessionManager _logger; private readonly OpcUaSettings _settings; public OpcUaSessionManager(ILoggerOpcUaSessionManager logger, OpcUaSettings settings) { _logger logger; _settings settings; InitializeApplicationConfiguration(); } private void InitializeApplicationConfiguration() { _appConfig new ApplicationConfiguration { ApplicationName MyOpcUaClient, ApplicationType ApplicationType.Client, SecurityConfiguration new SecurityConfiguration { ApplicationCertificate new CertificateIdentifier { StoreType Directory, StorePath %CommonApplicationData%/OPC Foundation/pki/own }, TrustedPeerCertificates new CertificateTrustList { StoreType Directory, StorePath %CommonApplicationData%/OPC Foundation/pki/trusted }, RejectedCertificateStore new CertificateTrustList { StoreType Directory, StorePath %CommonApplicationData%/OPC Foundation/pki/rejected }, AutoAcceptUntrustedCertificates true // 【注意】生产环境应设为false并实现证书验证逻辑 }, TransportConfigurations new TransportConfigurationCollection(), ClientConfiguration new ClientConfiguration { DefaultSessionTimeout 60000 } }; _appConfig.Validate(ApplicationType.Client).Wait(); } public async TaskSession ConnectAsync() { if (_session ! null _session.Connected) { return _session; } var endpointDescription CoreClientUtils.SelectEndpoint(_settings.ServerUrl, useSecurity: true); var endpointConfiguration EndpointConfiguration.Create(_appConfig); var endpoint new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); UserIdentity userIdentity UserIdentity.Anonymous; if (!string.IsNullOrEmpty(_settings.Username)) { userIdentity new UserIdentity(_settings.Username, _settings.Password); } _session await Session.Create( _appConfig, endpoint, updateBeforeConnect: true, checkDomain: false, _settings.ApplicationName, 60000, userIdentity, preferredLocales: null ).ConfigureAwait(false); _session.KeepAlive OnKeepAlive; // 订阅保活事件处理连接中断 _logger.LogInformation($成功连接到OPC UA服务器: {_settings.ServerUrl}, 会话ID: {_session.SessionId}); return _session; } private void OnKeepAlive(Session session, KeepAliveEventArgs e) { if (e.Status ! null ServiceResult.IsNotGood(e.Status)) { _logger.LogWarning($服务器连接状态不佳: {e.Status}); // 这里可以触发重连逻辑 } } }实操心得AutoAcceptUntrustedCertificates在生产环境中必须设置为false。正确的做法是实现CertificateValidation事件根据你的安全策略如对比证书指纹来决定是否接受证书。为了方便开发和测试可以先设为true但上线前一定要改回来并完善证书管理。3.3 实现数据读取与订阅覆盖1.03核心服务OPC UA 1.03规范定义了诸如Read, Write, Browse, CreateSubscription, CreateMonitoredItems等服务。我们的客户端需要实现其中最常用的几个。3.3.1 单次读取与批量读取public class DataAccessService { private readonly Session _session; public DataAccessService(Session session) { _session session; } // 读取单个节点的值 public async TaskDataValue ReadNodeAsync(string nodeIdString) { var nodeId new NodeId(nodeIdString); ReadValueIdCollection nodesToRead new ReadValueIdCollection { new ReadValueId { NodeId nodeId, AttributeId Attributes.Value } }; var response await _session.ReadAsync( requestHeader: null, maxAge: 0, timestampsToReturn: TimestampsToReturn.Both, nodesToRead: nodesToRead, cancellationToken: CancellationToken.None ).ConfigureAwait(false); var result response.Results[0]; if (StatusCode.IsGood(result.StatusCode)) { return result; } else { throw new Exception($读取节点 {nodeIdString} 失败状态码: {result.StatusCode}); } } // 批量读取多个节点效率更高 public async TaskListDataValue ReadNodesAsync(Liststring nodeIdStrings) { var nodesToRead new ReadValueIdCollection(); foreach (var id in nodeIdStrings) { nodesToRead.Add(new ReadValueId { NodeId new NodeId(id), AttributeId Attributes.Value }); } var response await _session.ReadAsync(null, 0, TimestampsToReturn.Both, nodesToRead, CancellationToken.None); return response.Results.ToList(); } }3.3.2 创建订阅与监控项实时数据流的关键订阅/发布模式是OPC UA实现高效实时数据推送的核心。服务器按固定周期PublishingInterval将多个监控项的数据打包成“通知”发送给客户端远比客户端轮询高效。public class SubscriptionService { private Session _session; private Subscription _subscription; private readonly ILoggerSubscriptionService _logger; private Dictionarystring, ActionMonitoredItem, MonitoredItemNotificationEventArgs _callbacks; public SubscriptionService(Session session, ILoggerSubscriptionService logger) { _session session; _logger logger; _callbacks new Dictionarystring, ActionMonitoredItem, MonitoredItemNotificationEventArgs(); } public async Task CreateSubscriptionAsync(int publishingInterval) { _subscription new Subscription(_session.DefaultSubscription) { PublishingInterval publishingInterval, KeepAliveCount 10, LifetimeCount 30, MaxNotificationsPerPublish 1000, PublishingEnabled true, Priority 100 }; _session.AddSubscription(_subscription); await _subscription.CreateAsync().ConfigureAwait(false); _logger.LogInformation($创建订阅成功发布间隔: {publishingInterval}ms); } public async Task AddMonitoredItemAsync(string nodeIdString, int samplingInterval, ActionMonitoredItem, MonitoredItemNotificationEventArgs callback) { if (_subscription null) throw new InvalidOperationException(请先创建订阅); var monitoredItem new MonitoredItem(_subscription.DefaultItem) { StartNodeId new NodeId(nodeIdString), AttributeId Attributes.Value, SamplingInterval samplingInterval, QueueSize 10, // 缓存的通知数量 DiscardOldest true, DisplayName nodeIdString }; monitoredItem.Notification (item, args) callback?.Invoke(item, args); _callbacks[nodeIdString] callback; _subscription.AddItem(monitoredItem); await _subscription.ApplyChangesAsync().ConfigureAwait(false); _logger.LogInformation($已添加监控项: {nodeIdString}, 采样间隔: {samplingInterval}ms); } // 处理通知数据 private void OnNotification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) { foreach (var value in monitoredItem.DequeueValues()) { _logger.LogDebug($节点[{monitoredItem.DisplayName}] 新值: {value.Value}, 时间戳: {value.SourceTimestamp}, 状态: {value.StatusCode}); // 这里可以将数据推送到消息队列如RabbitMQ、数据库或前端 } } }核心参数解析PublishingInterval订阅的发布间隔。服务器会尝试按此间隔发送数据通知包。这是客户端期望的“数据更新频率”。SamplingInterval监控项的采样间隔。这是服务器端对某个变量进行采样的频率。它必须小于等于PublishingInterval。如果设置为0则表示“尽可能快”或使用服务器默认值。QueueSize当客户端处理速度跟不上时服务器端为每个监控项缓存的通知数量。超过此数量后根据DiscardOldest决定是丢弃最老的还是最新的。KeepAliveCount在没有数据通知的情况下服务器发送“保活”通知的间隔以发布周期为单位。用于检测连接是否存活。3.4 浏览地址空间与节点发现一个优秀的客户端不仅要知道具体节点的ID还应能动态发现服务器提供的数据。浏览服务Browse就是用来探索服务器地址空间的。public async TaskListReferenceDescription BrowseNodeAsync(string nodeIdString null) { NodeId startingNodeId Objects.RootFolder; // 默认从根节点开始 if (!string.IsNullOrEmpty(nodeIdString)) { startingNodeId new NodeId(nodeIdString); } var browseDescription new BrowseDescription { NodeId startingNodeId, BrowseDirection BrowseDirection.Forward, ReferenceTypeId ReferenceTypeIds.HierarchicalReferences, // 浏览组织层次引用 IncludeSubtypes true, NodeClassMask (uint)(NodeClass.Object | NodeClass.Variable), // 只查看对象和变量 ResultMask (uint)BrowseResultMask.All }; BrowseDescriptionCollection nodesToBrowse new BrowseDescriptionCollection { browseDescription }; var response await _session.BrowseAsync(null, null, 0, nodesToBrowse, CancellationToken.None); var references new ListReferenceDescription(); foreach (var result in response.Results) { if (result.References ! null) { references.AddRange(result.References); } // 处理 continuation point 以浏览更多结果当结果被截断时 while (result.ContinuationPoint ! null result.ContinuationPoint.Length 0) { var nextResponse await _session.BrowseNextAsync(null, false, new ByteStringCollection { result.ContinuationPoint }, CancellationToken.None); result.ContinuationPoint null; if (nextResponse.Results[0].References ! null) { references.AddRange(nextResponse.Results[0].References); } } } return references; }这个方法返回的ReferenceDescription对象包含了子节点的NodeId、BrowseName、DisplayName和NodeClass客户端可以据此构建一个树形视图让用户像浏览文件夹一样浏览PLC的数据块。4. 高级特性与性能优化实战4.1 处理复杂数据类型与自定义结构OPC UA的强大之处在于它能传输复杂的数据结构而不仅仅是基本类型Int32, Double, String。例如一个“机器状态”可能是一个包含Running布尔、Speed浮点、FaultCode整型的结构体。服务器在地址空间中会定义一个DataType节点对应一个Structure和一个Variable节点值为此结构类型。客户端要读取这样的变量需要知道如何解码。通常有两种方式服务器提供数据字典DataTypeDictionary这是最规范的方式。客户端可以动态加载数据字典通常是XML或二进制格式然后使用官方库的EncodeableFactory系统来注册解码器。客户端预定义类型如果结构是已知的可以在客户端用C#定义一个类继承自EncodeableObject并实现编码解码方法然后将其注册到EncodeableFactory.GlobalFactory。// 示例定义一个简单的“机器状态”结构 [DataTypeId(nsuhttp://yournamespace/;i1001)] // 这个ID必须与服务器端定义的类型NodeId一致 [BinaryEncodingId(nsuhttp://yournamespace/;i1002)] public class MachineStatus : EncodeableObject { public bool Running { get; set; } public double Speed { get; set; } public int FaultCode { get; set; } public override void Encode(IEncoder encoder) { encoder.WriteBoolean(Running, Running); encoder.WriteDouble(Speed, Speed); encoder.WriteInt32(FaultCode, FaultCode); } public override void Decode(IDecoder decoder) { Running decoder.ReadBoolean(Running); Speed decoder.ReadDouble(Speed); FaultCode decoder.ReadInt32(FaultCode); } } // 在程序启动时注册这个类型 EncodeableFactory.GlobalFactory.AddEncodeableType(typeof(MachineStatus).AssemblyQualifiedName, typeof(MachineStatus));注册后当你读取一个数据类型为这个自定义结构的变量时DataValue的Value属性就会自动被反序列化为一个MachineStatus对象。4.2 连接池与多服务器管理在工业边缘网关场景中一个客户端可能需要同时连接几十台甚至上百台设备OPC UA服务器。为每个连接创建一个独立的Session和ApplicationConfiguration实例会消耗大量资源。这时需要引入连接池的概念。会话复用对于同一个服务器避免频繁创建和销毁会话。使用一个Dictionarystring, Session来管理活跃会话并实现会话的保活和失效重连。异步与并发所有OPC UA服务调用Read, Write, Browse都应该是异步的async/await以避免阻塞线程。使用SemaphoreSlim等工具控制对同一个会话的并发访问防止请求混乱。资源隔离每个服务器的会话、订阅应相互独立避免一个服务器的故障如网络中断影响其他服务器的数据采集。配置化驱动将每个服务器的连接参数URL、安全策略、订阅节点列表存储在外部配置如数据库中客户端启动时动态加载并建立连接。4.3 数据持久化与下游集成采集到的数据需要落地或转发。常见的模式有写入时序数据库如InfluxDB、TimescaleDB。这类数据库为时间序列数据做了大量优化非常适合存储设备产生的带时间戳的过程数据。发送到消息队列如RabbitMQ、Kafka、MQTT Broker。将数据作为事件发布供其他微服务如数据分析、报警、大屏展示消费实现系统解耦。缓存与聚合在内存中使用ConcurrentDictionary或MemoryCache缓存最新值并提供REST API供其他系统实时查询。也可以做一些简单的边缘计算比如计算5分钟内的平均值、最大值再上报。// 示例在订阅回调中将数据发送到InfluxDB monitoredItem.Notification async (item, args) { foreach (var value in item.DequeueValues()) { var point PointData.Measurement(machine_data) .Tag(node_id, item.DisplayName) .Field(value, Convert.ToDouble(value.Value)) .Timestamp(value.SourceTimestamp.ToUniversalTime(), WritePrecision.Ns); using (var writeApi _influxDBClient.GetWriteApi()) { writeApi.WritePoint(my-bucket, my-org, point); } } };5. 部署、监控与故障排查实录5.1 跨平台部署实践将.NET Core应用部署到Linux服务器如Ubuntu已成为常态。发布使用dotnet publish -c Release -r linux-x64 --self-contained false命令发布。--self-contained false可以减小发布包体积因为目标机器上已安装.NET运行时。作为服务运行在Linux上使用systemd将客户端配置为后台服务实现开机自启和故障重启。创建服务文件/etc/systemd/system/my-opcua-client.service[Unit] DescriptionMy OPC UA Data Collection Client Afternetwork.target [Service] Typeexec WorkingDirectory/opt/my-opcua-client ExecStart/usr/bin/dotnet /opt/my-opcua-client/MyOpcUaClient.dll Restartalways RestartSec10 Useropcclient EnvironmentASPNETCORE_ENVIRONMENTProduction EnvironmentDOTNET_PRINT_TELEMETRY_MESSAGEfalse [Install] WantedBymulti-user.target容器化部署编写Dockerfile构建镜像。这便于在Kubernetes集群中进行编排和管理实现高可用和弹性伸缩。5.2 客户端监控与日志一个运行在生产环境的数据采集客户端必须具备可观测性。结构化日志使用Serilog或NLog将日志输出到控制台、文件并集成到如Elasticsearch Kibana的日志平台。日志中必须包含会话ID、节点ID、操作类型、状态码、耗时等关键信息。健康检查暴露一个HTTP健康检查端点如/health检查与所有配置服务器的会话连接状态。如果某个服务器连接失败健康状态应标记为Degraded。指标Metrics使用Prometheus客户端库暴露指标如opcua_connection_total连接数、opcua_read_requests_total读请求数、opcua_subscription_items监控项数量、opcua_notification_latency_seconds数据通知延迟。通过Grafana进行可视化监控。5.3 常见问题排查手册以下是我在多年实践中总结的“踩坑”记录问题现象可能原因排查步骤与解决方案连接失败报“证书验证失败”或“安全通道创建失败”1. 客户端/服务器证书不受信任。2. 安全策略不匹配。3. 服务器证书主机名与URL不匹配。1. 检查证书存储路径。将服务器证书导入客户端的“受信任的颁发机构”或“受信任的对方”存储区。2. 使用UaExpert等通用客户端测试连接确认服务器支持的安全策略。3. 对于开发测试可临时在ApplicationConfiguration中设置AutoAcceptUntrustedCertificates true但需知风险。可以连接但浏览Browse不到任何节点1. 用户权限不足。2. 浏览的起始节点ID错误。3. 服务器地址空间为空或未正确配置。1. 尝试使用更高权限的用户身份连接如果服务器支持。2. 尝试从ObjectsFolder或RootFolder等标准节点开始浏览。3. 使用服务器配套的配置工具或UaExpert检查服务器是否发布了变量。订阅Subscribe成功但收不到数据通知1. 监控项MonitoredItem的SamplingInterval或PublishingInterval设置过大。2. 变量值没有变化对于数据变化才报告的订阅模式。3. 服务器端发布队列出现问题。1. 检查SamplingInterval是否设为0最快或一个合理的值如1000ms。确保PublishingIntervalSamplingInterval。2. 在订阅回调中打印日志确认回调是否被触发。尝试将监控项模式改为Reporting模式报告所有采样。3. 检查服务器日志或尝试重启服务器端的OPC UA服务。读取或写入时返回BadNodeIdUnknown1. 节点ID字符串格式错误或拼写错误。2. 节点在当前会话的视图View中不可见。3. 节点已被删除或不存在。1. 仔细核对节点ID字符串注意命名空间索引ns和标识符类型s字符串i数字gGUID。2. 使用浏览功能确认节点是否存在及其精确ID。3. 联系服务器管理员确认节点状态。高并发下客户端内存持续增长或崩溃1. 未及时处理订阅通知导致通知队列堆积。2. 会话或订阅对象未正确释放内存泄漏。3. 异步操作未妥善处理异常和取消。1. 确保订阅回调函数执行迅速避免阻塞。如果处理逻辑重应将数据推入内存队列由后台线程处理。2. 实现IDisposable接口在客户端关闭时调用session.Close()和session.Dispose()。3. 为所有异步操作传递CancellationToken并在应用关闭时触发取消确保资源清理。在Linux上运行报“找不到libopcua.so”等原生库错误OPC UA官方库的某些加密或压缩功能依赖原生库。1. 确保Linux系统上安装了必要的依赖如libssl-dev。2. 在发布时检查runtimes文件夹下对应平台的本地库是否被正确打包。可以尝试发布为--self-contained模式。最后一点个人体会开发OPC UA客户端尤其是用于生产环境的数据采集稳定性远比功能丰富性更重要。网络是不稳定的服务器是会重启的证书是会过期的。你的代码必须对所有这些情况有充分的防御和恢复能力。花在重连逻辑、异常处理和详细日志上的时间在未来运维时都会成倍地回报给你。这个覆盖1.03版本的客户端已经能够解决工业现场95%以上的数据接入需求剩下的就是根据你的具体业务场景把它打磨得更加坚固和易用。本文还有配套的精品资源点击获取