├── .vs └── Axiu.Opcua.Demo │ ├── v16 │ └── .suo │ └── DesignTimeBuild │ └── .dtbcache.v2 ├── Axiu.Opcua.Demo.Model ├── Axiu.Opcua.Demo.Model.csproj └── OpcuaNode.cs ├── README.md ├── Axiu.Opcua.Demo.Common ├── Axiu.Opcua.Demo.Common.csproj ├── RandomLibrary.cs └── JsonExtension.cs ├── Axiu.Opcua.Demo.Entry ├── Program.cs └── Axiu.Opcua.Demo.Entry.csproj ├── Axiu.Opcua.Demo.Service ├── Axiu.Opcua.Demo.Service.csproj ├── OpcuaManagement.cs ├── DiscoveryManagement.cs ├── AxiuOpcuaServer.cs ├── AxiuNodeManager.cs └── DiscoveryServer.cs └── Axiu.Opcua.Demo.sln /.vs/Axiu.Opcua.Demo/v16/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiu233/AxiuOpcua.ServerDemo/HEAD/.vs/Axiu.Opcua.Demo/v16/.suo -------------------------------------------------------------------------------- /.vs/Axiu.Opcua.Demo/DesignTimeBuild/.dtbcache.v2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axiu233/AxiuOpcua.ServerDemo/HEAD/.vs/Axiu.Opcua.Demo/DesignTimeBuild/.dtbcache.v2 -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Model/Axiu.Opcua.Demo.Model.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | AnyCPU;x86 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # AxiuOpcua.ServerDemo--简单的OPC-UA Server示例 3 | 本示例是在 OPCFoundation/UA-.NETStandard 代码的基础上实现的一个简单OPC-UA服务端 4 | 包含 动态创建节点、删除节点、修改节点,实时数据刷新,历史数据读取等等 5 | ## OPCFoundation官方源码连接: [OPCFoundation/UA-.NETStandard](https://github.com/OPCFoundation/UA-.NETStandard). -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Common/Axiu.Opcua.Demo.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | AnyCPU;x86 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Entry/Program.cs: -------------------------------------------------------------------------------- 1 | using Axiu.Opcua.Demo.Service; 2 | using System; 3 | 4 | namespace Axiu.Opcua.Demo.Entry 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | OpcuaManagement server = new OpcuaManagement(); 11 | server.CreateServerInstance(); 12 | Console.WriteLine("OPC-UA服务已启动..."); 13 | Console.ReadLine(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Entry/Axiu.Opcua.Demo.Entry.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | AnyCPU;x86 7 | 8 | 9 | 10 | D:\CET\Common\ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Service/Axiu.Opcua.Demo.Service.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | AnyCPU;x86 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Model/OpcuaNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Axiu.Opcua.Demo.Model 4 | { 5 | public class OpcuaNode 6 | { 7 | /// 8 | /// 节点路径(逐级拼接) 9 | /// 10 | public string NodePath { get; set; } 11 | /// 12 | /// 父节点路径(逐级拼接) 13 | /// 14 | public string ParentPath { get; set; } 15 | /// 16 | /// 节点编号 (在我的业务系统中的节点编号并不完全唯一,但是所有测点Id都是不同的) 17 | /// 18 | public int NodeId { get; set; } 19 | /// 20 | /// 节点名称(展示名称) 21 | /// 22 | public string NodeName { get; set; } 23 | /// 24 | /// 是否端点(最底端子节点) 25 | /// 26 | public bool IsTerminal { get; set; } 27 | /// 28 | /// 节点类型 29 | /// 30 | public NodeType NodeType { get; set; } 31 | } 32 | public enum NodeType 33 | { 34 | /// 35 | /// 根节点 36 | /// 37 | Scada = 1, 38 | /// 39 | /// 目录 40 | /// 41 | Channel = 2, 42 | /// 43 | /// 目录 44 | /// 45 | Device = 3, 46 | /// 47 | /// 测点 48 | /// 49 | Measure = 4 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Common/RandomLibrary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Axiu.Opcua.Demo.Common 6 | { 7 | public class RandomLibrary 8 | { 9 | //private static string RandomString = "123456789abcdefghijkmnpqrstuvwxyz123456789ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"; 10 | private static string RandomString = "123456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; 11 | private static Random Random = new Random(DateTime.Now.Second); 12 | private static Random _random = new Random(); 13 | 14 | #region 产生随机字符串 +GetRandomStr(int length) 15 | /// 16 | /// 产生随机字符串 17 | /// 18 | /// 字符串长度 19 | /// 20 | public static string GetRandomStr(int length) 21 | { 22 | string retValue = string.Empty; 23 | for (int i = 0; i < length; i++) 24 | { 25 | int r = Random.Next(0, RandomString.Length - 1); 26 | retValue += RandomString[r]; 27 | } 28 | return retValue; 29 | } 30 | #endregion 31 | 32 | #region 产生随机数 +GetRandomInt(int min, int max) 33 | /// 34 | /// 产生随机数 35 | /// 36 | /// 最小值 37 | /// 最大值 38 | /// 39 | public static int GetRandomInt(int min, int max) 40 | { 41 | return Random.Next(min, max); 42 | } 43 | #endregion 44 | 45 | #region 产生一个随机小数 +GetRandomDouble() 46 | /// 47 | /// 产生一个随机小数 48 | /// 49 | /// 50 | public static double GetRandomDouble() 51 | { 52 | return _random.NextDouble(); 53 | } 54 | #endregion 55 | 56 | #region 随机排序 +GetRandomSort(T[] arr) 57 | /// 58 | /// 随机排序 59 | /// 因为数组是引用类型 所以不需要返回值 60 | /// 调用之后即改变原数组排序,直接使用即可 61 | /// 62 | /// 参数类型,必须为数组类型 63 | /// 数组 64 | public static void GetRandomSort(T[] arr) 65 | { 66 | int count = arr.Length; 67 | for (int i = 0; i < count; i++) 68 | { 69 | int rn1 = GetRandomInt(0, arr.Length); 70 | int rn2 = GetRandomInt(0, arr.Length); 71 | T temp; 72 | temp = arr[rn1]; 73 | arr[rn1] = arr[rn2]; 74 | arr[rn2] = temp; 75 | } 76 | //return arr; 77 | } 78 | #endregion 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Common/JsonExtension.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | 8 | namespace Axiu.Opcua.Demo.Common 9 | { 10 | public static class JsonExtension 11 | { 12 | /// 13 | /// 字符串转为json 14 | /// 15 | /// 16 | /// 17 | public static object ToJson(this string Json) 18 | { 19 | return Json == null ? null : JsonConvert.DeserializeObject(Json); 20 | } 21 | /// 22 | /// object转为json 23 | /// 24 | /// 25 | /// 26 | public static string ToJson(this object obj) 27 | { 28 | var timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" }; 29 | return JsonConvert.SerializeObject(obj, timeConverter); 30 | } 31 | /// 32 | /// object转为json 33 | /// 34 | /// 35 | /// 36 | /// 37 | public static string ToJson(this object obj, string datetimeformats) 38 | { 39 | var timeConverter = new IsoDateTimeConverter { DateTimeFormat = datetimeformats }; 40 | return JsonConvert.SerializeObject(obj, timeConverter); 41 | } 42 | /// 43 | /// json串转为对象 44 | /// 45 | /// 46 | /// 47 | /// 48 | public static T JsonToObject(this string Json) 49 | { 50 | return Json == null ? default(T) : JsonConvert.DeserializeObject(Json); 51 | } 52 | /// 53 | /// json串转为List 54 | /// 55 | /// 56 | /// 57 | /// 58 | public static List JsonToList(this string Json) 59 | { 60 | return Json == null ? null : JsonConvert.DeserializeObject>(Json); 61 | } 62 | /// 63 | /// json串转为DataTable 64 | /// 65 | /// 66 | /// 67 | public static DataTable JsonToTable(this string Json) 68 | { 69 | return Json == null ? null : JsonConvert.DeserializeObject(Json); 70 | } 71 | public static JObject JsonToJObject(this string Json) 72 | { 73 | return Json == null ? JObject.Parse("{}") : JObject.Parse(Json.Replace(" ", "")); 74 | } 75 | public static string DataTableToJson(this DataTable dt) 76 | { 77 | IsoDateTimeConverter timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss" }; 78 | return JsonConvert.SerializeObject(dt, Formatting.Indented, timeConverter); 79 | 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Service/OpcuaManagement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Opc.Ua; 3 | using Opc.Ua.Configuration; 4 | using Opc.Ua.Server; 5 | using Opc.Ua.Gds.Server; 6 | 7 | namespace Axiu.Opcua.Demo.Service 8 | { 9 | public class OpcuaManagement 10 | { 11 | public void CreateServerInstance() 12 | { 13 | try 14 | { 15 | var config = new ApplicationConfiguration() 16 | { 17 | ApplicationName = "AxiuOpcua", 18 | ApplicationUri = Utils.Format(@"urn:{0}:AxiuOpcua", System.Net.Dns.GetHostName()), 19 | ApplicationType = ApplicationType.Server, 20 | ServerConfiguration = new ServerConfiguration() 21 | { 22 | BaseAddresses = { "opc.tcp://localhost:8020/", "https://localhost:8021/" }, 23 | MinRequestThreadCount = 5, 24 | MaxRequestThreadCount = 100, 25 | MaxQueuedRequestCount = 200, 26 | }, 27 | SecurityConfiguration = new SecurityConfiguration 28 | { 29 | ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", SubjectName = Utils.Format(@"CN={0}, DC={1}", "AxiuOpcua", System.Net.Dns.GetHostName()) }, 30 | TrustedIssuerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" }, 31 | TrustedPeerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" }, 32 | RejectedCertificateStore = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" }, 33 | AutoAcceptUntrustedCertificates = true, 34 | AddAppCertToTrustedStore = true 35 | }, 36 | TransportConfigurations = new TransportConfigurationCollection(), 37 | TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, 38 | ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }, 39 | TraceConfiguration = new TraceConfiguration() 40 | }; 41 | config.Validate(ApplicationType.Server).GetAwaiter().GetResult(); 42 | if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates) 43 | { 44 | config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted); }; 45 | } 46 | 47 | var application = new ApplicationInstance 48 | { 49 | ApplicationName = "AxiuOpcua", 50 | ApplicationType = ApplicationType.Server, 51 | ApplicationConfiguration = config 52 | }; 53 | //application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult(); 54 | bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result; 55 | if (!certOk) 56 | { 57 | Console.WriteLine("证书验证失败!"); 58 | } 59 | 60 | var dis =new DiscoveryServerBase(); 61 | // start the server. 62 | application.Start(new AxiuOpcuaServer()).Wait(); 63 | } 64 | catch (Exception ex) 65 | { 66 | Console.ForegroundColor = ConsoleColor.Red; 67 | Console.WriteLine("启动OPC-UA服务端触发异常:" + ex.Message); 68 | Console.ResetColor(); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30225.117 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Axiu.Opcua.Demo.Entry", "Axiu.Opcua.Demo.Entry\Axiu.Opcua.Demo.Entry.csproj", "{544ADC48-A90A-4DAB-B238-E49D75482439}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Axiu.Opcua.Demo.Service", "Axiu.Opcua.Demo.Service\Axiu.Opcua.Demo.Service.csproj", "{D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Axiu.Opcua.Demo.Common", "Axiu.Opcua.Demo.Common\Axiu.Opcua.Demo.Common.csproj", "{8445E2F3-AE84-4894-A272-6FBD98F09278}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Axiu.Opcua.Demo.Model", "Axiu.Opcua.Demo.Model\Axiu.Opcua.Demo.Model.csproj", "{019124BD-6BBF-4451-9E33-E23881171D4D}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x86 = Debug|x86 18 | Release|Any CPU = Release|Any CPU 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Debug|x86.ActiveCfg = Debug|x86 25 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Debug|x86.Build.0 = Debug|x86 26 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Release|x86.ActiveCfg = Release|x86 29 | {544ADC48-A90A-4DAB-B238-E49D75482439}.Release|x86.Build.0 = Release|x86 30 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Debug|x86.ActiveCfg = Debug|x86 33 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Debug|x86.Build.0 = Debug|x86 34 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Release|x86.ActiveCfg = Release|x86 37 | {D7941C9A-3955-4C75-8809-DC6C1B3ACFAD}.Release|x86.Build.0 = Release|x86 38 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Debug|x86.ActiveCfg = Debug|x86 41 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Debug|x86.Build.0 = Debug|x86 42 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Release|x86.ActiveCfg = Release|x86 45 | {8445E2F3-AE84-4894-A272-6FBD98F09278}.Release|x86.Build.0 = Release|x86 46 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Debug|x86.ActiveCfg = Debug|x86 49 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Debug|x86.Build.0 = Debug|x86 50 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Release|x86.ActiveCfg = Release|x86 53 | {019124BD-6BBF-4451-9E33-E23881171D4D}.Release|x86.Build.0 = Release|x86 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {88B972FA-57C5-4C0C-83DA-45944C03A37C} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Service/DiscoveryManagement.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua; 2 | using Opc.Ua.Configuration; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | 8 | namespace Axiu.Opcua.Demo.Service 9 | { 10 | public class DiscoveryManagement 11 | { 12 | /// 13 | /// 启动一个Discovery服务端 14 | /// 15 | public void StartDiscovery() 16 | { 17 | try 18 | { 19 | var config = new ApplicationConfiguration() 20 | { 21 | ApplicationName = "Axiu UA Discovery", 22 | ApplicationUri = Utils.Format(@"urn:{0}:AxiuUADiscovery", System.Net.Dns.GetHostName()), 23 | ApplicationType = ApplicationType.DiscoveryServer, 24 | ServerConfiguration = new ServerConfiguration() 25 | { 26 | BaseAddresses = { "opc.tcp://localhost:4840/" }, 27 | MinRequestThreadCount = 5, 28 | MaxRequestThreadCount = 100, 29 | MaxQueuedRequestCount = 200 30 | }, 31 | DiscoveryServerConfiguration = new DiscoveryServerConfiguration() 32 | { 33 | BaseAddresses = { "opc.tcp://localhost:4840/" }, 34 | ServerNames = { "OpcuaDiscovery" } 35 | }, 36 | SecurityConfiguration = new SecurityConfiguration 37 | { 38 | ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", SubjectName = Utils.Format(@"CN={0}, DC={1}", "AxiuOpcua", System.Net.Dns.GetHostName()) }, 39 | TrustedIssuerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" }, 40 | TrustedPeerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" }, 41 | RejectedCertificateStore = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" }, 42 | AutoAcceptUntrustedCertificates = true, 43 | AddAppCertToTrustedStore = true 44 | }, 45 | TransportConfigurations = new TransportConfigurationCollection(), 46 | TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, 47 | ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }, 48 | TraceConfiguration = new TraceConfiguration() 49 | }; 50 | config.Validate(ApplicationType.DiscoveryServer).GetAwaiter().GetResult(); 51 | if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates) 52 | { 53 | config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted); }; 54 | } 55 | 56 | var application = new ApplicationInstance 57 | { 58 | ApplicationName = "Axiu UA Discovery", 59 | ApplicationType = ApplicationType.DiscoveryServer, 60 | ApplicationConfiguration = config 61 | }; 62 | //application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult(); 63 | bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result; 64 | if (!certOk) 65 | { 66 | Console.WriteLine("证书验证失败!"); 67 | } 68 | 69 | var server = new DiscoveryServer(); 70 | // start the server. 71 | application.Start(server).Wait(); 72 | } 73 | catch (Exception ex) 74 | { 75 | Console.ForegroundColor = ConsoleColor.Red; 76 | Console.WriteLine("启动OPC-UA Discovery服务端触发异常:" + ex.Message); 77 | Console.ResetColor(); 78 | } 79 | } 80 | } 81 | 82 | /// 83 | /// 已注册服务列表 84 | /// 85 | /// 86 | /// 主要用于保存服务注册信息,客户端获取列表时将对应的信息返回给客户端 87 | /// 88 | public class RegisteredServerTable 89 | { 90 | public string ServerUri { get; set; } 91 | 92 | public string ProductUri { get; set; } 93 | 94 | public LocalizedTextCollection ServerNames { get; set; } 95 | 96 | public ApplicationType ServerType { get; set; } 97 | 98 | public string GatewayServerUri { get; set; } 99 | 100 | public StringCollection DiscoveryUrls { get; set; } 101 | 102 | public string SemaphoreFilePath { get; set; } 103 | 104 | public bool IsOnline { get; set; } 105 | 106 | /// 107 | /// 最后一次注册时间 108 | /// 109 | public DateTime LastRegistered { get; set; } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Service/AxiuOpcuaServer.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua; 2 | using Opc.Ua.Server; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Security.Cryptography.X509Certificates; 6 | using System.Text; 7 | 8 | namespace Axiu.Opcua.Demo.Service 9 | { 10 | public class AxiuOpcuaServer : StandardServer 11 | { 12 | #region Overridden Methods 13 | /// 14 | /// Creates the node managers for the server. 15 | /// 16 | /// 17 | /// This method allows the sub-class create any additional node managers which it uses. The SDK 18 | /// always creates a CoreNodeManager which handles the built-in nodes defined by the specification. 19 | /// Any additional NodeManagers are expected to handle application specific nodes. 20 | /// 21 | protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration) 22 | { 23 | Utils.Trace("Creating the Node Managers."); 24 | 25 | List nodeManagers = new List(); 26 | 27 | // create the custom node managers. 28 | nodeManagers.Add(new AxiuNodeManager(server, configuration)); 29 | 30 | // create master node manager. 31 | return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray()); 32 | } 33 | 34 | /// 35 | /// Loads the non-configurable properties for the application. 36 | /// 37 | /// 38 | /// These properties are exposed by the server but cannot be changed by administrators. 39 | /// 40 | protected override ServerProperties LoadServerProperties() 41 | { 42 | ServerProperties properties = new ServerProperties(); 43 | 44 | properties.ManufacturerName = "OPC Foundation"; 45 | properties.ProductName = "Quickstart Reference Server"; 46 | properties.ProductUri = "http://opcfoundation.org/Quickstart/ReferenceServer/v1.04"; 47 | properties.SoftwareVersion = Utils.GetAssemblySoftwareVersion(); 48 | properties.BuildNumber = Utils.GetAssemblyBuildNumber(); 49 | properties.BuildDate = Utils.GetAssemblyTimestamp(); 50 | 51 | return properties; 52 | } 53 | 54 | /// 55 | /// Creates the resource manager for the server. 56 | /// 57 | protected override ResourceManager CreateResourceManager(IServerInternal server, ApplicationConfiguration configuration) 58 | { 59 | ResourceManager resourceManager = new ResourceManager(server, configuration); 60 | 61 | System.Reflection.FieldInfo[] fields = typeof(StatusCodes).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); 62 | 63 | foreach (System.Reflection.FieldInfo field in fields) 64 | { 65 | uint? id = field.GetValue(typeof(StatusCodes)) as uint?; 66 | 67 | if (id != null) 68 | { 69 | resourceManager.Add(id.Value, "en-US", field.Name); 70 | } 71 | } 72 | 73 | return resourceManager; 74 | } 75 | 76 | /// 77 | /// Initializes the server before it starts up. 78 | /// 79 | /// 80 | /// This method is called before any startup processing occurs. The sub-class may update the 81 | /// configuration object or do any other application specific startup tasks. 82 | /// 83 | protected override void OnServerStarting(ApplicationConfiguration configuration) 84 | { 85 | Utils.Trace("The server is starting."); 86 | 87 | base.OnServerStarting(configuration); 88 | 89 | // it is up to the application to decide how to validate user identity tokens. 90 | // this function creates validator for X509 identity tokens. 91 | CreateUserIdentityValidators(configuration); 92 | } 93 | 94 | /// 95 | /// Called after the server has been started. 96 | /// 97 | protected override void OnServerStarted(IServerInternal server) 98 | { 99 | base.OnServerStarted(server); 100 | 101 | // request notifications when the user identity is changed. all valid users are accepted by default. 102 | server.SessionManager.ImpersonateUser += new ImpersonateEventHandler(SessionManager_ImpersonateUser); 103 | 104 | try 105 | { 106 | // allow a faster sampling interval for CurrentTime node. 107 | server.Status.Variable.CurrentTime.MinimumSamplingInterval = 250; 108 | } 109 | catch 110 | { } 111 | 112 | } 113 | 114 | #endregion 115 | #region User Validation Functions 116 | /// 117 | /// Creates the objects used to validate the user identity tokens supported by the server. 118 | /// 119 | private void CreateUserIdentityValidators(ApplicationConfiguration configuration) 120 | { 121 | for (int ii = 0; ii < configuration.ServerConfiguration.UserTokenPolicies.Count; ii++) 122 | { 123 | UserTokenPolicy policy = configuration.ServerConfiguration.UserTokenPolicies[ii]; 124 | 125 | // create a validator for a certificate token policy. 126 | if (policy.TokenType == UserTokenType.Certificate) 127 | { 128 | // check if user certificate trust lists are specified in configuration. 129 | if (configuration.SecurityConfiguration.TrustedUserCertificates != null && 130 | configuration.SecurityConfiguration.UserIssuerCertificates != null) 131 | { 132 | CertificateValidator certificateValidator = new CertificateValidator(); 133 | certificateValidator.Update(configuration.SecurityConfiguration).Wait(); 134 | certificateValidator.Update(configuration.SecurityConfiguration.UserIssuerCertificates, 135 | configuration.SecurityConfiguration.TrustedUserCertificates, 136 | configuration.SecurityConfiguration.RejectedCertificateStore); 137 | 138 | // set custom validator for user certificates. 139 | m_userCertificateValidator = certificateValidator.GetChannelValidator(); 140 | } 141 | } 142 | } 143 | } 144 | 145 | /// 146 | /// Called when a client tries to change its user identity. 147 | /// 148 | private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args) 149 | { 150 | // check for a user name token. 151 | UserNameIdentityToken userNameToken = args.NewIdentity as UserNameIdentityToken; 152 | 153 | if (userNameToken != null) 154 | { 155 | args.Identity = VerifyPassword(userNameToken); 156 | 157 | // set AuthenticatedUser role for accepted user/password authentication 158 | args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser); 159 | 160 | if (args.Identity is SystemConfigurationIdentity) 161 | { 162 | // set ConfigureAdmin role for user with permission to configure server 163 | args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_ConfigureAdmin); 164 | args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_SecurityAdmin); 165 | } 166 | 167 | return; 168 | } 169 | 170 | // check for x509 user token. 171 | X509IdentityToken x509Token = args.NewIdentity as X509IdentityToken; 172 | 173 | if (x509Token != null) 174 | { 175 | VerifyUserTokenCertificate(x509Token.Certificate); 176 | args.Identity = new UserIdentity(x509Token); 177 | Utils.Trace("X509 Token Accepted: {0}", args.Identity.DisplayName); 178 | 179 | // set AuthenticatedUser role for accepted certificate authentication 180 | args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser); 181 | 182 | return; 183 | } 184 | 185 | // allow anonymous authentication and set Anonymous role for this authentication 186 | args.Identity = new UserIdentity(); 187 | args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_Anonymous); 188 | } 189 | 190 | /// 191 | /// Validates the password for a username token. 192 | /// 193 | private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken) 194 | { 195 | var userName = userNameToken.UserName; 196 | var password = userNameToken.DecryptedPassword; 197 | if (String.IsNullOrEmpty(userName)) 198 | { 199 | // an empty username is not accepted. 200 | throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid, 201 | "Security token is not a valid username token. An empty username is not accepted."); 202 | } 203 | 204 | if (String.IsNullOrEmpty(password)) 205 | { 206 | // an empty password is not accepted. 207 | throw ServiceResultException.Create(StatusCodes.BadIdentityTokenRejected, 208 | "Security token is not a valid username token. An empty password is not accepted."); 209 | } 210 | 211 | // User with permission to configure server 212 | if (userName == "sysadmin" && password == "demo") 213 | { 214 | return new SystemConfigurationIdentity(new UserIdentity(userNameToken)); 215 | } 216 | 217 | // standard users for CTT verification 218 | if (!((userName == "user1" && password == "password") || 219 | (userName == "user2" && password == "password1"))) 220 | { 221 | // construct translation object with default text. 222 | TranslationInfo info = new TranslationInfo( 223 | "InvalidPassword", 224 | "en-US", 225 | "Invalid username or password.", 226 | userName); 227 | 228 | // create an exception with a vendor defined sub-code. 229 | throw new ServiceResultException(new ServiceResult( 230 | StatusCodes.BadUserAccessDenied, 231 | "InvalidPassword", 232 | LoadServerProperties().ProductUri, 233 | new LocalizedText(info))); 234 | } 235 | 236 | return new UserIdentity(userNameToken); 237 | } 238 | 239 | /// 240 | /// Verifies that a certificate user token is trusted. 241 | /// 242 | private void VerifyUserTokenCertificate(X509Certificate2 certificate) 243 | { 244 | try 245 | { 246 | if (m_userCertificateValidator != null) 247 | { 248 | m_userCertificateValidator.Validate(certificate); 249 | } 250 | else 251 | { 252 | CertificateValidator.Validate(certificate); 253 | } 254 | } 255 | catch (Exception e) 256 | { 257 | TranslationInfo info; 258 | StatusCode result = StatusCodes.BadIdentityTokenRejected; 259 | ServiceResultException se = e as ServiceResultException; 260 | if (se != null && se.StatusCode == StatusCodes.BadCertificateUseNotAllowed) 261 | { 262 | info = new TranslationInfo( 263 | "InvalidCertificate", 264 | "en-US", 265 | "'{0}' is an invalid user certificate.", 266 | certificate.Subject); 267 | 268 | result = StatusCodes.BadIdentityTokenInvalid; 269 | } 270 | else 271 | { 272 | // construct translation object with default text. 273 | info = new TranslationInfo( 274 | "UntrustedCertificate", 275 | "en-US", 276 | "'{0}' is not a trusted user certificate.", 277 | certificate.Subject); 278 | } 279 | 280 | // create an exception with a vendor defined sub-code. 281 | throw new ServiceResultException(new ServiceResult( 282 | result, 283 | info.Key, 284 | LoadServerProperties().ProductUri, 285 | new LocalizedText(info))); 286 | } 287 | } 288 | #endregion 289 | 290 | #region Private Fields 291 | private ICertificateValidator m_userCertificateValidator; 292 | #endregion 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Service/AxiuNodeManager.cs: -------------------------------------------------------------------------------- 1 | using Axiu.Opcua.Demo.Common; 2 | using Axiu.Opcua.Demo.Model; 3 | using Opc.Ua; 4 | using Opc.Ua.Server; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Axiu.Opcua.Demo.Service 13 | { 14 | /// 15 | /// 以下备注中 测点即代表最叶子级节点 16 | /// 目前设计是 只有测点有数据 其余节点都是目录 17 | /// 18 | public class AxiuNodeManager : CustomNodeManager2 19 | { 20 | /// 21 | /// 配置修改次数 主要用来识别菜单树是否有变动 如果发生变动则修改菜单树对应节点 测点的实时数据变化不算在内 22 | /// 23 | private int cfgCount = -1; 24 | private IList _references; 25 | /// 26 | /// 测点集合,实时数据刷新时,直接从字典中取出对应的测点,修改值即可 27 | /// 28 | private Dictionary _nodeDic = new Dictionary(); 29 | /// 30 | /// 目录集合,修改菜单树时需要(我们需要知道哪些菜单需要修改,哪些需要新增,哪些需要删除) 31 | /// 32 | private Dictionary _folderDic = new Dictionary(); 33 | 34 | public AxiuNodeManager(IServerInternal server, ApplicationConfiguration configuration) : base(server, configuration, "http://opcfoundation.org/Quickstarts/ReferenceApplications") 35 | { 36 | } 37 | 38 | /// 39 | /// 重写NodeId生成方式(目前采用'_'分隔,如需更改,请修改此方法) 40 | /// 41 | /// 42 | /// 43 | /// 44 | public override NodeId New(ISystemContext context, NodeState node) 45 | { 46 | BaseInstanceState instance = node as BaseInstanceState; 47 | 48 | if (instance != null && instance.Parent != null) 49 | { 50 | string id = instance.Parent.NodeId.Identifier as string; 51 | 52 | if (id != null) 53 | { 54 | return new NodeId(id + "_" + instance.SymbolicName, instance.Parent.NodeId.NamespaceIndex); 55 | } 56 | } 57 | 58 | return node.NodeId; 59 | } 60 | 61 | /// 62 | /// 重写获取节点句柄的方法 63 | /// 64 | /// 65 | /// 66 | /// 67 | /// 68 | protected override NodeHandle GetManagerHandle(ServerSystemContext context, NodeId nodeId, IDictionary cache) 69 | { 70 | lock (Lock) 71 | { 72 | // quickly exclude nodes that are not in the namespace. 73 | if (!IsNodeIdInNamespace(nodeId)) 74 | { 75 | return null; 76 | } 77 | 78 | NodeState node = null; 79 | 80 | if (!PredefinedNodes.TryGetValue(nodeId, out node)) 81 | { 82 | return null; 83 | } 84 | 85 | NodeHandle handle = new NodeHandle(); 86 | 87 | handle.NodeId = nodeId; 88 | handle.Node = node; 89 | handle.Validated = true; 90 | 91 | return handle; 92 | } 93 | } 94 | 95 | /// 96 | /// 重写节点的验证方式 97 | /// 98 | /// 99 | /// 100 | /// 101 | /// 102 | protected override NodeState ValidateNode(ServerSystemContext context, NodeHandle handle, IDictionary cache) 103 | { 104 | // not valid if no root. 105 | if (handle == null) 106 | { 107 | return null; 108 | } 109 | 110 | // check if previously validated. 111 | if (handle.Validated) 112 | { 113 | return handle.Node; 114 | } 115 | // TBD 116 | return null; 117 | } 118 | 119 | /// 120 | /// 重写创建基础目录 121 | /// 122 | /// 123 | public override void CreateAddressSpace(IDictionary> externalReferences) 124 | { 125 | lock (Lock) 126 | { 127 | if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out _references)) 128 | { 129 | externalReferences[ObjectIds.ObjectsFolder] = _references = new List(); 130 | } 131 | 132 | try 133 | { 134 | //TODO: 获取节点树 135 | List nodes = new List() 136 | { 137 | /* 138 | * OpcuaNode类由于个人业务相关 定义成如下格式, 139 | * 可根据自身数据便利 修改相应的数据结构 140 | * 只需保证能明确知道各个节点的从属关系即可 141 | */ 142 | new OpcuaNode(){NodeId=1,NodeName="模拟根节点",NodePath="1",NodeType=NodeType.Scada,ParentPath="",IsTerminal=false }, 143 | new OpcuaNode(){NodeId=11,NodeName="子目录1",NodePath="11",NodeType=NodeType.Channel,ParentPath="1",IsTerminal=false }, 144 | new OpcuaNode(){NodeId=12,NodeName="子目录2",NodePath="12",NodeType=NodeType.Device,ParentPath="1",IsTerminal=false }, 145 | new OpcuaNode(){NodeId=111,NodeName="叶子节点1",NodePath="111",NodeType=NodeType.Measure,ParentPath="11", IsTerminal=true }, 146 | new OpcuaNode(){NodeId=112,NodeName="叶子节点2",NodePath="112",NodeType=NodeType.Measure,ParentPath="11",IsTerminal=true }, 147 | new OpcuaNode(){NodeId=113,NodeName="叶子节点3",NodePath="113",NodeType=NodeType.Measure,ParentPath="11",IsTerminal=true }, 148 | new OpcuaNode(){NodeId=114,NodeName="叶子节点4",NodePath="114",NodeType=NodeType.Measure,ParentPath="11",IsTerminal=true }, 149 | new OpcuaNode(){NodeId=121,NodeName="叶子节点1",NodePath="121",NodeType=NodeType.Measure,ParentPath="12",IsTerminal=true }, 150 | new OpcuaNode(){NodeId=122,NodeName="叶子节点2",NodePath="122",NodeType=NodeType.Measure,ParentPath="12",IsTerminal=true } 151 | }; 152 | //开始创建节点的菜单树 153 | GeneraterNodes(nodes, _references); 154 | //实时更新测点的数据 155 | UpdateVariableValue(); 156 | } 157 | catch (Exception ex) 158 | { 159 | Console.ForegroundColor = ConsoleColor.Red; 160 | Console.WriteLine("调用接口初始化触发异常:" + ex.Message); 161 | Console.ResetColor(); 162 | } 163 | } 164 | } 165 | 166 | /// 167 | /// 生成根节点(由于根节点需要特殊处理,此处单独出来一个方法) 168 | /// 169 | /// 170 | /// 171 | private void GeneraterNodes(List nodes, IList references) 172 | { 173 | var list = nodes.Where(d => d.NodeType == NodeType.Scada); 174 | foreach (var item in list) 175 | { 176 | try 177 | { 178 | FolderState root = CreateFolder(null, item.NodePath, item.NodeName); 179 | root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); 180 | references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, root.NodeId)); 181 | root.EventNotifier = EventNotifiers.SubscribeToEvents; 182 | AddRootNotifier(root); 183 | CreateNodes(nodes, root, item.NodePath); 184 | _folderDic.Add(item.NodePath, root); 185 | //添加引用关系 186 | AddPredefinedNode(SystemContext, root); 187 | } 188 | catch (Exception ex) 189 | { 190 | Console.ForegroundColor = ConsoleColor.Red; 191 | Console.WriteLine("创建OPC-UA根节点触发异常:" + ex.Message); 192 | Console.ResetColor(); 193 | } 194 | } 195 | } 196 | 197 | /// 198 | /// 递归创建子节点(包括创建目录和测点) 199 | /// 200 | /// 201 | /// 202 | private void CreateNodes(List nodes, FolderState parent, string parentPath) 203 | { 204 | var list = nodes.Where(d => d.ParentPath == parentPath); 205 | foreach (var node in list) 206 | { 207 | try 208 | { 209 | if (!node.IsTerminal) 210 | { 211 | FolderState folder = CreateFolder(parent, node.NodePath, node.NodeName); 212 | _folderDic.Add(node.NodePath, folder); 213 | CreateNodes(nodes, folder, node.NodePath); 214 | } 215 | else 216 | { 217 | BaseDataVariableState variable = CreateVariable(parent, node.NodePath, node.NodeName, DataTypeIds.Double, ValueRanks.Scalar); 218 | //此处需要注意 目录字典是以目录路径作为KEY 而 测点字典是以测点ID作为KEY 为了方便更新实时数据 219 | _nodeDic.Add(node.NodeId.ToString(), variable); 220 | } 221 | } 222 | catch (Exception ex) 223 | { 224 | Console.ForegroundColor = ConsoleColor.Red; 225 | Console.WriteLine("创建OPC-UA子节点触发异常:" + ex.Message); 226 | Console.ResetColor(); 227 | } 228 | } 229 | } 230 | 231 | /// 232 | /// 创建目录 233 | /// 234 | /// 235 | /// 236 | /// 237 | /// 238 | private FolderState CreateFolder(NodeState parent, string path, string name) 239 | { 240 | FolderState folder = new FolderState(parent); 241 | 242 | folder.SymbolicName = name; 243 | folder.ReferenceTypeId = ReferenceTypes.Organizes; 244 | folder.TypeDefinitionId = ObjectTypeIds.FolderType; 245 | folder.NodeId = new NodeId(path, NamespaceIndex); 246 | folder.BrowseName = new QualifiedName(path, NamespaceIndex); 247 | folder.DisplayName = new LocalizedText("en", name); 248 | folder.WriteMask = AttributeWriteMask.None; 249 | folder.UserWriteMask = AttributeWriteMask.None; 250 | folder.EventNotifier = EventNotifiers.None; 251 | 252 | if (parent != null) 253 | { 254 | parent.AddChild(folder); 255 | } 256 | 257 | return folder; 258 | } 259 | 260 | /// 261 | /// 创建节点 262 | /// 263 | /// 264 | /// 265 | /// 266 | /// 267 | /// 268 | /// 269 | private BaseDataVariableState CreateVariable(NodeState parent, string path, string name, NodeId dataType, int valueRank) 270 | { 271 | BaseDataVariableState variable = new BaseDataVariableState(parent); 272 | 273 | variable.SymbolicName = name; 274 | variable.ReferenceTypeId = ReferenceTypes.Organizes; 275 | variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType; 276 | variable.NodeId = new NodeId(path, NamespaceIndex); 277 | variable.BrowseName = new QualifiedName(path, NamespaceIndex); 278 | variable.DisplayName = new LocalizedText("en", name); 279 | variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description; 280 | variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description; 281 | variable.DataType = dataType; 282 | variable.ValueRank = valueRank; 283 | variable.AccessLevel = AccessLevels.CurrentReadOrWrite; 284 | variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite; 285 | variable.Historizing = false; 286 | //variable.Value = GetNewValue(variable); 287 | variable.StatusCode = StatusCodes.Good; 288 | variable.Timestamp = DateTime.Now; 289 | variable.OnWriteValue = OnWriteDataValue; 290 | 291 | if (valueRank == ValueRanks.OneDimension) 292 | { 293 | variable.ArrayDimensions = new ReadOnlyList(new List { 0 }); 294 | } 295 | else if (valueRank == ValueRanks.TwoDimensions) 296 | { 297 | variable.ArrayDimensions = new ReadOnlyList(new List { 0, 0 }); 298 | } 299 | 300 | if (parent != null) 301 | { 302 | parent.AddChild(variable); 303 | } 304 | 305 | return variable; 306 | } 307 | 308 | /// 309 | /// 实时更新节点数据 310 | /// 311 | public void UpdateVariableValue() 312 | { 313 | Task.Run(() => 314 | { 315 | while (true) 316 | { 317 | try 318 | { 319 | /* 320 | * 此处仅作示例代码 所以不修改节点树 故将UpdateNodesAttribute()方法跳过 321 | * 在实际业务中 请根据自身的业务需求决定何时修改节点菜单树 322 | */ 323 | int count = 0; 324 | //配置发生更改时,重新生成节点树 325 | if (count > 0 && count != cfgCount) 326 | { 327 | cfgCount = count; 328 | List nodes = new List(); 329 | /* 330 | * 此处有想过删除整个菜单树,然后重建 保证各个NodeId仍与原来的一直 331 | * 但是 后来发现这样会导致原来的客户端订阅信息丢失 无法获取订阅数据 332 | * 所以 只能一级级的检查节点 然后修改属性 333 | */ 334 | UpdateNodesAttribute(nodes); 335 | } 336 | //模拟获取实时数据 337 | BaseDataVariableState node = null; 338 | /* 339 | * 在实际业务中应该是根据对应的标识来更新固定节点的数据 340 | * 这里 我偷个懒 全部测点都更新为一个新的随机数 341 | */ 342 | foreach (var item in _nodeDic) 343 | { 344 | node = item.Value; 345 | node.Value = RandomLibrary.GetRandomInt(0, 99); 346 | node.Timestamp = DateTime.Now; 347 | //变更标识 只有执行了这一步,订阅的客户端才会收到新的数据 348 | node.ClearChangeMasks(SystemContext, false); 349 | } 350 | //1秒更新一次 351 | Thread.Sleep(1000 * 1); 352 | } 353 | catch (Exception ex) 354 | { 355 | Console.ForegroundColor = ConsoleColor.Red; 356 | Console.WriteLine("更新OPC-UA节点数据触发异常:" + ex.Message); 357 | Console.ResetColor(); 358 | } 359 | } 360 | }); 361 | } 362 | 363 | /// 364 | /// 修改节点树(添加节点,删除节点,修改节点名称) 365 | /// 366 | /// 367 | public void UpdateNodesAttribute(List nodes) 368 | { 369 | //修改或创建根节点 370 | var scadas = nodes.Where(d => d.NodeType == NodeType.Scada); 371 | foreach (var item in scadas) 372 | { 373 | FolderState scadaNode = null; 374 | if (!_folderDic.TryGetValue(item.NodePath, out scadaNode)) 375 | { 376 | //如果根节点都不存在 那么整个树都需要创建 377 | FolderState root = CreateFolder(null, item.NodePath, item.NodeName); 378 | root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); 379 | _references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, root.NodeId)); 380 | root.EventNotifier = EventNotifiers.SubscribeToEvents; 381 | AddRootNotifier(root); 382 | CreateNodes(nodes, root, item.NodePath); 383 | _folderDic.Add(item.NodePath, root); 384 | AddPredefinedNode(SystemContext, root); 385 | continue; 386 | } 387 | else 388 | { 389 | scadaNode.DisplayName = item.NodeName; 390 | scadaNode.ClearChangeMasks(SystemContext, false); 391 | } 392 | } 393 | //修改或创建目录(此处设计为可以有多级目录,上面是演示数据,所以我只写了三级,事实上更多级也是可以的) 394 | var folders = nodes.Where(d => d.NodeType != NodeType.Scada && !d.IsTerminal); 395 | foreach (var item in folders) 396 | { 397 | FolderState folder = null; 398 | if (!_folderDic.TryGetValue(item.NodePath, out folder)) 399 | { 400 | var par = GetParentFolderState(nodes, item); 401 | folder = CreateFolder(par, item.NodePath, item.NodeName); 402 | AddPredefinedNode(SystemContext, folder); 403 | par.ClearChangeMasks(SystemContext, false); 404 | _folderDic.Add(item.NodePath, folder); 405 | } 406 | else 407 | { 408 | folder.DisplayName = item.NodeName; 409 | folder.ClearChangeMasks(SystemContext, false); 410 | } 411 | } 412 | //修改或创建测点 413 | //这里我的数据结构采用IsTerminal来代表是否是测点 实际业务中可能需要根据自身需要调整 414 | var paras = nodes.Where(d => d.IsTerminal); 415 | foreach (var item in paras) 416 | { 417 | BaseDataVariableState node = null; 418 | if (_nodeDic.TryGetValue(item.NodeId.ToString(), out node)) 419 | { 420 | node.DisplayName = item.NodeName; 421 | node.Timestamp = DateTime.Now; 422 | node.ClearChangeMasks(SystemContext, false); 423 | } 424 | else 425 | { 426 | FolderState folder = null; 427 | if (_folderDic.TryGetValue(item.ParentPath, out folder)) 428 | { 429 | node = CreateVariable(folder, item.NodePath, item.NodeName, DataTypeIds.Double, ValueRanks.Scalar); 430 | AddPredefinedNode(SystemContext, node); 431 | folder.ClearChangeMasks(SystemContext, false); 432 | _nodeDic.Add(item.NodeId.ToString(), node); 433 | } 434 | } 435 | } 436 | 437 | /* 438 | * 将新获取到的菜单列表与原列表对比 439 | * 如果新菜单列表中不包含原有的菜单 440 | * 则说明这个菜单被删除了 这里也需要删除 441 | */ 442 | List folderPath = _folderDic.Keys.ToList(); 443 | List nodePath = _nodeDic.Keys.ToList(); 444 | var remNode = nodePath.Except(nodes.Where(d => d.IsTerminal).Select(d => d.NodeId.ToString())); 445 | foreach (var str in remNode) 446 | { 447 | BaseDataVariableState node = null; 448 | if (_nodeDic.TryGetValue(str, out node)) 449 | { 450 | var parent = node.Parent; 451 | parent.RemoveChild(node); 452 | _nodeDic.Remove(str); 453 | } 454 | } 455 | var remFolder = folderPath.Except(nodes.Where(d => !d.IsTerminal).Select(d => d.NodePath)); 456 | foreach (string str in remFolder) 457 | { 458 | FolderState folder = null; 459 | if (_folderDic.TryGetValue(str, out folder)) 460 | { 461 | var parent = folder.Parent; 462 | if (parent != null) 463 | { 464 | parent.RemoveChild(folder); 465 | _folderDic.Remove(str); 466 | } 467 | else 468 | { 469 | RemoveRootNotifier(folder); 470 | RemovePredefinedNode(SystemContext, folder, new List()); 471 | } 472 | } 473 | } 474 | } 475 | 476 | /// 477 | /// 创建父级目录(请确保对应的根目录已创建) 478 | /// 479 | /// 480 | /// 481 | /// 482 | public FolderState GetParentFolderState(IEnumerable nodes, OpcuaNode currentNode) 483 | { 484 | FolderState folder = null; 485 | if (!_folderDic.TryGetValue(currentNode.ParentPath, out folder)) 486 | { 487 | var parent = nodes.Where(d => d.NodePath == currentNode.ParentPath).FirstOrDefault(); 488 | if (!string.IsNullOrEmpty(parent.ParentPath)) 489 | { 490 | var pFol = GetParentFolderState(nodes, parent); 491 | folder = CreateFolder(pFol, parent.NodePath, parent.NodeName); 492 | pFol.ClearChangeMasks(SystemContext, false); 493 | AddPredefinedNode(SystemContext, folder); 494 | _folderDic.Add(currentNode.ParentPath, folder); 495 | } 496 | } 497 | return folder; 498 | } 499 | 500 | /// 501 | /// 客户端写入值时触发(绑定到节点的写入事件上) 502 | /// 503 | /// 504 | /// 505 | /// 506 | /// 507 | /// 508 | /// 509 | /// 510 | /// 511 | private ServiceResult OnWriteDataValue(ISystemContext context, NodeState node, NumericRange indexRange, QualifiedName dataEncoding, 512 | ref object value, ref StatusCode statusCode, ref DateTime timestamp) 513 | { 514 | BaseDataVariableState variable = node as BaseDataVariableState; 515 | try 516 | { 517 | //验证数据类型 518 | TypeInfo typeInfo = TypeInfo.IsInstanceOfDataType( 519 | value, 520 | variable.DataType, 521 | variable.ValueRank, 522 | context.NamespaceUris, 523 | context.TypeTable); 524 | 525 | if (typeInfo == null || typeInfo == TypeInfo.Unknown) 526 | { 527 | return StatusCodes.BadTypeMismatch; 528 | } 529 | if (typeInfo.BuiltInType == BuiltInType.Double) 530 | { 531 | double number = Convert.ToDouble(value); 532 | value = TypeInfo.Cast(number, typeInfo.BuiltInType); 533 | } 534 | return ServiceResult.Good; 535 | } 536 | catch (Exception) 537 | { 538 | return StatusCodes.BadTypeMismatch; 539 | } 540 | } 541 | 542 | /// 543 | /// 读取历史数据 544 | /// 545 | /// 546 | /// 547 | /// 548 | /// 549 | /// 550 | /// 551 | /// 552 | public override void HistoryRead(OperationContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, 553 | IList nodesToRead, IList results, IList errors) 554 | { 555 | ReadProcessedDetails readDetail = details as ReadProcessedDetails; 556 | //假设查询历史数据 都是带上时间范围的 557 | if (readDetail == null || readDetail.StartTime == DateTime.MinValue || readDetail.EndTime == DateTime.MinValue) 558 | { 559 | errors[0] = StatusCodes.BadHistoryOperationUnsupported; 560 | return; 561 | } 562 | for (int ii = 0; ii < nodesToRead.Count; ii++) 563 | { 564 | int sss = readDetail.StartTime.Millisecond; 565 | double res = sss + DateTime.Now.Millisecond; 566 | //这里 返回的历史数据可以是多种数据类型 请根据实际的业务来选择 567 | Opc.Ua.KeyValuePair keyValue = new Opc.Ua.KeyValuePair() 568 | { 569 | Key = new QualifiedName(nodesToRead[ii].NodeId.Identifier.ToString()), 570 | Value = res 571 | }; 572 | results[ii] = new HistoryReadResult() 573 | { 574 | StatusCode = StatusCodes.Good, 575 | HistoryData = new ExtensionObject(keyValue) 576 | }; 577 | errors[ii] = StatusCodes.Good; 578 | //切记,如果你已处理完了读取历史数据的操作,请将Processed设为true,这样OPC-UA类库就知道你已经处理过了 不需要再进行检查了 579 | nodesToRead[ii].Processed = true; 580 | } 581 | } 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /Axiu.Opcua.Demo.Service/DiscoveryServer.cs: -------------------------------------------------------------------------------- 1 | using Opc.Ua; 2 | using Opc.Ua.Server; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Axiu.Opcua.Demo.Service 13 | { 14 | public class DiscoveryServer : StandardServer, IDiscoveryServer 15 | { 16 | private ServerInternalData m_serverInternal; 17 | /// 18 | /// 清理掉线服务端定时器 19 | /// 20 | private static Timer m_timer = null; 21 | /// 22 | /// 已注册服务端列表 23 | /// 24 | List _serverTable = new List(); 25 | 26 | /// 27 | /// 加载服务属性(这里采用直接写死,不用配置文件的方式) 28 | /// 29 | /// 30 | protected override ServerProperties LoadServerProperties() 31 | { 32 | ServerProperties properties = new ServerProperties(); 33 | 34 | properties.ManufacturerName = "OPC Foundation"; 35 | properties.ProductName = "CET Discovery Server"; 36 | properties.ProductUri = "http://opcfoundation.org/Quickstart/ReferenceServer/v1.04"; 37 | properties.SoftwareVersion = Utils.GetAssemblySoftwareVersion(); 38 | properties.BuildNumber = Utils.GetAssemblyBuildNumber(); 39 | properties.BuildDate = Utils.GetAssemblyTimestamp(); 40 | 41 | return properties; 42 | } 43 | 44 | /// 45 | /// 获取终结点实例(此方法必须重写,Discovery服务端必须创建DiscoveryEndpoint终结点,而实时数据服务端必须创建SessionEndpoint终结点) 46 | /// 47 | /// 48 | /// 49 | protected override EndpointBase GetEndpointInstance(ServerBase server) 50 | { 51 | return new DiscoveryEndpoint(server);//SessionEndpoint 52 | } 53 | 54 | /// 55 | /// 启动服务 56 | /// 57 | /// 58 | protected override void StartApplication(ApplicationConfiguration configuration) 59 | { 60 | lock (m_lock) 61 | { 62 | try 63 | { 64 | // create the datastore for the instance. 65 | m_serverInternal = new ServerInternalData( 66 | ServerProperties, 67 | configuration, 68 | MessageContext, 69 | new CertificateValidator(), 70 | InstanceCertificate); 71 | 72 | // create the manager responsible for providing localized string resources. 73 | ResourceManager resourceManager = CreateResourceManager(m_serverInternal, configuration); 74 | 75 | // create the manager responsible for incoming requests. 76 | RequestManager requestManager = new RequestManager(m_serverInternal); 77 | 78 | // create the master node manager. 79 | MasterNodeManager masterNodeManager = new MasterNodeManager(m_serverInternal, configuration, null); 80 | 81 | // add the node manager to the datastore. 82 | m_serverInternal.SetNodeManager(masterNodeManager); 83 | 84 | // put the node manager into a state that allows it to be used by other objects. 85 | masterNodeManager.Startup(); 86 | 87 | // create the manager responsible for handling events. 88 | EventManager eventManager = new EventManager(m_serverInternal, (uint)configuration.ServerConfiguration.MaxEventQueueSize); 89 | 90 | // creates the server object. 91 | m_serverInternal.CreateServerObject( 92 | eventManager, 93 | resourceManager, 94 | requestManager); 95 | 96 | 97 | // create the manager responsible for aggregates. 98 | m_serverInternal.AggregateManager = CreateAggregateManager(m_serverInternal, configuration); 99 | 100 | // start the session manager. 101 | SessionManager sessionManager = new SessionManager(m_serverInternal, configuration); 102 | sessionManager.Startup(); 103 | 104 | // start the subscription manager. 105 | SubscriptionManager subscriptionManager = new SubscriptionManager(m_serverInternal, configuration); 106 | subscriptionManager.Startup(); 107 | 108 | // add the session manager to the datastore. 109 | m_serverInternal.SetSessionManager(sessionManager, subscriptionManager); 110 | 111 | ServerError = null; 112 | 113 | // set the server status as running. 114 | SetServerState(ServerState.Running); 115 | 116 | // monitor the configuration file. 117 | if (!String.IsNullOrEmpty(configuration.SourceFilePath)) 118 | { 119 | var m_configurationWatcher = new ConfigurationWatcher(configuration); 120 | m_configurationWatcher.Changed += new EventHandler(this.OnConfigurationChanged); 121 | } 122 | 123 | CertificateValidator.CertificateUpdate += OnCertificateUpdate; 124 | //60s后开始清理过期服务列表,此后每60s检查一次 125 | m_timer = new Timer(ClearNoliveServer, null, 60000, 60000); 126 | Console.WriteLine("Discovery服务已启动完成,请勿退出程序!!!"); 127 | } 128 | catch (Exception e) 129 | { 130 | Utils.Trace(e, "Unexpected error starting application"); 131 | m_serverInternal = null; 132 | ServiceResult error = ServiceResult.Create(e, StatusCodes.BadInternalError, "Unexpected error starting application"); 133 | ServerError = error; 134 | throw new ServiceResultException(error); 135 | } 136 | } 137 | } 138 | 139 | /// 140 | /// 初始化服务监听(此方法可以不重写) 141 | /// 142 | /// 143 | /// 144 | /// 145 | /// 146 | protected override IList InitializeServiceHosts( 147 | ApplicationConfiguration configuration, 148 | out ApplicationDescription serverDescription, 149 | out EndpointDescriptionCollection endpoints) 150 | { 151 | serverDescription = null; 152 | endpoints = null; 153 | 154 | Dictionary hosts = new Dictionary(); 155 | 156 | // ensure at least one security policy exists. 157 | if (configuration.ServerConfiguration.SecurityPolicies.Count == 0) 158 | { 159 | configuration.ServerConfiguration.SecurityPolicies.Add(new ServerSecurityPolicy()); 160 | } 161 | 162 | // ensure at least one user token policy exists. 163 | if (configuration.ServerConfiguration.UserTokenPolicies.Count == 0) 164 | { 165 | UserTokenPolicy userTokenPolicy = new UserTokenPolicy(); 166 | 167 | userTokenPolicy.TokenType = UserTokenType.Anonymous; 168 | userTokenPolicy.PolicyId = userTokenPolicy.TokenType.ToString(); 169 | 170 | configuration.ServerConfiguration.UserTokenPolicies.Add(userTokenPolicy); 171 | } 172 | 173 | // set server description. 174 | serverDescription = new ApplicationDescription(); 175 | 176 | serverDescription.ApplicationUri = configuration.ApplicationUri; 177 | serverDescription.ApplicationName = new LocalizedText("en-US", configuration.ApplicationName); 178 | serverDescription.ApplicationType = configuration.ApplicationType; 179 | serverDescription.ProductUri = configuration.ProductUri; 180 | serverDescription.DiscoveryUrls = GetDiscoveryUrls(); 181 | 182 | endpoints = new EndpointDescriptionCollection(); 183 | IList endpointsForHost = null; 184 | 185 | // create UA TCP host. 186 | endpointsForHost = CreateUaTcpServiceHost( 187 | hosts, 188 | configuration, 189 | configuration.ServerConfiguration.BaseAddresses, 190 | serverDescription, 191 | configuration.ServerConfiguration.SecurityPolicies); 192 | 193 | endpoints.InsertRange(0, endpointsForHost); 194 | 195 | // create HTTPS host. 196 | #if !NO_HTTPS 197 | endpointsForHost = CreateHttpsServiceHost( 198 | hosts, 199 | configuration, 200 | configuration.ServerConfiguration.BaseAddresses, 201 | serverDescription, 202 | configuration.ServerConfiguration.SecurityPolicies); 203 | 204 | endpoints.AddRange(endpointsForHost); 205 | #endif 206 | return new List(hosts.Values); 207 | } 208 | 209 | /// 210 | /// 发现服务(由客户端请求) 211 | /// 212 | /// 213 | /// 214 | /// 215 | /// 216 | /// 217 | /// 218 | public override ResponseHeader FindServers( 219 | RequestHeader requestHeader, 220 | string endpointUrl, 221 | StringCollection localeIds, 222 | StringCollection serverUris, 223 | out ApplicationDescriptionCollection servers) 224 | { 225 | servers = new ApplicationDescriptionCollection(); 226 | 227 | ValidateRequest(requestHeader); 228 | 229 | Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":请求查找服务..."); 230 | string hostName = Dns.GetHostName(); 231 | 232 | lock (_serverTable) 233 | { 234 | foreach (var item in _serverTable) 235 | { 236 | StringCollection urls = new StringCollection(); 237 | foreach (var url in item.DiscoveryUrls) 238 | { 239 | if (url.Contains("localhost")) 240 | { 241 | string str = url.Replace("localhost", hostName); 242 | urls.Add(str); 243 | } 244 | else 245 | { 246 | urls.Add(url); 247 | } 248 | } 249 | 250 | servers.Add(new ApplicationDescription() 251 | { 252 | ApplicationName = item.ServerNames.FirstOrDefault(), 253 | ApplicationType = item.ServerType, 254 | ApplicationUri = item.ServerUri, 255 | DiscoveryProfileUri = item.SemaphoreFilePath, 256 | DiscoveryUrls = urls, 257 | ProductUri = item.ProductUri, 258 | GatewayServerUri = item.GatewayServerUri 259 | }); 260 | } 261 | } 262 | 263 | return CreateResponse(requestHeader, StatusCodes.Good); 264 | } 265 | 266 | /// 267 | /// 获得终结点列表 268 | /// 269 | /// 270 | /// 271 | /// 272 | /// 273 | /// 274 | /// 275 | public override ResponseHeader GetEndpoints( 276 | RequestHeader requestHeader, 277 | string endpointUrl, 278 | StringCollection localeIds, 279 | StringCollection profileUris, 280 | out EndpointDescriptionCollection endpoints) 281 | { 282 | endpoints = null; 283 | 284 | ValidateRequest(requestHeader); 285 | 286 | try 287 | { 288 | lock (m_lock) 289 | { 290 | // filter by profile. 291 | IList baseAddresses = FilterByProfile(profileUris, BaseAddresses); 292 | 293 | // get the descriptions. 294 | Uri parsedEndpointUrl = Utils.ParseUri(endpointUrl); 295 | 296 | if (parsedEndpointUrl != null) 297 | { 298 | baseAddresses = FilterByEndpointUrl(parsedEndpointUrl, baseAddresses); 299 | } 300 | 301 | // check if nothing to do. 302 | if (baseAddresses.Count != 0) 303 | { 304 | // localize the application name if requested. 305 | LocalizedText applicationName = this.ServerDescription.ApplicationName; 306 | 307 | if (localeIds != null && localeIds.Count > 0) 308 | { 309 | applicationName = m_serverInternal.ResourceManager.Translate(localeIds, applicationName); 310 | } 311 | 312 | // translate the application description. 313 | ApplicationDescription application = TranslateApplicationDescription( 314 | parsedEndpointUrl, 315 | base.ServerDescription, 316 | baseAddresses, 317 | applicationName); 318 | 319 | // translate the endpoint descriptions. 320 | endpoints = TranslateEndpointDescriptions( 321 | parsedEndpointUrl, 322 | baseAddresses, 323 | this.Endpoints, 324 | application); 325 | } 326 | } 327 | return CreateResponse(requestHeader, StatusCodes.Good); 328 | } 329 | catch (Exception ex) 330 | { 331 | Console.ForegroundColor = ConsoleColor.Red; 332 | Console.WriteLine("获取终结点列表GetEndpoints()触发异常:" + ex.Message); 333 | Console.ResetColor(); 334 | } 335 | return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError); 336 | } 337 | 338 | /// 339 | /// 验证请求是否有效 340 | /// 341 | /// 342 | protected override void ValidateRequest(RequestHeader requestHeader) 343 | { 344 | // check for server error. 345 | ServiceResult error = ServerError; 346 | 347 | if (ServiceResult.IsBad(error)) 348 | { 349 | throw new ServiceResultException(error); 350 | } 351 | 352 | // check server state. 353 | ServerInternalData serverInternal = m_serverInternal; 354 | 355 | if (serverInternal == null || !serverInternal.IsRunning) 356 | { 357 | throw new ServiceResultException(StatusCodes.BadServerHalted); 358 | } 359 | 360 | if (requestHeader == null) 361 | { 362 | throw new ServiceResultException(StatusCodes.BadRequestHeaderInvalid); 363 | } 364 | } 365 | 366 | /// 367 | /// 发现网络中的服务端(主要为其他PC端获取本机的服务列表使用) 368 | /// 369 | /// 370 | /// 371 | /// 372 | /// 373 | /// 374 | /// 375 | /// 376 | public override ResponseHeader FindServersOnNetwork( 377 | RequestHeader requestHeader, 378 | uint startingRecordId, 379 | uint maxRecordsToReturn, 380 | StringCollection serverCapabilityFilter, 381 | out DateTime lastCounterResetTime, 382 | out ServerOnNetworkCollection servers) 383 | { 384 | lastCounterResetTime = DateTime.MinValue; 385 | servers = null; 386 | 387 | ValidateRequest(requestHeader); 388 | 389 | try 390 | { 391 | lock (_serverTable) 392 | { 393 | uint rId = 0; 394 | foreach (var item in _serverTable) 395 | { 396 | rId++; 397 | servers.Add(new ServerOnNetwork() 398 | { 399 | DiscoveryUrl = item.DiscoveryUrls.FirstOrDefault(), 400 | RecordId = rId, 401 | ServerName = item.ServerNames.FirstOrDefault()?.Text, 402 | ServerCapabilities = { "Realtime Data", "Events & Alarms" }, 403 | }); 404 | } 405 | } 406 | 407 | lastCounterResetTime = DateTime.Now; 408 | return CreateResponse(requestHeader, StatusCodes.Good); 409 | } 410 | catch (Exception ex) 411 | { 412 | Console.ForegroundColor = ConsoleColor.Red; 413 | Console.WriteLine("查找网络中的服务FindServersOnNetwork()时触发异常:" + ex.Message); 414 | Console.ResetColor(); 415 | } 416 | return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError); 417 | } 418 | 419 | /// 420 | /// 注册服务(每个在本机启动的服务端都会主动调用此方法进行注册) 421 | /// 422 | /// 423 | /// 424 | /// 425 | public virtual ResponseHeader RegisterServer( 426 | RequestHeader requestHeader, 427 | RegisteredServer server) 428 | { 429 | ValidateRequest(requestHeader); 430 | 431 | // Insert implementation. 432 | try 433 | { 434 | Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":服务注册:" + server.DiscoveryUrls.FirstOrDefault()); 435 | RegisteredServerTable model = _serverTable.Where(d => d.ServerUri == server.ServerUri).FirstOrDefault(); 436 | if (model != null) 437 | { 438 | model.LastRegistered = DateTime.Now; 439 | } 440 | else 441 | { 442 | model = new RegisteredServerTable() 443 | { 444 | DiscoveryUrls = server.DiscoveryUrls, 445 | GatewayServerUri = server.GatewayServerUri, 446 | IsOnline = server.IsOnline, 447 | LastRegistered = DateTime.Now, 448 | ProductUri = server.ProductUri, 449 | SemaphoreFilePath = server.SemaphoreFilePath, 450 | ServerNames = server.ServerNames, 451 | ServerType = server.ServerType, 452 | ServerUri = server.ServerUri 453 | }; 454 | _serverTable.Add(model); 455 | } 456 | return CreateResponse(requestHeader, StatusCodes.Good); 457 | } 458 | catch (Exception ex) 459 | { 460 | Console.ForegroundColor = ConsoleColor.Red; 461 | Console.WriteLine("客户端调用RegisterServer()注册服务时触发异常:" + ex.Message); 462 | Console.ResetColor(); 463 | } 464 | return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError); 465 | } 466 | 467 | /// 468 | /// 注册服务(每个在本机启动的服务端都会主动调用此方法进行注册) 469 | /// 470 | /// 471 | /// 472 | /// 473 | /// 474 | /// 475 | /// 476 | public virtual ResponseHeader RegisterServer2( 477 | RequestHeader requestHeader, 478 | RegisteredServer server, 479 | ExtensionObjectCollection discoveryConfiguration, 480 | out StatusCodeCollection configurationResults, 481 | out DiagnosticInfoCollection diagnosticInfos) 482 | { 483 | configurationResults = null; 484 | diagnosticInfos = null; 485 | 486 | ValidateRequest(requestHeader); 487 | 488 | // Insert implementation. 489 | try 490 | { 491 | Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":服务注册:" + server.DiscoveryUrls.FirstOrDefault()); 492 | RegisteredServerTable model = _serverTable.Where(d => d.ServerUri == server.ServerUri).FirstOrDefault(); 493 | if (model != null) 494 | { 495 | model.LastRegistered = DateTime.Now; 496 | } 497 | else 498 | { 499 | model = new RegisteredServerTable() 500 | { 501 | DiscoveryUrls = server.DiscoveryUrls, 502 | GatewayServerUri = server.GatewayServerUri, 503 | IsOnline = server.IsOnline, 504 | LastRegistered = DateTime.Now, 505 | ProductUri = server.ProductUri, 506 | SemaphoreFilePath = server.SemaphoreFilePath, 507 | ServerNames = server.ServerNames, 508 | ServerType = server.ServerType, 509 | ServerUri = server.ServerUri 510 | }; 511 | _serverTable.Add(model); 512 | } 513 | configurationResults = new StatusCodeCollection() { StatusCodes.Good }; 514 | return CreateResponse(requestHeader, StatusCodes.Good); 515 | } 516 | catch (Exception ex) 517 | { 518 | Console.ForegroundColor = ConsoleColor.Red; 519 | Console.WriteLine("客户端调用RegisterServer2()注册服务时触发异常:" + ex.Message); 520 | Console.ResetColor(); 521 | } 522 | return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError); 523 | } 524 | 525 | /// 526 | /// 创建服务器使用的聚合管理器 527 | /// 528 | /// 529 | /// 530 | /// 531 | protected override AggregateManager CreateAggregateManager(IServerInternal server, ApplicationConfiguration configuration) 532 | { 533 | AggregateManager manager = new AggregateManager(server); 534 | 535 | manager.RegisterFactory(ObjectIds.AggregateFunction_Interpolative, BrowseNames.AggregateFunction_Interpolative, Aggregators.CreateStandardCalculator); 536 | manager.RegisterFactory(ObjectIds.AggregateFunction_Average, BrowseNames.AggregateFunction_Average, Aggregators.CreateStandardCalculator); 537 | manager.RegisterFactory(ObjectIds.AggregateFunction_TimeAverage, BrowseNames.AggregateFunction_TimeAverage, Aggregators.CreateStandardCalculator); 538 | manager.RegisterFactory(ObjectIds.AggregateFunction_TimeAverage2, BrowseNames.AggregateFunction_TimeAverage2, Aggregators.CreateStandardCalculator); 539 | manager.RegisterFactory(ObjectIds.AggregateFunction_Total, BrowseNames.AggregateFunction_Total, Aggregators.CreateStandardCalculator); 540 | manager.RegisterFactory(ObjectIds.AggregateFunction_Total2, BrowseNames.AggregateFunction_Total2, Aggregators.CreateStandardCalculator); 541 | 542 | manager.RegisterFactory(ObjectIds.AggregateFunction_Minimum, BrowseNames.AggregateFunction_Minimum, Aggregators.CreateStandardCalculator); 543 | manager.RegisterFactory(ObjectIds.AggregateFunction_Maximum, BrowseNames.AggregateFunction_Maximum, Aggregators.CreateStandardCalculator); 544 | manager.RegisterFactory(ObjectIds.AggregateFunction_MinimumActualTime, BrowseNames.AggregateFunction_MinimumActualTime, Aggregators.CreateStandardCalculator); 545 | manager.RegisterFactory(ObjectIds.AggregateFunction_MaximumActualTime, BrowseNames.AggregateFunction_MaximumActualTime, Aggregators.CreateStandardCalculator); 546 | manager.RegisterFactory(ObjectIds.AggregateFunction_Range, BrowseNames.AggregateFunction_Range, Aggregators.CreateStandardCalculator); 547 | manager.RegisterFactory(ObjectIds.AggregateFunction_Minimum2, BrowseNames.AggregateFunction_Minimum2, Aggregators.CreateStandardCalculator); 548 | manager.RegisterFactory(ObjectIds.AggregateFunction_Maximum2, BrowseNames.AggregateFunction_Maximum2, Aggregators.CreateStandardCalculator); 549 | manager.RegisterFactory(ObjectIds.AggregateFunction_MinimumActualTime2, BrowseNames.AggregateFunction_MinimumActualTime2, Aggregators.CreateStandardCalculator); 550 | manager.RegisterFactory(ObjectIds.AggregateFunction_MaximumActualTime2, BrowseNames.AggregateFunction_MaximumActualTime2, Aggregators.CreateStandardCalculator); 551 | manager.RegisterFactory(ObjectIds.AggregateFunction_Range2, BrowseNames.AggregateFunction_Range2, Aggregators.CreateStandardCalculator); 552 | 553 | manager.RegisterFactory(ObjectIds.AggregateFunction_Count, BrowseNames.AggregateFunction_Count, Aggregators.CreateStandardCalculator); 554 | manager.RegisterFactory(ObjectIds.AggregateFunction_AnnotationCount, BrowseNames.AggregateFunction_AnnotationCount, Aggregators.CreateStandardCalculator); 555 | manager.RegisterFactory(ObjectIds.AggregateFunction_DurationInStateZero, BrowseNames.AggregateFunction_DurationInStateZero, Aggregators.CreateStandardCalculator); 556 | manager.RegisterFactory(ObjectIds.AggregateFunction_DurationInStateNonZero, BrowseNames.AggregateFunction_DurationInStateNonZero, Aggregators.CreateStandardCalculator); 557 | manager.RegisterFactory(ObjectIds.AggregateFunction_NumberOfTransitions, BrowseNames.AggregateFunction_NumberOfTransitions, Aggregators.CreateStandardCalculator); 558 | 559 | manager.RegisterFactory(ObjectIds.AggregateFunction_Start, BrowseNames.AggregateFunction_Start, Aggregators.CreateStandardCalculator); 560 | manager.RegisterFactory(ObjectIds.AggregateFunction_End, BrowseNames.AggregateFunction_End, Aggregators.CreateStandardCalculator); 561 | manager.RegisterFactory(ObjectIds.AggregateFunction_Delta, BrowseNames.AggregateFunction_Delta, Aggregators.CreateStandardCalculator); 562 | manager.RegisterFactory(ObjectIds.AggregateFunction_StartBound, BrowseNames.AggregateFunction_StartBound, Aggregators.CreateStandardCalculator); 563 | manager.RegisterFactory(ObjectIds.AggregateFunction_EndBound, BrowseNames.AggregateFunction_EndBound, Aggregators.CreateStandardCalculator); 564 | manager.RegisterFactory(ObjectIds.AggregateFunction_DeltaBounds, BrowseNames.AggregateFunction_DeltaBounds, Aggregators.CreateStandardCalculator); 565 | 566 | manager.RegisterFactory(ObjectIds.AggregateFunction_DurationGood, BrowseNames.AggregateFunction_DurationGood, Aggregators.CreateStandardCalculator); 567 | manager.RegisterFactory(ObjectIds.AggregateFunction_DurationBad, BrowseNames.AggregateFunction_DurationBad, Aggregators.CreateStandardCalculator); 568 | manager.RegisterFactory(ObjectIds.AggregateFunction_PercentGood, BrowseNames.AggregateFunction_PercentGood, Aggregators.CreateStandardCalculator); 569 | manager.RegisterFactory(ObjectIds.AggregateFunction_PercentBad, BrowseNames.AggregateFunction_PercentBad, Aggregators.CreateStandardCalculator); 570 | manager.RegisterFactory(ObjectIds.AggregateFunction_WorstQuality, BrowseNames.AggregateFunction_WorstQuality, Aggregators.CreateStandardCalculator); 571 | manager.RegisterFactory(ObjectIds.AggregateFunction_WorstQuality2, BrowseNames.AggregateFunction_WorstQuality2, Aggregators.CreateStandardCalculator); 572 | 573 | manager.RegisterFactory(ObjectIds.AggregateFunction_StandardDeviationPopulation, BrowseNames.AggregateFunction_StandardDeviationPopulation, Aggregators.CreateStandardCalculator); 574 | manager.RegisterFactory(ObjectIds.AggregateFunction_VariancePopulation, BrowseNames.AggregateFunction_VariancePopulation, Aggregators.CreateStandardCalculator); 575 | manager.RegisterFactory(ObjectIds.AggregateFunction_StandardDeviationSample, BrowseNames.AggregateFunction_StandardDeviationSample, Aggregators.CreateStandardCalculator); 576 | manager.RegisterFactory(ObjectIds.AggregateFunction_VarianceSample, BrowseNames.AggregateFunction_VarianceSample, Aggregators.CreateStandardCalculator); 577 | 578 | return manager; 579 | } 580 | 581 | /// 582 | /// 为服务器创建资源管理器 583 | /// 584 | /// 585 | /// 586 | /// 587 | protected override ResourceManager CreateResourceManager(IServerInternal server, ApplicationConfiguration configuration) 588 | { 589 | ResourceManager resourceManager = new ResourceManager(server, configuration); 590 | 591 | System.Reflection.FieldInfo[] fields = typeof(StatusCodes).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); 592 | 593 | foreach (System.Reflection.FieldInfo field in fields) 594 | { 595 | uint? id = field.GetValue(typeof(StatusCodes)) as uint?; 596 | 597 | if (id != null) 598 | { 599 | resourceManager.Add(id.Value, "en-US", field.Name); 600 | } 601 | } 602 | 603 | return resourceManager; 604 | } 605 | 606 | /// 607 | /// 设置服务状态 608 | /// 609 | /// 610 | protected override void SetServerState(ServerState state) 611 | { 612 | lock (m_lock) 613 | { 614 | if (ServiceResult.IsBad(ServerError)) 615 | { 616 | throw new ServiceResultException(ServerError); 617 | } 618 | 619 | if (m_serverInternal == null) 620 | { 621 | throw new ServiceResultException(StatusCodes.BadServerHalted); 622 | } 623 | 624 | m_serverInternal.CurrentState = state; 625 | } 626 | } 627 | 628 | /// 629 | /// 配置变更事件 630 | /// 631 | /// 632 | /// 633 | protected override async void OnConfigurationChanged(object sender, ConfigurationWatcherEventArgs args) 634 | { 635 | try 636 | { 637 | ApplicationConfiguration configuration = await ApplicationConfiguration.Load( 638 | new FileInfo(args.FilePath), 639 | Configuration.ApplicationType, 640 | Configuration.GetType()); 641 | 642 | OnUpdateConfiguration(configuration); 643 | } 644 | catch (Exception e) 645 | { 646 | Utils.Trace(e, "Could not load updated configuration file from: {0}", args); 647 | } 648 | } 649 | 650 | /// 651 | /// 清理掉线服务 652 | /// 653 | /// 654 | private void ClearNoliveServer(object obj) 655 | { 656 | try 657 | { 658 | var tmpList = _serverTable.Where(d => d.LastRegistered < DateTime.Now.AddMinutes(-1) || !d.IsOnline).ToList(); 659 | if (tmpList.Count > 0) 660 | { 661 | lock (_serverTable) 662 | { 663 | foreach (var item in tmpList) 664 | { 665 | Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":清理服务:" + item.DiscoveryUrls.FirstOrDefault()); 666 | _serverTable.Remove(item); 667 | } 668 | } 669 | } 670 | } 671 | catch (Exception ex) 672 | { 673 | Console.ForegroundColor = ConsoleColor.Red; 674 | Console.WriteLine("清理掉线服务ClearNoliveServer()时触发异常:" + ex.Message); 675 | Console.ResetColor(); 676 | } 677 | } 678 | } 679 | } 680 | --------------------------------------------------------------------------------