經過C#實現OPC-UA服務端(二)

前言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。

相關文章
相關標籤/搜索