前言

通过我前面的一篇文件,我们已经能够搭建一个OPC-UA服务端了,并且也拥有了一些基础功能。这一次咱们就来了解一下OPC-UA的服务注册与发现,如果对服务注册与发现这个概念不理解的朋友,可以先百度一下,由于近年来微服务架构的兴起,服务注册与发现已经成为一个很时髦的概念,它的主要功能可分为三点:
1、服务注册;
2、服务发现;
3、心跳检测。

如果运行过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。

一、服务配置
Discovery Server的服务配置与普通的OPC-UA服务配置差不多,只需要注意几点:
1、服务的类型ApplicationType是DiscoveryServer而不是Server;
2、服务启动时application.Start()传入的实例化对象需要实现IDiscoveryServer接口。

配置代码如下:

  1. var config = new ApplicationConfiguration()
  2. {
  3. ApplicationName = "Axiu UA Discovery",
  4. ApplicationUri = Utils.Format(@"urn:{0}:AxiuUADiscovery", System.Net.Dns.GetHostName()),
  5. ApplicationType = ApplicationType.DiscoveryServer,
  6. ServerConfiguration = new ServerConfiguration()
  7. {
  8. BaseAddresses = { "opc.tcp://localhost:4840/" },
  9. MinRequestThreadCount = 5,
  10. MaxRequestThreadCount = 100,
  11. MaxQueuedRequestCount = 200
  12. },
  13. DiscoveryServerConfiguration = new DiscoveryServerConfiguration()
  14. {
  15. BaseAddresses = { "opc.tcp://localhost:4840/" },
  16. ServerNames = { "OpcuaDiscovery" }
  17. },
  18. SecurityConfiguration = new SecurityConfiguration
  19. {
  20. ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", SubjectName = Utils.Format(@"CN={0}, DC={1}", "AxiuOpcua", System.Net.Dns.GetHostName()) },
  21. TrustedIssuerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" },
  22. TrustedPeerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" },
  23. RejectedCertificateStore = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" },
  24. AutoAcceptUntrustedCertificates = true,
  25. AddAppCertToTrustedStore = true
  26. },
  27. TransportConfigurations = new TransportConfigurationCollection(),
  28. TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
  29. ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
  30. TraceConfiguration = new TraceConfiguration()
  31. };
  32. config.Validate(ApplicationType.DiscoveryServer).GetAwaiter().GetResult();
  33. if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
  34. {
  35. config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted); };
  36. }
  37.  
  38. var application = new ApplicationInstance
  39. {
  40. ApplicationName = "Axiu UA Discovery",
  41. ApplicationType = ApplicationType.DiscoveryServer,
  42. ApplicationConfiguration = config
  43. };
  44. //application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult();
  45. bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result;
  46. if (!certOk)
  47. {
  48. Console.WriteLine("证书验证失败!");
  49. }
  50.  
  51. var server = new DiscoveryServer();
  52. // start the server.
  53. application.Start(server).Wait();

  

二、实现IDiscoveryServer接口
下面我们就来看看前面Discovery服务启动时传入的实例化对象与普通服务启动时传入的对象有什么不一样,在我们启动一个普通OPC-UA服务时,我们可以直接使用StandardServer的对象,程序不会报错,只不过是没有任何节点和内容而已,而现在,如果我们直接使用DiscoveryServerBase类的对象,启动Discovery服务时会报错。哪怕是我们实现了IDiscoveryServer接口仍然会报错。为了能启动Discovery服务我们还必须重写ServerBase中的两个方法:
1、EndpointBase GetEndpointInstance(ServerBase server),默认的GetEndpointInstance方法返回的类型是SessionEndpoint对象,而Discovery服务应该返回的是DiscoveryEndpoint;

  1. protected override EndpointBase GetEndpointInstance(ServerBase server)
  2. {
  3.   return new DiscoveryEndpoint(server);//SessionEndpoint
  4. }

  

2、void StartApplication(ApplicationConfiguration configuration),默认的StartApplication方法没有执行任何操作,而我们需要去启动一系列与Discovery服务相关的操作。

  1. protected override void StartApplication(ApplicationConfiguration configuration)
  2. {
  3. lock (m_lock)
  4. {
  5. try
  6. {
  7. // create the datastore for the instance.
  8. m_serverInternal = new ServerInternalData(
  9. ServerProperties,
  10. configuration,
  11. MessageContext,
  12. new CertificateValidator(),
  13. InstanceCertificate);
  14.  
  15. // create the manager responsible for providing localized string resources.
  16. ResourceManager resourceManager = CreateResourceManager(m_serverInternal, configuration);
  17.  
  18. // create the manager responsible for incoming requests.
  19. RequestManager requestManager = new RequestManager(m_serverInternal);
  20.  
  21. // create the master node manager.
  22. MasterNodeManager masterNodeManager = new MasterNodeManager(m_serverInternal, configuration, null);
  23.  
  24. // add the node manager to the datastore.
  25. m_serverInternal.SetNodeManager(masterNodeManager);
  26.  
  27. // put the node manager into a state that allows it to be used by other objects.
  28. masterNodeManager.Startup();
  29.  
  30. // create the manager responsible for handling events.
  31. EventManager eventManager = new EventManager(m_serverInternal, (uint)configuration.ServerConfiguration.MaxEventQueueSize);
  32.  
  33. // creates the server object.
  34. m_serverInternal.CreateServerObject(
  35. eventManager,
  36. resourceManager,
  37. requestManager);
  38.  
  39. // create the manager responsible for aggregates.
  40. m_serverInternal.AggregateManager = CreateAggregateManager(m_serverInternal, configuration);
  41.  
  42. // start the session manager.
  43. SessionManager sessionManager = new SessionManager(m_serverInternal, configuration);
  44. sessionManager.Startup();
  45.  
  46. // start the subscription manager.
  47. SubscriptionManager subscriptionManager = new SubscriptionManager(m_serverInternal, configuration);
  48. subscriptionManager.Startup();
  49.  
  50. // add the session manager to the datastore.
  51. m_serverInternal.SetSessionManager(sessionManager, subscriptionManager);
  52.  
  53. ServerError = null;
  54.  
  55. // set the server status as running.
  56. SetServerState(ServerState.Running);
  57.  
  58. // monitor the configuration file.
  59. if (!String.IsNullOrEmpty(configuration.SourceFilePath))
  60. {
  61. var m_configurationWatcher = new ConfigurationWatcher(configuration);
  62. m_configurationWatcher.Changed += new EventHandler<ConfigurationWatcherEventArgs>(this.OnConfigurationChanged);
  63. }
  64.  
  65. CertificateValidator.CertificateUpdate += OnCertificateUpdate;
  66. //60s后开始清理过期服务列表,此后每60s检查一次
  67. m_timer = new Timer(ClearNoliveServer, null, 60000, 60000);
  68. Console.WriteLine("Discovery服务已启动完成,请勿退出程序!!!");
  69. }
  70. catch (Exception e)
  71. {
  72. Utils.Trace(e, "Unexpected error starting application");
  73. m_serverInternal = null;
  74. ServiceResult error = ServiceResult.Create(e, StatusCodes.BadInternalError, "Unexpected error starting application");
  75. ServerError = error;
  76. throw new ServiceResultException(error);
  77. }
  78. }
  79. }

三、注册与发现服务
服务注册之后,就涉及到服务信息如何保存,OPC-UA标准里面好像是没有固定要的要求,应该是没有,至少我没有发现...傲娇.jpg。

1.注册服务
这里我就直接使用一个集合来保存服务信息,这种方式存在一个问题:如果Discovery服务重启了,那么在服务重新注册之前这段时间内,所有已注册的服务信息都丢失了(因为OPC-UA服务的心跳间隔是30s,也就是最大可能会有30s的时间服务信息丢失)。所以如果对服务状态信息敏感的情况,请自行使用其他方式,可以存储到数据库,也可以用其他分布式缓存来保存。这些就不在我们的讨论范围内了,我们先看看服务注册的代码。

  1. public virtual ResponseHeader RegisterServer2(
  2. RequestHeader requestHeader,
  3. RegisteredServer server,
  4. ExtensionObjectCollection discoveryConfiguration,
  5. out StatusCodeCollection configurationResults,
  6. out DiagnosticInfoCollection diagnosticInfos)
  7. {
  8. configurationResults = null;
  9. diagnosticInfos = null;
  10.  
  11. ValidateRequest(requestHeader);
  12.  
  13. // Insert implementation.
  14. try
  15. {
  16. Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":服务注册:" + server.DiscoveryUrls.FirstOrDefault());
  17. RegisteredServerTable model = _serverTable.Where(d => d.ServerUri == server.ServerUri).FirstOrDefault();
  18. if (model != null)
  19. {
  20. model.LastRegistered = DateTime.Now;
  21. }
  22. else
  23. {
  24. model = new RegisteredServerTable()
  25. {
  26. DiscoveryUrls = server.DiscoveryUrls,
  27. GatewayServerUri = server.GatewayServerUri,
  28. IsOnline = server.IsOnline,
  29. LastRegistered = DateTime.Now,
  30. ProductUri = server.ProductUri,
  31. SemaphoreFilePath = server.SemaphoreFilePath,
  32. ServerNames = server.ServerNames,
  33. ServerType = server.ServerType,
  34. ServerUri = server.ServerUri
  35. };
  36. _serverTable.Add(model);
  37. }
  38. configurationResults = new StatusCodeCollection() { StatusCodes.Good };
  39. return CreateResponse(requestHeader, StatusCodes.Good);
  40. }
  41. catch (Exception ex)
  42. {
  43. Console.ForegroundColor = ConsoleColor.Red;
  44. Console.WriteLine("客户端调用RegisterServer2()注册服务时触发异常:" + ex.Message);
  45. Console.ResetColor();
  46. }
  47. return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError);
  48. }

前面有说到,OPC-UA普通服务启动后会先调用RegisterServer2方法注册自己,如果注册失败,则会立即调用RegisterServer方法再次进行服务注册。所以,为防万一。RegisterServer2和RegisterServer我们都需要实现,但是他们的内容其实是一样的,毕竟都是干一样的活--接收服务信息,然后把服务信息保存起来。

  1. public virtual ResponseHeader RegisterServer(
  2. RequestHeader requestHeader,
  3. RegisteredServer server)
  4. {
  5. ValidateRequest(requestHeader);
  6.  
  7. // Insert implementation.
  8. try
  9. {
  10. Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":服务注册:" + server.DiscoveryUrls.FirstOrDefault());
  11. RegisteredServerTable model = _serverTable.Where(d => d.ServerUri == server.ServerUri).FirstOrDefault();
  12. if (model != null)
  13. {
  14. model.LastRegistered = DateTime.Now;
  15. }
  16. else
  17. {
  18. model = new RegisteredServerTable()
  19. {
  20. DiscoveryUrls = server.DiscoveryUrls,
  21. GatewayServerUri = server.GatewayServerUri,
  22. IsOnline = server.IsOnline,
  23. LastRegistered = DateTime.Now,
  24. ProductUri = server.ProductUri,
  25. SemaphoreFilePath = server.SemaphoreFilePath,
  26. ServerNames = server.ServerNames,
  27. ServerType = server.ServerType,
  28. ServerUri = server.ServerUri
  29. };
  30. _serverTable.Add(model);
  31. }
  32. return CreateResponse(requestHeader, StatusCodes.Good);
  33. }
  34. catch (Exception ex)
  35. {
  36. Console.ForegroundColor = ConsoleColor.Red;
  37. Console.WriteLine("客户端调用RegisterServer()注册服务时触发异常:" + ex.Message);
  38. Console.ResetColor();
  39. }
  40. return CreateResponse(requestHeader, StatusCodes.BadUnexpectedError);
  41. }

  

2.发现服务
服务注册之后,我们的Discovery服务就知道有哪些OPC-UA服务已经启动了,所以我们还需要一个方法来告诉客户端这些已启动的服务信息。FindServers()方法就是来干这件事的。

  1. public override ResponseHeader FindServers(
  2. RequestHeader requestHeader,
  3. string endpointUrl,
  4. StringCollection localeIds,
  5. StringCollection serverUris,
  6. out ApplicationDescriptionCollection servers)
  7. {
  8. servers = new ApplicationDescriptionCollection();
  9.  
  10. ValidateRequest(requestHeader);
  11.  
  12. Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":请求查找服务...");
  13. string hostName = Dns.GetHostName();
  14.  
  15. lock (_serverTable)
  16. {
  17. foreach (var item in _serverTable)
  18. {
  19. StringCollection urls = new StringCollection();
  20. foreach (var url in item.DiscoveryUrls)
  21. {
  22. if (url.Contains("localhost"))
  23. {
  24. string str = url.Replace("localhost", hostName);
  25. urls.Add(str);
  26. }
  27. else
  28. {
  29. urls.Add(url);
  30. }
  31. }
  32.  
  33. servers.Add(new ApplicationDescription()
  34. {
  35. ApplicationName = item.ServerNames.FirstOrDefault(),
  36. ApplicationType = item.ServerType,
  37. ApplicationUri = item.ServerUri,
  38. DiscoveryProfileUri = item.SemaphoreFilePath,
  39. DiscoveryUrls = urls,
  40. ProductUri = item.ProductUri,
  41. GatewayServerUri = item.GatewayServerUri
  42. });
  43. }
  44. }
  45.  
  46. return CreateResponse(requestHeader, StatusCodes.Good);
  47. }

  

3.心跳检测
需要注意一点,在OPC-UA标准中并没有提供单独的心跳方法,它采用的心跳方式就是再次向Discovery服务注册自己,这也就是为什么服务注册失败之后会重试;服务注册成功了,它也还是会重试。所以在服务注册时,我们需要判断一下服务信息是否已经存在了,如果已经存在了,那么就执行心跳的操作。

至此,我们已经实现的服务的注册与发现,IDiscoveryServer接口要求的内容我们也都实现了,但是有没有发现我们还少了一样东西,就是如果我们的某个普通服务关闭了或是掉线了,我们的Discovery服务还是保存着它的信息,这个时候理论上来讲,已离线的服务信息就应该删掉,不应该给客户端返回了。所以这就需要一个方法来清理那些已经离线的服务。

  1. private void ClearNoliveServer(object obj)
  2. {
  3. try
  4. {
  5. var tmpList = _serverTable.Where(d => d.LastRegistered < DateTime.Now.AddMinutes(-1) || !d.IsOnline).ToList();
  6. if (tmpList.Count > 0)
  7. {
  8. lock (_serverTable)
  9. {
  10. foreach (var item in tmpList)
  11. {
  12. Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ":清理服务:" + item.DiscoveryUrls.FirstOrDefault());
  13. _serverTable.Remove(item);
  14. }
  15. }
  16. }
  17. }
  18. catch (Exception ex)
  19. {
  20. Console.ForegroundColor = ConsoleColor.Red;
  21. Console.WriteLine("清理掉线服务ClearNoliveServer()时触发异常:" + ex.Message);
  22. Console.ResetColor();
  23. }
  24. }

我这里以一分钟为限,如果一分钟内都没有心跳的服务,我就当它是离线了。关于这个一分钟需要根据自身情况来调整。

补充说明
OPC-UA服务默认是向localhost注册自己,当然,也可以调整配置信息,把服务注册到其他地方去,只需在ApplicationConfiguration对象中修改ServerConfiguration属性如下:

  1. ServerConfiguration = new ServerConfiguration() {
  2. BaseAddresses = { "opc.tcp://localhost:8020/", "https://localhost:8021/" },
  3. MinRequestThreadCount = 5,
  4. MaxRequestThreadCount = 100,
  5. MaxQueuedRequestCount = 200,
  6. RegistrationEndpoint = new EndpointDescription() {
  7. EndpointUrl = "opc.tcp://172.17.4.68:4840",
  8. SecurityLevel = ServerSecurityPolicy.CalculateSecurityLevel(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256),
  9. SecurityMode = MessageSecurityMode.SignAndEncrypt,
  10. SecurityPolicyUri = SecurityPolicies.Basic256Sha256,
  11. Server = new ApplicationDescription() { ApplicationType = ApplicationType.DiscoveryServer },
  12. }
  13. },

最新的Discovery Server代码在我的GitHub上已经上传,地址:
https://github.com/axiu233/AxiuOpcua.ServerDemo
代码文件为:
Axiu.Opcua.Demo.Service.DiscoveryManagement;
Axiu.Opcua.Demo.Service.DiscoveryServer。

通过C#实现OPC-UA服务端(二)的更多相关文章

  1. [Python 网络编程] TCP编程/群聊服务端 (二)

    群聊服务端 需求分析: 1. 群聊服务端需支持启动和停止(清理资源); 2. 可以接收客户端的连接; 接收客户端发来的数据 3. 可以将每条信息分发到所有客户端 1) 先搭架子: #TCP Serve ...

  2. 网络编程、三要素、Socket通信、UDP传输、TCP协议、服务端(二十五)

    1.网络编程概述 * A:计算机网络 * 是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传 ...

  3. maven版cxf集合spring开发服务端(二)

    一.新建一个maven项目 二.pom.xml引入依赖 <dependency> <groupId>org.apache.cxf</groupId> <art ...

  4. iOS In-App Purchase(IAP)内购服务端二次验证注意事项

    前端iOS完成对应的商品购买之后,会得到一个Transaction(交易)的数据结构指针,后端实际上只需要这个结构内的一个东西,那就是 transaction.transactionReceipt. ...

  5. [发布]SuperIO v2.2.5 集成OPC服务端和OPC客户端

    SuperIO 下载:本站下载 百度网盘 1.修复串口号大于等于10的时候导致IO未知状态. 2.优化RunIODevice(io)函数内部处理流程,二次开发可以重载这个接口. 3.优化IO接收数据, ...

  6. OPC UA 统一架构) (二)

    OPC UA (二) 重头戏,捞取数据,才是该干的事.想获取数据,先有数据源DataPrivade,DataPrivade的数据集合不能和BaseDataVariableState的集合存储同一地址, ...

  7. WCF学习之旅—实现支持REST服务端应用(二十三)

    在上一篇(WCF学习之旅—实现REST服务(二十二))文章中简单介绍了一下RestFul与WCF支持RestFul所提供的方法,本文讲解一下如何创建一个支持REST的WCF服务端程序. 四.在WCF中 ...

  8. (二)Netty源码学习笔记之服务端启动

    尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6129971.html  本文将不会对netty中每个点分类讲解,而是一个服务端启 ...

  9. 【原创】NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示

    前言 NIO框架的流行,使得开发大并发.高性能的互联网服务端成为可能.这其中最流行的无非就是MINA和Netty了,MINA目前的主要版本是MINA2.而Netty的主要版本是Netty3和Netty ...

随机推荐

  1. 搭建mysql NDB集群

    NDB群集安装 介绍 https://dev.mysql.com/doc/refman/8.0/en/mysql-cluster-basics.html NDBCLUSTER (也称为NDB)是一种内 ...

  2. Flutter学习笔记(41)--自定义Dialog实现版本更新弹窗

    如需转载,请注明出处:Flutter学习笔记(41)--自定义Dialog实现版本更新弹窗 功能点: 1.更新弹窗UI 2.强更与非强更且别控制 3.屏蔽物理返回键(因为强更的时候点击返回键,弹窗会消 ...

  3. vue 修改路由

    直接放代码: this.$router.push({ path: "/login" });

  4. 面试题五十四:二叉搜索树的第K大节点

    方法:搜索二叉树的特点就是左树小于节点,节点小于右树,所以采用中序遍历法就可以得到排序序列 BinaryTreeNode KthNode(BinaryTreeNode pNode ,int k){ i ...

  5. 面试题十八:在O(1)的时间内删除链表的节点

    方法一:将要删除的·节点的下一个节点的内容复制到该节点上,然后删除下一个节点注意特殊情况:链表只有一个节点时,则删除头节点,则把头节点设置为null, 如果删除的尾节点则需要顺序遍历链表,取得前序节点 ...

  6. 没想到 Hash 冲突还能这么玩,你的服务中招了吗?

    背景 其实这个问题我之前也看到过,刚好在前几天,洪教授在某个群里分享的一个<一些有意思的攻击手段.pdf>,我觉得这个话题还是有不少人不清楚的,今天我就准备来“实战”一把,还请各位看官轻拍 ...

  7. 一个在raw里面放着数据库文件的网上例子

    https://www.cnblogs.com/yutingliuyl/p/6880103.html

  8. Nginx与Apache简单对比

    Nginx 1.轻量级,采用C进行编写,同样的 web 服务,会占用更少的内存及资源 2.抗并发,处理请求是异步非阻塞的,负载能力比apache高很多,而 apache 则是阻塞型的.在高并发下 ng ...

  9. Pandas 复习2

    import pandas as pd import numpy as np food_info = pd.read_csv('food_info.csv') 1.处理缺失值(可使用平均数,众数填充) ...

  10. bzoj 2780 [Spoj]8093 Sevenk Love Oimaster

    LINK:Sevenk Love Oimaster 询问一个模式串在多少个文本串中出现过. 考虑广义SAM 统计这种数量问题一般有三种做法. 一种 暴力bitset 这道题可能可以过? 一种 暴力跳p ...