前言

通过我前面的一篇文件,我们已经能够搭建一个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接口。

配置代码如下:

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();

  

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

protected override EndpointBase GetEndpointInstance(ServerBase server)
{
  return new DiscoveryEndpoint(server);//SessionEndpoint
}

  

2、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);
}
}
}

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

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

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。

通过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. Python "按位或"和"按位异或"的区别

    首先分别解释一下按位或和按位异或 按位或: 按位或指的是参与运算的两个数分别对应的二进制位进行“或”的操作.只要对应的两个二进制位有一个为1时,结果位就为1.python中运算符为“|” 按位异或: ...

  2. MySQL数据管理

    3.MySQL数据管理 3.1外键 方式一:  create table `grade`(  `gradeid` int(10) not null auto_increment comment '年纪 ...

  3. Bug--时区问题导致IDEA连接数据库失败

    打开cmd进入mysql,设置 set global time_zone='+8:00';

  4. Java 匿名对象、内部类

    一.匿名对象 1.概念 匿名对象是指创建对象时,只有创建对象的语句,却没有把对象地址值赋值给某个变量. public class Person{ public void eat(){ System.o ...

  5. 史蒂夫-乔布斯(Steve Jobs)斯坦福大学演讲稿(中英对照)

    这是苹果公司和Pixar动画工作室的CEO Steve Jobs于2005年6月12号在斯坦福大学的毕业典礼上面的演讲稿. Thank you. I'm honored to be with you ...

  6. PHP fileperms() 函数

    定义和用法 fileperms() 函数返回文件或目录的权限. 如果成功,该函数以数字形式返回权限.如果失败,则返回 FALSE. 语法 fileperms(filename) 参数 描述 filen ...

  7. PHP password_needs_rehash() 函数

    password_hash() 函数用于检测散列值是否匹配指定的选项. PHP 版本要求: PHP 5 >= 5.5.0, PHP 7高佣联盟 www.cgewang.com 语法 bool p ...

  8. 经验分享:一个 30 岁的人是如何转行做程序员,进入IT行业的?

    大约一年以前,我成为了一名全职开发者,我想要总结一下这一年的经验,并且和所有人分享,一个 30 多岁的人是如何进入科技行业的: 改变职业是一件吓人的事情,有时候还会成为一件危险的事情.年龄越大,危险就 ...

  9. Canal工作原理

    摘自:http://www.importnew.com/25189.html 背景 mysql主备复制实现: 从上层来看,复制分成三步: master将改变记录到二进制日志(binary log)中( ...

  10. docker 容器使用 systemctl 命令是报错

    看了许多解决方案,但是对于新手来说并不友好,不是特别清楚 报错内容: System has not been booted with systemd as init system (PID 1). C ...