前言node
经过我前面的一篇文件,咱们已经可以搭建一个OPC-UA服务端了,而且也拥有了一些基础功能。这一次我们就来了解一下OPC-UA的服务注册与发现,若是对服务注册与发现这个概念不理解的朋友,能够先百度一下,因为近年来微服务架构的兴起,服务注册与发现已经成为一个很时髦的概念,它的主要功能可分为三点:
一、服务注册;
二、服务发现;
三、心跳检测。git
若是运行过OPC-UA源码的朋友们应该已经发现了,OPC-UA服务端启动以后,每隔一会就会输出一行错误提示信息,大体内容是"服务端注册失败,xxx毫秒以后重试",经过查看源码咱们能够知道,这是由于OPC-UA服务端启动以后,会自动调用"opc.tcp://localhost:4840/"的RegisterServer2方法注册本身,若是注册失败,则会当即调用RegisterServer方法再次进行服务注册,而因为咱们没有"opc.tcp://localhost:4840/"这个服务,因此每隔一下子就会提示服务注册失败。
如今咱们就动手来搭建一个"opc.tcp://localhost:4840/"服务,在OPC-UA标准中,它叫Discovery Server。github
1、服务配置
Discovery Server的服务配置与普通的OPC-UA服务配置差很少,只须要注意几点:
一、服务的类型ApplicationType是DiscoveryServer而不是Server;
二、服务启动时application.Start()传入的实例化对象须要实现IDiscoveryServer接口。数据库
配置代码以下:缓存
var config = new ApplicationConfiguration() { ApplicationName = "Axiu UA Discovery", ApplicationUri = Utils.Format(@"urn:{0}:AxiuUADiscovery", System.Net.Dns.GetHostName()), ApplicationType = ApplicationType.DiscoveryServer, ServerConfiguration = new ServerConfiguration() { BaseAddresses = { "opc.tcp://localhost:4840/" }, MinRequestThreadCount = 5, MaxRequestThreadCount = 100, MaxQueuedRequestCount = 200 }, DiscoveryServerConfiguration = new DiscoveryServerConfiguration() { BaseAddresses = { "opc.tcp://localhost:4840/" }, ServerNames = { "OpcuaDiscovery" } }, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", SubjectName = Utils.Format(@"CN={0}, DC={1}", "AxiuOpcua", System.Net.Dns.GetHostName()) }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" }, TrustedPeerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" }, RejectedCertificateStore = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" }, AutoAcceptUntrustedCertificates = true, AddAppCertToTrustedStore = true }, TransportConfigurations = new TransportConfigurationCollection(), TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }, TraceConfiguration = new TraceConfiguration() }; config.Validate(ApplicationType.DiscoveryServer).GetAwaiter().GetResult(); if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates) { config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted); }; } var application = new ApplicationInstance { ApplicationName = "Axiu UA Discovery", ApplicationType = ApplicationType.DiscoveryServer, ApplicationConfiguration = config }; //application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult(); bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result; if (!certOk) { Console.WriteLine("证书验证失败!"); } var server = new DiscoveryServer(); // start the server. application.Start(server).Wait();
2、实现IDiscoveryServer接口
下面咱们就来看看前面Discovery服务启动时传入的实例化对象与普通服务启动时传入的对象有什么不同,在咱们启动一个普通OPC-UA服务时,咱们能够直接使用StandardServer的对象,程序不会报错,只不过是没有任何节点和内容而已,而如今,若是咱们直接使用DiscoveryServerBase类的对象,启动Discovery服务时会报错。哪怕是咱们实现了IDiscoveryServer接口仍然会报错。为了能启动Discovery服务咱们还必须重写ServerBase中的两个方法:
一、EndpointBase GetEndpointInstance(ServerBase server),默认的GetEndpointInstance方法返回的类型是SessionEndpoint对象,而Discovery服务应该返回的是DiscoveryEndpoint;session
protected override EndpointBase GetEndpointInstance(ServerBase server) { return new DiscoveryEndpoint(server);//SessionEndpoint }
二、void StartApplication(ApplicationConfiguration configuration),默认的StartApplication方法没有执行任何操做,而咱们须要去启动一系列与Discovery服务相关的操做。架构
protected override void StartApplication(ApplicationConfiguration configuration) { lock (m_lock) { try { // create the datastore for the instance. m_serverInternal = new ServerInternalData( ServerProperties, configuration, MessageContext, new CertificateValidator(), InstanceCertificate); // create the manager responsible for providing localized string resources. ResourceManager resourceManager = CreateResourceManager(m_serverInternal, configuration); // create the manager responsible for incoming requests. RequestManager requestManager = new RequestManager(m_serverInternal); // create the master node manager. MasterNodeManager masterNodeManager = new MasterNodeManager(m_serverInternal, configuration, null); // add the node manager to the datastore. m_serverInternal.SetNodeManager(masterNodeManager); // put the node manager into a state that allows it to be used by other objects. masterNodeManager.Startup(); // create the manager responsible for handling events. EventManager eventManager = new EventManager(m_serverInternal, (uint)configuration.ServerConfiguration.MaxEventQueueSize); // creates the server object. m_serverInternal.CreateServerObject( eventManager, resourceManager, requestManager); // create the manager responsible for aggregates. m_serverInternal.AggregateManager = CreateAggregateManager(m_serverInternal, configuration); // start the session manager. SessionManager sessionManager = new SessionManager(m_serverInternal, configuration); sessionManager.Startup(); // start the subscription manager. SubscriptionManager subscriptionManager = new SubscriptionManager(m_serverInternal, configuration); subscriptionManager.Startup(); // add the session manager to the datastore. m_serverInternal.SetSessionManager(sessionManager, subscriptionManager); ServerError = null; // set the server status as running. SetServerState(ServerState.Running); // monitor the configuration file. if (!String.IsNullOrEmpty(configuration.SourceFilePath)) { var m_configurationWatcher = new ConfigurationWatcher(configuration); m_configurationWatcher.Changed += new EventHandler<ConfigurationWatcherEventArgs>(this.OnConfigurationChanged); } CertificateValidator.CertificateUpdate += OnCertificateUpdate; //60s后开始清理过时服务列表,此后每60s检查一次 m_timer = new Timer(ClearNoliveServer, null, 60000, 60000); Console.WriteLine("Discovery服务已启动完成,请勿退出程序!!!"); } catch (Exception e) { Utils.Trace(e, "Unexpected error starting application"); m_serverInternal = null; ServiceResult error = ServiceResult.Create(e, StatusCodes.BadInternalError, "Unexpected error starting application"); ServerError = error; throw new ServiceResultException(error); } } }
3、注册与发现服务
服务注册以后,就涉及到服务信息如何保存,OPC-UA标准里面好像是没有固定要的要求,应该是没有,至少我没有发现...傲娇.jpg。app
1.注册服务
这里我就直接使用一个集合来保存服务信息,这种方式存在一个问题:若是Discovery服务重启了,那么在服务从新注册以前这段时间内,全部已注册的服务信息都丢失了(由于OPC-UA服务的心跳间隔是30s,也就是最大可能会有30s的时间服务信息丢失)。因此若是对服务状态信息敏感的状况,请自行使用其余方式,能够存储到数据库,也能够用其余分布式缓存来保存。这些就不在咱们的讨论范围内了,咱们先看看服务注册的代码。tcp
public virtual ResponseHeader RegisterServer2( RequestHeader requestHeader, RegisteredServer server, ExtensionObjectCollection discoveryConfiguration, out StatusCodeCollection configurationResults, out DiagnosticInfoCollection diagnosticInfos) { configurationResults = null; diagnosticInfos = null; ValidateRequest(requestHeader); // Insert implementation. try { Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":服务注册:" + server.DiscoveryUrls.FirstOrDefault()); RegisteredServerTable model = _serverTable.Where(d => d.ServerUri == server.ServerUri).FirstOrDefault(); if (model != null) { model.LastRegistered = DateTime.Now; } else { model = new RegisteredServerTable() { DiscoveryUrls = server.DiscoveryUrls, GatewayServerUri = server.GatewayServerUri, IsOnline = server.IsOnline, LastRegistered = DateTime.Now, ProductUri = server.ProductUri, SemaphoreFilePath = server.SemaphoreFilePath, ServerNames = server.ServerNames, ServerType = server.ServerType, ServerUri = server.ServerUri }; _serverTable.Add(model); } configurationResults = new StatusCodeCollection() { StatusCodes.Good }; return CreateResponse(requestHeader, StatusCodes.Good); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("客户端调用RegisterServer2()注册服务时触发异常:" + ex.Message); Console.ResetColor(); } return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError); }
前面有说到,OPC-UA普通服务启动后会先调用RegisterServer2方法注册本身,若是注册失败,则会当即调用RegisterServer方法再次进行服务注册。因此,为防万一。RegisterServer2和RegisterServer咱们都须要实现,可是他们的内容实际上是同样的,毕竟都是干同样的活--接收服务信息,而后把服务信息保存起来。分布式
public virtual ResponseHeader RegisterServer( RequestHeader requestHeader, RegisteredServer server) { ValidateRequest(requestHeader); // Insert implementation. try { Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":服务注册:" + server.DiscoveryUrls.FirstOrDefault()); RegisteredServerTable model = _serverTable.Where(d => d.ServerUri == server.ServerUri).FirstOrDefault(); if (model != null) { model.LastRegistered = DateTime.Now; } else { model = new RegisteredServerTable() { DiscoveryUrls = server.DiscoveryUrls, GatewayServerUri = server.GatewayServerUri, IsOnline = server.IsOnline, LastRegistered = DateTime.Now, ProductUri = server.ProductUri, SemaphoreFilePath = server.SemaphoreFilePath, ServerNames = server.ServerNames, ServerType = server.ServerType, ServerUri = server.ServerUri }; _serverTable.Add(model); } return CreateResponse(requestHeader, StatusCodes.Good); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("客户端调用RegisterServer()注册服务时触发异常:" + ex.Message); Console.ResetColor(); } return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError); }
2.发现服务
服务注册以后,咱们的Discovery服务就知道有哪些OPC-UA服务已经启动了,因此咱们还须要一个方法来告诉客户端这些已启动的服务信息。FindServers()方法就是来干这件事的。
public override ResponseHeader FindServers( RequestHeader requestHeader, string endpointUrl, StringCollection localeIds, StringCollection serverUris, out ApplicationDescriptionCollection servers) { servers = new ApplicationDescriptionCollection(); ValidateRequest(requestHeader); Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":请求查找服务..."); string hostName = Dns.GetHostName(); lock (_serverTable) { foreach (var item in _serverTable) { StringCollection urls = new StringCollection(); foreach (var url in item.DiscoveryUrls) { if (url.Contains("localhost")) { string str = url.Replace("localhost", hostName); urls.Add(str); } else { urls.Add(url); } } servers.Add(new ApplicationDescription() { ApplicationName = item.ServerNames.FirstOrDefault(), ApplicationType = item.ServerType, ApplicationUri = item.ServerUri, DiscoveryProfileUri = item.SemaphoreFilePath, DiscoveryUrls = urls, ProductUri = item.ProductUri, GatewayServerUri = item.GatewayServerUri }); } } return CreateResponse(requestHeader, StatusCodes.Good); }
3.心跳检测
须要注意一点,在OPC-UA标准中并无提供单独的心跳方法,它采用的心跳方式就是再次向Discovery服务注册本身,这也就是为何服务注册失败以后会重试;服务注册成功了,它也仍是会重试。因此在服务注册时,咱们须要判断一下服务信息是否已经存在了,若是已经存在了,那么就执行心跳的操做。
至此,咱们已经实现的服务的注册与发现,IDiscoveryServer接口要求的内容咱们也都实现了,可是有没有发现咱们还少了同样东西,就是若是咱们的某个普通服务关闭了或是掉线了,咱们的Discovery服务仍是保存着它的信息,这个时候理论上来说,已离线的服务信息就应该删掉,不该该给客户端返回了。因此这就须要一个方法来清理那些已经离线的服务。
private void ClearNoliveServer(object obj) { try { var tmpList = _serverTable.Where(d => d.LastRegistered < DateTime.Now.AddMinutes(-1) || !d.IsOnline).ToList(); if (tmpList.Count > 0) { lock (_serverTable) { foreach (var item in tmpList) { Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":清理服务:" + item.DiscoveryUrls.FirstOrDefault()); _serverTable.Remove(item); } } } } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("清理掉线服务ClearNoliveServer()时触发异常:" + ex.Message); Console.ResetColor(); } }
我这里以一分钟为限,若是一分钟内都没有心跳的服务,我就当它是离线了。关于这个一分钟须要根据自身状况来调整。
补充说明
OPC-UA服务默认是向localhost注册本身,固然,也能够调整配置信息,把服务注册到其余地方去,只需在ApplicationConfiguration对象中修改ServerConfiguration属性以下:
ServerConfiguration = new ServerConfiguration() { BaseAddresses = { "opc.tcp://localhost:8020/", "https://localhost:8021/" }, MinRequestThreadCount = 5, MaxRequestThreadCount = 100, MaxQueuedRequestCount = 200, RegistrationEndpoint = new EndpointDescription() { EndpointUrl = "opc.tcp://172.17.4.68:4840", SecurityLevel = ServerSecurityPolicy.CalculateSecurityLevel(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256), SecurityMode = MessageSecurityMode.SignAndEncrypt, SecurityPolicyUri = SecurityPolicies.Basic256Sha256, Server = new ApplicationDescription() { ApplicationType = ApplicationType.DiscoveryServer }, } },
最新的Discovery Server代码在个人GitHub上已经上传,地址:
https://github.com/axiu233/AxiuOpcua.ServerDemo代码文件为:Axiu.Opcua.Demo.Service.DiscoveryManagement;Axiu.Opcua.Demo.Service.DiscoveryServer。