一款工业物联网高性能通讯中间件-服务端实现篇

在与远控操作台、自动化作业指令、人机交互、设备实时监控等工业物联网领域专家深入交流后,我了解到数据通讯方面的特殊要求。

请教了一些同事,我熟悉了工业标准协议。

经过三个版本的迭代,四个月的设计、编码、测试,上生产验证,一款由我独立研发的工业物联网高性能通讯中间件交付了。

本着交流合作,共同进步的精神,我决定把中间件的研发分享出来。由于内容较多,篇幅较长,将分三篇文章全面介绍这款中间件。

《一款工业物联网高性能通讯中间件-架构篇》:介绍中间件设计思想,技术方案,协议与标准,风险与应对

《一款工业物联网高性能通讯中间件-服务端实现篇》:介绍中间件服务端项目结构,开源组件,自封组件,PLC测点配置,数据采集,数据交互,OpcUa断线重连,常见问题及解决

《一款工业物联网高性能通讯中间件-客户端实现及封装篇》:内容待定

本次分享的是《一款工业物联网高性能通讯中间件-服务端实现篇》。服务端是中间件的核心,承担了底层数据采集,清洗,节点业务处理,IEC和Node接口交互,实时数据发送,历史数据发送的作用。

1 服务端项目结构

 类库说明如下:

DataCenter.Server:控制台启动程序,注册组件

DataCenter.Server.Service:服务层,解析OpuUa协议,提供IEC及Node接口,与客户端进行Socket通讯

DataCenter.Server.Model:实体层,包含请求实体,响应实体,配置实体等等

DataCenter.Server.Utility:公共调用层,包含了一些配置读取类,OpcUa数据清洗类等等

HPSocketCS:HPSocket开源源码

 类库之间关系如下:

HPSocketCS:供DataCenter.Server.Service调用

DataCenter.Server.Utility:供DataCenter.Server,DataCenter.Server.Service调用

DataCenter.Server.Model:供DataCenter.Server,DataCenter.Server.Service,DataCenter.Server.Utility调用

DataCenter.Server.Service:供DataCenter.Server调用

1.1 DataCenter.Server

1.1.1 windows服务

关键代码

复制代码
        /// <summary>
        /// 入口程序
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            bool runAsService = ConfigUtil.GetBool("RunAsService", false);
            if (runAsService)
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                {
                    new SanyEDPipeServer()
                };
                ServiceBase.Run(ServicesToRun);
            }
            else
            {
                Log.Info("数据同步服务启动...");
                SetConsoleCtrlHandler(cancelHandler, true);
                new EDPipeServerInit().Start();
            }
        }
复制代码

用一个install.bat文件安装服务,内容如下

sc create "DataCenter.Server" binpath= "%~dp0DataCenter.Server.exe" start= auto
sc description DataCenter.Server "设备管道Socket服务及数据中心"
sc failure "DataCenter.Server" reset= 3600 actions= restart/500/restart/5000/restart/30000
sc start "DataCenter.Server"

用一个uninstall.bat文件卸载服务,内容如下

sc stop "DataCenter.Server"
sc delete "DataCenter.Server"

1.1.2 类图

 1.1.2.1 Program类-启动类

复制代码
 1 using DataCenter.Server.Service;
 2 using DataCenter.Server.Service.Iec;
 3 using Infrastructure.Log;
 4 using DataCenter.Server.Service.Cache;
 5 using DataCenter.Server.Utility;
 6 using System;
 7 using System.Runtime.InteropServices;
 8 using System.ServiceProcess;
 9 
10 namespace DataCenter.Server
11 {
12     public class Program
13     {
14         private static readonly string BasePath = AppDomain.CurrentDomain.BaseDirectory;
15 
16         /// <summary>
17         /// 控制台关闭事件 
18         /// </summary>
19         /// <param name="ctrlType"></param>
20         /// <returns></returns>
21         public delegate bool ControlCtrlDelegate(int ctrlType);
22         [DllImport("kernel32.dll")]
23         private static extern bool SetConsoleCtrlHandler(ControlCtrlDelegate HandlerRoutine, bool Add);
24         private static ControlCtrlDelegate cancelHandler = new ControlCtrlDelegate(HandlerRoutine);
25 
26         public static bool HandlerRoutine(int ctrlType)
27         {
28             switch (ctrlType)
29             {
30                 //Ctrl+C关闭
31                 case 0:
32                 //按控制台关闭按钮关闭 
33                 case 2:
34                     OnStop();
35                     break;
36             }
37             return true;
38         }
39 
40         /// <summary>
41         /// 入口程序
42         /// </summary>
43         /// <param name="args"></param>
44         static void Main(string[] args)
45         {
46             bool runAsService = ConfigUtil.GetBool("RunAsService", false);
47             if (runAsService)
48             {
49                 ServiceBase[] ServicesToRun;
50                 ServicesToRun = new ServiceBase[]
51                 {
52                     new EDPipeServer()
53                 };
54                 ServiceBase.Run(ServicesToRun);
55             }
56             else
57             {
58                 Log.Info("数据同步服务启动...");
59                 SetConsoleCtrlHandler(cancelHandler, true);
60                 new EDPipeServerInit().Start();
61             }
62         }
63 
64         /// <summary>
65         /// 关闭执行事件
66         /// </summary>
67         private static void OnStop()
68         {
69             Log.Info("设备管道服务停止开始...");
70             try
71             {
72                 Log.Info("关闭OpcUa连接功能 - 开始...");
73                 DataCollectionService.DisConnectOpcUa();
74                 Log.Info("关闭OpcUa连接功能 - 结束...");
75                 Log.Info("关闭IEC最新值的记录功能 - 开始...");
76                 RealtimeMemoryService.Stop();
77                 Log.Info("关闭IEC最新值的记录功能 - 成功");
78                 Log.Info("持久化客户端节点订阅 - 开始...");
79                 NodeSubscribeClientCache.Persist();
80                 Log.Info("持久化客户端节点订阅 - 成功");
81             }
82             catch (Exception ex)
83             {
84                 Log.Error("设备管道服务停止报错,错误原因:" + ex.Message, ex);
85             }
86             Log.Info("设备管道服务停止成功");
87         }
88     }
89 }
复制代码

 1.1.2.2 EDPipeServer类-服务自动生成类

复制代码
 1 namespace DataCenter.Server
 2 {
 3     partial class EDPipeServer
 4     {
 5         /// <summary> 
 6         /// 必需的设计器变量。
 7         /// </summary>
 8         private System.ComponentModel.IContainer components = null;
 9 
10         /// <summary>
11         /// 清理所有正在使用的资源。
12         /// </summary>
13         /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
14         protected override void Dispose(bool disposing)
15         {
16             if (disposing && (components != null))
17             {
18                 components.Dispose();
19             }
20             base.Dispose(disposing);
21         }
22 
23         #region 组件设计器生成的代码
24 
25         /// <summary> 
26         /// 设计器支持所需的方法 - 不要修改
27         /// 使用代码编辑器修改此方法的内容。
28         /// </summary>
29         private void InitializeComponent()
30         {
31             components = new System.ComponentModel.Container();
32             this.ServiceName = "DataCenter.Service";
33         }
34 
35         #endregion
36     }
37 }
复制代码

 1.1.2.3 EDPipeServerInit类-数据采集,Socket注册类

复制代码
 1 using DataCenter.Server.Service;
 2 using System;
 3 
 4 namespace DataCenter.Server
 5 {
 6     public class EDPipeServerInit
 7     {
 8         public void Start()
 9         {
10 
11             //数据采集注册
12             new DataCollectionService().DataCollectionServiceRegister();
13 
14             //Socket注册
15             new HPSocketServerService().HPSocketServerServiceRegister();
16  
17             //阻塞线程,防止退出,控制台消失
18             Console.ReadLine();
19         }
20     }
21 }
复制代码

1.2 DataCenter.Server.Model

可供中间件的客户端共用

1.2.1 文档分类

 

 Enum:枚举文件,存放指令类型

复制代码
    /// <summary>
    /// 指令类型
    /// </summary>
    public enum CmdTypeEnum
    {
        心跳检测 = 1,
        握手 = 2,
        客户端测试消息 = 3,
        浏览数据 = 4, //Cmd:4
        查询点 = 5, //Cmd:5,NodeId:ns=1;i=73
        修改点 =6,
        订阅点 = 7,
        推送点 = 8,
        退订点 = 9
    }
复制代码

Iec:Iec标准实体类

Mq:Mq实体类

Persistence:持久化实体类

Plc:PLC实体类

Receive:中间件-服务端接受应用客户端消息实体类

Send:中间件-服务端发送到应用客户端消息实体类

Ua:Opu Ua自定义实体类

 1.2.2 类图

 

 

 

 

1.3 DataCenter.Server.Service

数据采集,清洗,协议解析,HpSocket通讯,MQ转发业务

1.3.1 文档分类

Cache:11个并发缓存类。包含浏览节点目录缓存,应用客户端标识与长连接标识关系缓存,应用客户端需订阅节点缓存,设备与IEC点关系缓存,节点Id与全路径缓存,节点Id与Iec点关系缓存,节点Id与浏览名称缓存,节点Id与值缓存,服务端连接客户端点持久化缓存,服务端订阅点缓存,OpcUaClient实例化缓存

Cmd:指令服务类。包含Socket心跳服务,Socket握手服务,Socket消息测试服务,节点目录浏览服务,点推送服务,点查询服务,点订阅服务,点退订服务,点修改服务

Consumers:MQ消息消费类。包含推送时发布的历史数据的消息消费服务

Iec:包含Iec格式组装及解析

Proxy:协议代理服务。包含基于OpcUa协议的解析

DataCollectionSevice:数据采集服务类

HPSoceketServerService:HpSocket服务端实现类

OpcUaServer:应用客户端请求数据交互的服务端基于OpuUa协议的服务类

1.3.2 类图

 

1.4 DataCenter.Server.Utility

 帮助类,文档结构如下

1.5 HPSocketCS

HPSocket源码,文档结构如下

 

2 开源组件

2.1 Opc Ua 基金会源码

开源地址:https://github.com/camais/UA-.NETStandard

版本:1.4.365.23

2.2 OpcUaHelper源码

开源地址:https://github.com/dathlin/OpcUaHelper

版本:1.4.365.0

2.3 HpSocket源码

开源地址:https://github.com/ldcsaa/HP-Socket

版本:5.4.2.4

3 自封组件

自封组件包含日志组件,MQ组件,UA组件,可独立成篇介绍

日志组件实现分级分类的日志功能

MQ组件实现fanout,direct消息发布与订阅功能

UA实现PLC服务器连接,节点目录浏览,节点查询,节点修改,节点订阅及节点退订功能

4 PLC测点配置

 通过配置工具读取数据库的测点信息、Plc信息,设备信息,IEC版本信息,生成PLC测点配置的json文件,文件格式如下

复制代码
 1 {
 2     "Head": "port",
 3     "Collection": "change",
 4     "PlcList": [{
 5         "Index": 1,
 6         "DeviceId": 10001001,
 7         "Name": "P1",
 8         "OpcUaServerAddress": "opc.tcp://192.168.1.11:4840",
 9         "BrowseDisplayNamespace": false,
10         "RootNodeId": "i=85",
11         "RootNodeName": "Root",
12         "IecPathList": [{
13             "DeviceId": 10001001,
14             "IecPath": "SPRD.Pos.YC.F32.X",
15             "DataPath": "ns=3;s=\"publicInfoSet\".\"spreaderInfo\".\"X\"",
16             "Type": "2",
17             "IsArray": false
18         }, {
19             "DeviceId": 10001001,
20             "IecPath": "SPRD.Pos.YC.F32.Z",
21             "DataPath": "ns=3;s=\"publicInfoSet\".\"spreaderInfo\".\"Z\"",
22             "Type": "2",
23             "IsArray": false
24         }]
25     }]
26 }
复制代码

读取配置文件的代码如下

复制代码
 1         /// <summary>
 2         /// 获取Json文件
 3         /// </summary>
 4         /// <param name="filePath"></param>
 5         /// <returns></returns>
 6         public static OpcUaConfigInfo ReadJsonFile(string filePath)
 7         {
 8             if (string.IsNullOrWhiteSpace(filePath))
 9             {
10                 return null;
11             }
12             var fullPath = GetPath(filePath);
13             var opcUaInfoList = new List<OpcUaInfo>();
14             var opcUaConfigInfo = new OpcUaConfigInfo() { OpcUaInfoList = opcUaInfoList };
15             using (StreamReader file = File.OpenText(fullPath))
16             {
17                 using (JsonTextReader reader = new JsonTextReader(file))
18                 {
19                     JObject jsonObject = (JObject)JToken.ReadFrom(reader);
20                     var head = (string)jsonObject["Head"];
21                     var collection = (string)jsonObject["Collection"];
22                     opcUaConfigInfo.Head = head;
23                     opcUaConfigInfo.Collection = collection;
24                     var plcList = jsonObject["PlcList"];
25                     foreach (JObject jobjectPlc in plcList)
26                     {
27                         var opcUaInfo = new OpcUaInfo();
28                         opcUaInfo.Index = (int)jobjectPlc["Index"];
29                         opcUaInfo.Name = (string)jobjectPlc["Name"];
30                         opcUaInfo.OpcUaServerAddress = (string)jobjectPlc["OpcUaServerAddress"];
31                         opcUaInfo.Head = head;
32                         opcUaInfo.Collection = collection;
33                         var iecPathObject = jobjectPlc["IecPathList"];
34                         var iecPathList = new List<IecPathInfo>();
35                         foreach (JObject iecJobject in iecPathObject)
36                         {
37                             var iecPathInfo = new IecPathInfo();
38                             iecPathInfo.DeviceId = (int)iecJobject["DeviceId"];
39                             iecPathInfo.IecPath = (string)iecJobject["IecPath"];
40                             iecPathInfo.DataPath = (string)iecJobject["DataPath"];
41                             iecPathInfo.IsArray = (bool)iecJobject["IsArray"];
42                             //IEC配置,类型默认是0,最大值、最小值、系数、小数位、单位默认无, zyj 2021-08-31
43                             iecPathInfo.Type = string.IsNullOrEmpty(iecJobject["Type"]?.ToString()) ? 0 : Convert.ToInt32(iecJobject["Type"]);
44                             if (!string.IsNullOrEmpty(iecJobject["MaxValue"]?.ToString()))
45                             {
46                                 iecPathInfo.MaxValue = Convert.ToDouble(iecJobject["MaxValue"]);
47                             }
48                             if (!string.IsNullOrEmpty(iecJobject["MinValue"]?.ToString()))
49                             {
50                                 iecPathInfo.MinValue = Convert.ToDouble(iecJobject["MinValue"]);
51                             }
52                             if (!string.IsNullOrEmpty(iecJobject["Coefficient"]?.ToString()))
53                             {
54                                 iecPathInfo.Coefficient = Convert.ToDouble(iecJobject["Coefficient"]);
55                             }
56                             if (!string.IsNullOrEmpty(iecJobject["DecimalDigit"]?.ToString()))
57                             {
58                                 iecPathInfo.DecimalDigit = Convert.ToInt32(iecJobject["DecimalDigit"]);
59                             }
60                             if (!string.IsNullOrEmpty(iecJobject["IecUnit"]?.ToString()))
61                             {
62                                 iecPathInfo.IecUnit = Convert.ToString(iecJobject["IecUnit"]);
63                             }
64                             iecPathList.Add(iecPathInfo);
65                         }
66                         opcUaInfo.IecPathList = iecPathList;
67 
68                         opcUaInfoList.Add(opcUaInfo);
69                     }
70                 }
71             }
72             return opcUaConfigInfo;
73         }
复制代码

5 数据采集

通过OpcUaHelper开源框架实现数据采集,实现了连接,关闭OpcUaServer端,节点目录浏览,节点查询,节日修改,节点订阅,节点退订功能,开放了OpcUa断线重连接口。在OpcUaHelper的基础上再次封装了Infrastructure.UA组件。

5.1 文档格式

5.2 类图

6 数据交互

数据交互提供了应用客户端调用接口,提供了节点查询,修改,订阅,退订,推送功能,代码如下

复制代码
  1 using Newtonsoft.Json;
  2 using DataCenter.Server.Service.Cache;
  3 using DataCenter.Server.Service.Proxy;
  4 using Infrastructure.Log;
  5 using DataCenter.Server.Model.Ua;
  6 using DataCenter.Server.Utility;
  7 using System;
  8 using System.Collections.Concurrent;
  9 using System.Collections.Generic;
 10 using System.Diagnostics;
 11 using System.Linq;
 12 
 13 namespace DataCenter.Server.Service
 14 {
 15     public class OpcUaServer
 16     {
 17         /// <summary>
 18         /// 是否打印日志
 19         /// </summary>
 20         private static bool PrintLog { get { return ConfigUtil.GetBool("PrintLog", false); } }
 21 
 22         #region 浏览
 23         /// <summary>
 24         /// 缓存查询
 25         /// </summary>
 26         /// <param name="clientNodeIdStrList"></param>
 27         /// <returns></returns>
 28         public static List<BrowseResultTree> QueryBrowseListFromCacheByNodeId(List<NodeIdStr> clientNodeIdStrList)
 29         {
 30             var result = new List<BrowseResultTree>();
 31             var clientNodeIdList = ConvertToNodeIdRequest(clientNodeIdStrList);
 32             if (clientNodeIdList == null || !clientNodeIdList.Any() || clientNodeIdList.Any(o => o == null || o.PlcKey <= 0))
 33             {
 34                 Log.Info("节点Id为空或Plc键非法,已从缓存中查询");
 35                 var opcUaProxyCache = OpcUaProxyCache.GetDic();
 36                 if (opcUaProxyCache == null || opcUaProxyCache.Keys == null || !opcUaProxyCache.Keys.Any())
 37                 {
 38                     return null;
 39                 }
 40                 foreach (var keyValue in opcUaProxyCache)
 41                 {
 42                     var opcUaProxy = OpcUaProxyCache.Get(keyValue.Key);
 43                     var tree = opcUaProxy.QueryBrowseFromCache(null);
 44                     result.Add(tree);
 45                 }
 46                 return result;
 47             }
 48             foreach(var clientNodeId in clientNodeIdList)
 49             {
 50                 var tree = QueryBrowseFromCacheByNodeId(clientNodeId);
 51                 if (tree != null)
 52                 {
 53                     result.Add(tree);
 54                 }
 55             }
 56             return result;
 57         }
 58 
 59         #endregion
 60 
 61         #region 节点查询
 62         /// <summary>
 63         /// 查点值集合
 64         /// </summary>
 65         /// <param name="nodeIdStrList"></param>
 66         /// <param name="needSort">是否需要排序</param>
 67         /// <returns></returns>
 68         public static List<NodeInfo> QueryNodeList(List<NodeIdStr> nodeIdStrList, bool needSort = true)
 69         {
 70             var nodeInfoList = new List<NodeInfo>();
 71             if (nodeIdStrList == null || !nodeIdStrList.Any())
 72             {
 73                 return null;
 74             }
 75             var plcKeyValidate = nodeIdStrList.Any(o => o.Id == null || OpcUaConfigHelper.NodeIdRequestFromStr(o.Id)?.PlcKey <= 0);
 76             if (plcKeyValidate)
 77             {
 78                 return nodeInfoList;
 79             }
 80             var dicReadNode = NodeToClass(nodeIdStrList);
 81             if (dicReadNode == null || dicReadNode.Keys == null || dicReadNode.Keys.Count == 0)
 82             {
 83                 return nodeInfoList;
 84             }
 85             double cacheCost = 0;
 86             foreach (var keyValue in dicReadNode)
 87             {
 88                 var opcUaProxy = OpcUaProxyCache.Get(Convert.ToInt32(keyValue.Key));
 89                 if (opcUaProxy == null)
 90                 {
 91                     continue;
 92                 }
 93                 var nodeIdRequestList = ConvertToNodeIdRequest(keyValue.Value);
 94                 var nodeIdList = nodeIdRequestList.Select(o => o.NodeId).ToList();
 95                 var stopwatch = new Stopwatch();
 96                 stopwatch.Start();
 97                 var query = opcUaProxy.QueryNodeList(nodeIdList);
 98                 nodeInfoList.AddRange(query);
 99                 stopwatch.Stop();
100                 cacheCost += stopwatch.ElapsedMilliseconds;
101             }
102             if (!needSort)
103             {
104                 Log.Warn($"【查询】数据中心<-->opc耗时总:{cacheCost}");
105                 return nodeInfoList;
106             }
107             var result = ReSort(nodeIdStrList, nodeInfoList);
108             Log.Warn($"【查询】数据中心<-->opc耗时总:{cacheCost}");
109             return result;
110         }
111         #endregion
112 
113         #region 节点写入
114 
115         /// <summary>
116         /// 写值
117         /// </summary>
118         /// <param name="nodes">节点修改集合(节点Id包含了OpcUa的Name)</param>
119         /// <param name="statusCodeList">状态码集合</param>
120         /// <returns></returns>
121         public static bool WriteNode(List<NodeUpdateBase> nodes, out List<string> statusCodeList)
122         {
123             statusCodeList = new List<string>();
124             var errorMsg = string.Empty;
125             if (nodes == null || !nodes.Any())
126             {
127                 errorMsg = "值为空";
128                 Log.Info(errorMsg, "WriteNode");
129                 return false;
130             }
131             var plcKeyValidate = nodes.Any(o => o.Id == null || OpcUaConfigHelper.NodeIdRequestFromStr(o.Id)?.PlcKey <= 0);
132             if (plcKeyValidate)
133             {
134                 errorMsg = "PlcKey Invalid";
135                 foreach (var nodeUpdateBase in nodes)
136                 {
137                     statusCodeList.Add(errorMsg);
138                 }
139                 Log.Info(errorMsg, "WriteNode");
140                 return false;
141             }
142 
143             var dicWriteNode = NodeToClass(nodes);
144             if (dicWriteNode == null || dicWriteNode.Keys == null || dicWriteNode.Keys.Count == 0)
145             {
146                 errorMsg = "Data Null";
147                 foreach (var nodeUpdateBase in nodes)
148                 {
149                     statusCodeList.Add(errorMsg);
150                 }
151                 Log.Info(errorMsg, "WriteNode");
152                 return false;
153             }
154             var nodeUpdateResultList = new List<NodeUpdateResult>();
155             var result = true;
156             foreach (var keyValue in dicWriteNode)
157             {
158                 var opcUaProxy = OpcUaProxyCache.Get(Convert.ToInt32(keyValue.Key));
159                 if (opcUaProxy == null)
160                 {
161                     errorMsg = "Plc Example Null";
162                     foreach (var node in keyValue.Value)
163                     {
164                         var nodeUpdateResult = new NodeUpdateResult() { Id = node.Id, StatusCode = errorMsg };
165                         nodeUpdateResultList.Add(nodeUpdateResult);
166                         result = false;
167                     }
168                     continue;
169                 }
170                 var writeResult = opcUaProxy.WriteNode(keyValue.Value, ref nodeUpdateResultList);
171                 if (!writeResult)
172                 {
173                     result=false;
174                 }
175             }
176             #region 重排
177             var needResort = result == false || dicWriteNode.Count > 1;
178             statusCodeList = ReSort(nodes, nodeUpdateResultList, needResort);
179             #endregion
180             return result;
181         }
182 
183         #endregion
184 
185         #region 客户端点订阅、退订
186 
187         /// <summary>
188         /// 客户端订阅点
189         /// </summary>
190         /// <param name="clientId">客户端Id</param>
191         /// <param name="nodeIdStrList">订阅节点列表</param>
192         /// <param name="nodeQueryList">节点列表查询信息</param>
193         /// <param name="successSubNodeList">成功订阅的节点列表(乱序,只包含成功订阅的节点)</param>
194         /// <returns></returns>
195         public static bool ClientNodeSubscribe(string clientId, List<NodeIdStr> nodeIdStrList, ref List<NodeInfo> nodeQueryList,ref List<NodeOperateResult> successSubNodeList)
196         {
197             if(successSubNodeList == null)
198             {
199                 successSubNodeList = new List<NodeOperateResult>();
200             }
201             //非空校验
202             if (nodeIdStrList == null || !nodeIdStrList.Any())
203             {
204                 Log.Warn("无订阅点");
205                 return false;
206             }
207             //加入到待订阅的客户端缓存,防止断线重连未订阅到客户端订阅的点
208             ClientNeedSubscribeNodeCache.Set(clientId, nodeIdStrList);
209             //订阅点信息
210             nodeQueryList = QueryNodeList(nodeIdStrList);
211             if (nodeQueryList == null || !nodeQueryList.Any())
212             {
213                 Log.Warn("点信息查询失败");
214                 return false;
215             }
216             var nodeIds = new List<NodeInfo>();
217             for(int i = 0; i < nodeQueryList.Count; i++)
218             {
219                 if (nodeQueryList[i] == null || string.IsNullOrWhiteSpace(nodeQueryList[i].Id))
220                 {
221                     if (PrintLog)
222                     {
223                         Log.Warn($"第{i + 1}项节点Id为空,已忽略此节点订阅");
224                     }
225                     continue;
226                 }
227                 var idRequest = OpcUaConfigHelper.NodeIdRequestFromStr(nodeQueryList[i].Id);
228                 if (idRequest == null || idRequest.PlcKey<=0 || idRequest.NodeId==null|| string.IsNullOrWhiteSpace(idRequest.NodeId.ToString()))
229                 {
230                     Log.Warn($"第{i + 1}项节点PlcKey非法,已忽略此节点订阅,{JsonConvert.SerializeObject(nodeQueryList[i])}");
231                     continue;
232                 }
233                 nodeIds.Add(nodeQueryList[i]);
234 
235             }
236             if (!nodeIds.Any())
237             {
238                 Log.Warn("无合法订阅点");
239                 return false;
240             }
241             //节点分类
242             var dicWriteNode = NodeToClass(nodeIds);
243             if (dicWriteNode == null || dicWriteNode.Keys == null || dicWriteNode.Keys.Count == 0)
244             {
245                  Log.Warn("节点分类数据为空");
246                 return false;
247             }
248             //根据各自Opc实例订阅
249             var readyNumber = 0; //待订阅个数
250             var successNodeList = new List<NodeIdStr>();
251             foreach (var keyValue in dicWriteNode)
252             {
253                 readyNumber += keyValue.Value.Count;
254                 var opcUaProxy = OpcUaProxyCache.Get(Convert.ToInt32(keyValue.Key));
255                 if (opcUaProxy == null)
256                 {
257                     Log.Warn($"订阅点-获取不到OpcUa实例,PlcKey:{keyValue.Key}");
258                     continue;
259                 }
260                 List<NodeOperateResult> subResult = null;
261                 var success = opcUaProxy.SubscribeNode(keyValue.Value, ref subResult);
262                 if (success)
263                 {
264                     if (subResult != null && subResult.Any())
265                     {
266                         successSubNodeList.AddRange(subResult);
267                         subResult.ForEach(o =>
268                         {
269                             if (o.Success)
270                             {
271                                 successNodeList.Add(new NodeIdStr { Id = o.Id });
272                             }
273                         });
274                     }
275                 }
276                 else
277                 {
278                     Log.Warn($"订阅点失败:点{JsonConvert.SerializeObject(keyValue.Value)},subResult{JsonConvert.SerializeObject(subResult)}");
279                 }
280             }
281             if(successNodeList.Count != readyNumber)
282             {
283                 Log.Warn($"订阅个数不一致:共计{readyNumber}个点订阅,实际订阅{successNodeList.Count}个");
284             }
285             var addResult = NodeSubscribeClientCache.Set(clientId, successNodeList);
286             //客户端待订阅缓存清除订阅成功的点
287             if (addResult)
288             {
289                 ClientNeedSubscribeNodeCache.Remove(clientId, successNodeList);
290             }
291             var result = successNodeList.Any();
292             return result;
293         }
294 
295         /// <summary>
296         /// 客户端退订点
297         /// </summary>
298         /// <param name="clientId"></param>
299         /// <param name="nodeIdStrList"></param>
300         /// <param name="unSubResult">退订结果(只有退订有结果的的节点)</param>
301         /// <returns></returns>
302         public static bool ClientNodeUnSubscribe(string clientId, List<NodeIdStr> nodeIdStrList, ref List<NodeOperateResult> unSubResult)
303         {
304             if(unSubResult == null)
305             {
306                 unSubResult = new List<NodeOperateResult>();
307             }
308             //非空校验
309             if (nodeIdStrList == null || !nodeIdStrList.Any())
310             {
311                 Log.Warn("无退订点");
312                 return false;
313             }
314             //节点分类
315             var dicWriteNode = NodeToClass(nodeIdStrList);
316             if (dicWriteNode == null || dicWriteNode.Keys == null || dicWriteNode.Keys.Count == 0)
317             {
318                 Log.Warn("节点分类数据为空");
319                 return false;
320             }
321             //根据各自Opc实例退订
322             var readyNumber = 0; //待退订个数
323             var successNumber = 0;//退订成功个数
324             var stopwatch = new Stopwatch();
325             stopwatch.Start();
326             foreach (var keyValue in dicWriteNode)
327             {
328                 readyNumber += keyValue.Value.Count;
329                 var opcUaProxy = OpcUaProxyCache.Get(Convert.ToInt32(keyValue.Key));
330                 if (opcUaProxy == null)
331                 {
332                     Log.Warn($"退订点-获取不到OpcUa实例,PlcKey:{keyValue.Key}");
333                     continue;
334                 }
335                 var success = opcUaProxy.UnSubscribeNode(keyValue.Value.Select(o=>o.Id)?.ToList());
336                 if (success)
337                 {
338                     successNumber += keyValue.Value.Count;
339                     unSubResult.AddRange(keyValue.Value.Select(o => new NodeOperateResult { Id = o.Id, Success = true }));
340                 }
341                 else
342                 {
343                     Log.Warn($"退订点失败:点{JsonConvert.SerializeObject(keyValue.Value)}");
344                 }
345             }
346             stopwatch.Stop();
347             Log.Warn($"【退订】数据中心<-->opc耗时(ms)缓存:{stopwatch.ElapsedMilliseconds}");
348             if (successNumber != readyNumber)
349             {
350                 Log.Warn($"退阅个数不一致:共计{readyNumber}个点退订,实际退订{successNumber}个");
351             }
352             if (!unSubResult.Any())
353             {
354                 return false;
355             }
356             var successNodes = unSubResult.Select(o => new NodeIdStr { Id = o.Id }).ToList();
357             var result = NodeSubscribeClientCache.Remove(clientId, successNodes);
358             //客户端待订阅缓存清除订阅成功的点
359             if (result)
360             {
361                 ClientNeedSubscribeNodeCache.Remove(clientId, successNodes);
362             }
363             return result;
364         }
365         #endregion
366 
367         #region 私有方法
368 
369 
370         /// <summary>
371         /// 重排
372         /// </summary>
373         /// <param name="nodeIdStrList">传入的值</param>
374         /// <param name="nodeIdList">结果集</param>
375         /// <returns></returns>
376         private static List<NodeInfo> ReSort(List<NodeIdStr> nodeIdStrList, List<NodeInfo> nodeIdList)
377         {
378             var result = new List<NodeInfo>();
379             if (nodeIdList == null || !nodeIdList.Any())
380             {
381                 return result;
382             }
383             nodeIdStrList.ForEach(o =>
384             {
385                 var nodeInfo = nodeIdList.Find(p => p.Id == o.Id);
386                 result.Add(nodeInfo);
387             });
388             return result;
389         }
390 
391         /// <summary>
392         /// 重排序
393         /// </summary>
394         /// <param name="nodes">原始节点列表</param>
395         /// <param name="nodeUpdateResultList">修改后节点结果列表</param>
396         /// <param name="needResort">是否需要重排序</param>
397         /// <returns></returns>
398         private static List<string> ReSort(List<NodeUpdateBase> nodes, List<NodeUpdateResult> nodeUpdateResultList, bool needResort)
399         {
400             if (!needResort)
401             {
402                 var result = nodeUpdateResultList.Select(o => o.StatusCode).ToList();
403                 return result;
404             }
405             var statusCodeList = new List<string>();
406             foreach (var node in nodes)
407             {
408                 var nodeUpdateResult = nodeUpdateResultList.Find(o => o.Id == node.Id);
409                 var statusCode = nodeUpdateResult == null ? "未获取到写入信息" : nodeUpdateResult.StatusCode;
410                 statusCodeList.Add(statusCode);
411             }
412             return statusCodeList;
413         }
414 
415         /// <summary>
416         /// 节点分类
417         /// </summary>
418         /// <param name="nodes"></param>
419         /// <returns></returns>
420         private static ConcurrentDictionary<int, List<T>> NodeToClass<T>(List<T> nodes) where T : NodeIdStr
421         {
422             if (nodes == null || !nodes.Any())
423             {
424                 return null;
425             }
426             var result = new ConcurrentDictionary<int, List<T>>();
427             for (int i = 0; i < nodes.Count; i++)
428             {
429                 if (nodes[i] == null || string.IsNullOrWhiteSpace(nodes[i].Id))
430                 {
431                     Log.Warn($"写入点第{i + 1}项节点Id为空,已忽略此节点,写入集合{JsonConvert.SerializeObject(nodes)}", "NodeToClass");
432                     continue;
433                 }
434                 var nodeIdRequest = OpcUaConfigHelper.NodeIdRequestFromStr(nodes[i].Id);
435                 var key = nodeIdRequest.PlcKey;
436                 if (result.ContainsKey(key))
437                 {
438                     var value = result[key];
439                     value.Add(nodes[i]);
440                     result.AddOrUpdate(key, value, (tKey, existingVal) => { return value; });
441                 }
442                 else
443                 {
444                     var value = new List<T> { nodes[i] };
445                     result.AddOrUpdate(key, value, (tKey, existingVal) => { return value; });
446                 }
447             }
448             return result;
449         }
450 
451         /// <summary>
452         /// 缓存查询
453         /// </summary>
454         /// <param name="clientNodeId"></param>
455         /// <returns></returns>
456         private static BrowseResultTree QueryBrowseFromCacheByNodeId(NodeIdRequest clientNodeId)
457         {
458             if (clientNodeId == null && clientNodeId.NodeId == null || string.IsNullOrWhiteSpace(clientNodeId.NodeId.ToString()))
459             {
460                 Log.Warn("客户端点为空");
461                 return null;
462             }
463             if (clientNodeId.PlcKey <= 0)
464             {
465                 Log.Warn("plc标识错误");
466                 return null;
467             }
468             var opcUaProxy = OpcUaProxyCache.Get(clientNodeId.PlcKey);
469             if(opcUaProxy == null)
470             {
471                 Log.Warn($"没有获取到Plc实例:Plc:{clientNodeId.PlcKey}");
472                 return null;
473             }
474             var cacheNodeId = OpcUaConfigHelper.KeyToCache(clientNodeId.NodeId.ToString(), clientNodeId.PlcKey);
475             var result = opcUaProxy.QueryBrowseFromCache(cacheNodeId);
476             return result;
477         }
478 
479         /// <summary>
480         /// 转为NodeIdRequest集合
481         /// </summary>
482         /// <param name="clientNodeIdList"></param>
483         /// <returns></returns>
484         private static List<NodeIdRequest> ConvertToNodeIdRequest(List<NodeIdStr> clientNodeIdList)
485         {
486             if (clientNodeIdList == null || !clientNodeIdList.Any())
487             {
488                 return null;
489             }
490             var result = new List<NodeIdRequest>();
491             clientNodeIdList.ForEach(o =>
492             {
493                 var request = OpcUaConfigHelper.NodeIdRequestFromStr(o.Id);
494                 result.Add(request);
495             });
496             return result;
497         }
498 
499         #endregion
500     }
501 }
复制代码

7 OpcUa断线重连

Opc Ua Server更新,网络断开会触发Infrastructure.UA组件断线重连,重连成功后会通知OpcUa实体,然后重新建立连接,重新订阅点。实现如下

复制代码
1         /// <summary>
2         /// 断线重连成功通知注册
3         /// </summary>
4         /// <returns></returns>
5         public bool ReconnectSuccessNoticeRegister()
6         {
7             OpcUaClient.reconnectSuccessNoticeEvent.OnNotice += OnNotice;
8             return true;
9         }
复制代码
复制代码
 1         /// <summary>
 2         /// 订阅断线重连
 3         /// </summary>
 4         /// <returns></returns>
 5         public bool OnNotice()
 6         {
 7             try
 8             {
 9                 Log.Warn("Opc Ua触发了断线重连通知!");
10                 DisConnect();
11                 Thread.Sleep(500);
12                 Connect();
13                 if (OpcUaClient.IsConnected)
14                 {
15                     Log.Warn("Opc Ua断线重连成功了!");
16                     //停止订阅推初值
17                     OpcUaClient.Sub_Stop();
18                     //注册订阅
19                     NodeSubscribeRegister();
20                     //订阅
21                     var cacheNodeInfoList = NodeSubscribeServerCache.GetAll();
22                     //服务端订阅所有缓存
23                     if (cacheNodeInfoList != null && cacheNodeInfoList.Any())
24                     {
25                         List<NodeOperateResult> subResult = null;
26                         Subscribe(cacheNodeInfoList, ref subResult, false);
27                         Log.Warn($"Opc Ua断线重连:服务端点订阅缓存为{JsonConvert.SerializeObject(cacheNodeInfoList.Select(o => o.Id).ToList())}!");
28                     }
29                     else
30                     {
31                         Log.Warn("Opc Ua断线重连:服务端点订阅缓存为空,无需订阅!");
32                     }
33                     //客户端订阅
34                     ClientSubscribe();
35                 }
36                 return true;
37             }
38             catch(Exception ex)
39             {
40                 Log.Error($"断线重连结果异常:{ex.Message}", ex);
41                 return false;
42             }
43         }
复制代码

8 常见问题及解决

8.1 批量操作(查询,订阅,修改)出现ToManyOperation错误

问题原因:PLC硬件性能不一致,比如西门子PLC-1500系列,订阅批次数不能超过20,每批次不能超过1000个节点

问题解决:批量操作时分批处理,通过Infrastructure.UA组件查询,修改节点每批次数设置成500。订阅时通过key来关联批次,每个key是一个批次,一个批次1000个节点,保存key与节点的缓存关系,超过20个批次的节点共计2万个点时,超过部分丢弃,写异常日志。

8.2 启动服务时报错,报InvalidSession错误

问题原因:上一次关闭时OPC UA基金会有缓存,还未到2分钟的过期时间,Session还保存了订阅点信息,订阅时出错

问题解决:每次程序关闭时主动关闭OpcUa的连接

 8.3 OpcUA短线重连20次后重连不起作用

问题原因:OpcUaServer端默认对每个Session有重连次数的限制且不可更改

问题解决:每次重连时关闭原来的连接,重新启动新的连接

 

posted @   开水做的白菜  阅读(634)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示