利用C#实现OPC-UA服务端
前言
最近接手了一个项目,做一个 OPC-UA 服务端?刚听到这个消息我是一脸懵,发自灵魂的三问“OPC-UA是什么?”、“要怎么做?”、“有什么用?”。
我之前都是做互联网相关的东西,这种物联网的还真是第一次接触。没办法只能打开我的浏览器四处搜索,结果百度了一圈下来发现都是要么是介绍OPC-UA是什么的,要么就是OPC-UA客户端,反正服务端相关的内容是找了半天都没找到,但这是领导们安排的任务啊,我总不能回复网上没有教程吧,于是只能把目光投向了最后的希望:GitHub,好在最后找到了OPC基金会的源码。
源码地址:https://github.com/OPCFoundation/UA-.NETStandard
不过这个源码对于我这种刚接触工业物联网的人来说,太过于复杂,而且网上相关的技术说明文档太少,觉得非常有必要动手记录一下我的OPC-UA服务端实现过程,方便以后回过头来巩固。
关于什么是OPC-UA、OPCFoundation是什么我就不多说了,百度以下,一大堆说这些理论东西的,咱们还是更喜欢动手干起来。
以下就是我实现OPC-UA服务端的记录,分享出来,大家一起探讨以下。由于我也是第一次接触这种工业物联网,所以有什么说的不对的,请大家多多指点,共同学习共同进步!
引入Nuget包
Nuget包管理器中搜索 OPCFoundation.NetStandard.Opc.Ua 安装即可;
关于OPCFoundation.NetStandard.Opc.Ua的源码就是我上面所说的OPC基金会的源码,感兴趣的请自行前往GitHub查看;

初始化节点树
重写CustomNodeManager2类的CreateAddressSpace()方法,在服务启动时会调用CreateAddressSpace()方法创建我们自己定义的各个节点。在我的代码中,我主要用到两种创建节点方式:
1、创建目录
private FolderState CreateFolder(NodeState parent, string path, string name)
{
FolderState folder = new FolderState(parent); folder.SymbolicName = name;
folder.ReferenceTypeId = ReferenceTypes.Organizes;
folder.TypeDefinitionId = ObjectTypeIds.FolderType;
folder.NodeId = new NodeId(path, NamespaceIndex);
folder.BrowseName = new QualifiedName(path, NamespaceIndex);
folder.DisplayName = new LocalizedText("en", name);
folder.WriteMask = AttributeWriteMask.None;
folder.UserWriteMask = AttributeWriteMask.None;
folder.EventNotifier = EventNotifiers.None; if (parent != null)
{
parent.AddChild(folder);
} return folder;
}
2、创建子节点
private BaseDataVariableState CreateVariable(NodeState parent, string path, string name, NodeId dataType, int valueRank)
{
BaseDataVariableState variable = new BaseDataVariableState(parent); variable.SymbolicName = name;
variable.ReferenceTypeId = ReferenceTypes.Organizes;
variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
variable.NodeId = new NodeId(path, NamespaceIndex);
variable.BrowseName = new QualifiedName(path, NamespaceIndex);
variable.DisplayName = new LocalizedText("en", name);
variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
variable.DataType = dataType;
variable.ValueRank = valueRank;
variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
variable.Historizing = false;
//variable.Value = GetNewValue(variable);
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.Now;
//此处绑定节点的写入事件
variable.OnWriteValue = OnWriteDataValue; if (valueRank == ValueRanks.OneDimension)
{
variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { });
}
else if (valueRank == ValueRanks.TwoDimensions)
{
variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { , });
} if (parent != null)
{
parent.AddChild(variable);
} return variable;
}
简单的理解,我创建出来的节点树,类似于文件系统,从根节点开始向下是一级级的‘目录’,只有最后在‘目录’下的‘文件’才有值。
实时刷新数据
仅仅创建节点树还不够,他们的值都是固定的并不会变动,而实际的应用场景中,这些数据肯定是随时在变化的;所以,我们需要新开一个线程,去循环刷新我们各个节点的值。
Task.Run(() =>
{
while (true)
{
try
{
//模拟获取实时数据
BaseDataVariableState node = null;
/*
* 在实际业务中应该是根据对应的标识来更新固定节点的数据
* 这里 我偷个懒 全部测点都更新为一个新的随机数
*/
// _nodeDic:保存所有最子节点的字典Dictionary<string, BaseDataVariableState>
foreach (var item in _nodeDic)
{
node = item.Value;
node.Value = RandomLibrary.GetRandomInt(, );
node.Timestamp = DateTime.Now;
//变更标识 只有执行了这一步,订阅的客户端才会收到新的数据
node.ClearChangeMasks(SystemContext, false);
}
//休息1秒
Thread.Sleep( * );
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("更新OPC-UA节点数据触发异常:" + ex.Message);
Console.ResetColor();
}
}
});
动态添加节点
在实际的应用中,很有可能我们临时需要添加一个节点,或者由于某些业务的变动,我需要删除掉某些节点;这就好比我把电脑借给朋友之前,总是会先删掉E盘里的学习资料文件夹和里面的文件,等电脑还回来之后我再重新加上。
//nodes:包含所有节点及其从属关系的列表
public void UpdateNodesAttribute(List<OpcuaNode> nodes)
{
/*
* 此处有想过删除整个菜单树,然后重建 保证各个NodeId仍与原来的一直
* 但是 后来发现这样会导致原来的客户端订阅信息丢失 无法获取订阅数据
* 所以 只能一级级的检查节点 然后修改属性
*/
//修改或创建根节点
var scadas = nodes.Where(d => d.NodeType == NodeType.Scada);
foreach (var item in scadas)
{
FolderState scadaNode = null;
if (!_folderDic.TryGetValue(item.NodePath, out scadaNode))
{
//如果根节点都不存在 那么整个树都需要创建
FolderState root = CreateFolder(null, item.NodePath, item.NodeName);
root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
_references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, root.NodeId));
root.EventNotifier = EventNotifiers.SubscribeToEvents;
AddRootNotifier(root);
CreateNodes(nodes, root, item.NodePath);
_folderDic.Add(item.NodePath, root);
AddPredefinedNode(SystemContext, root);
continue;
}
else
{
scadaNode.DisplayName = item.NodeName;
scadaNode.ClearChangeMasks(SystemContext, false);
}
}
//修改或创建目录(此处设计为可以有多级目录,上面是演示数据,所以我只写了三级,事实上更多级也是可以的)
var folders = nodes.Where(d => d.NodeType != NodeType.Scada && !d.IsTerminal);
foreach (var item in folders)
{
FolderState folder = null;
if (!_folderDic.TryGetValue(item.NodePath, out folder))
{
var par = GetParentFolderState(nodes, item);
folder = CreateFolder(par, item.NodePath, item.NodeName);
AddPredefinedNode(SystemContext, folder);
par.ClearChangeMasks(SystemContext, false);
_folderDic.Add(item.NodePath, folder);
}
else
{
folder.DisplayName = item.NodeName;
folder.ClearChangeMasks(SystemContext, false);
}
}
//修改或创建测点
//这里我的数据结构采用IsTerminal来代表是否是测点 实际业务中可能需要根据自身需要调整
var paras = nodes.Where(d => d.IsTerminal);
foreach (var item in paras)
{
BaseDataVariableState node = null;
if (_nodeDic.TryGetValue(item.NodeId.ToString(), out node))
{
node.DisplayName = item.NodeName;
node.Timestamp = DateTime.Now;
node.ClearChangeMasks(SystemContext, false);
}
else
{
FolderState folder = null;
if (_folderDic.TryGetValue(item.ParentPath, out folder))
{
node = CreateVariable(folder, item.NodePath, item.NodeName, DataTypeIds.Double, ValueRanks.Scalar);
AddPredefinedNode(SystemContext, node);
folder.ClearChangeMasks(SystemContext, false);
_nodeDic.Add(item.NodeId.ToString(), node);
}
}
} /*
* 将新获取到的菜单列表与原列表对比
* 如果新菜单列表中不包含原有的菜单
* 则说明这个菜单被删除了 这里也需要删除
*/
List<string> folderPath = _folderDic.Keys.ToList();
List<string> nodePath = _nodeDic.Keys.ToList();
var remNode = nodePath.Except(nodes.Where(d => d.IsTerminal).Select(d => d.NodeId.ToString()));
foreach (var str in remNode)
{
BaseDataVariableState node = null;
if (_nodeDic.TryGetValue(str, out node))
{
var parent = node.Parent;
parent.RemoveChild(node);
_nodeDic.Remove(str);
}
}
var remFolder = folderPath.Except(nodes.Where(d => !d.IsTerminal).Select(d => d.NodePath));
foreach (string str in remFolder)
{
FolderState folder = null;
if (_folderDic.TryGetValue(str, out folder))
{
var parent = folder.Parent;
if (parent != null)
{
parent.RemoveChild(folder);
_folderDic.Remove(str);
}
else
{
RemoveRootNotifier(folder);
RemovePredefinedNode(SystemContext, folder, new List<LocalReference>());
}
}
}
}
需要特别说明的是:OpcuaNode类的属性可能需要根据你们自己的业务数据来定,只要确保一点:你能根据OpcuaNode对象的集合组成对应的节点树即可,下面给出OpcuaNode类的代码,但也只能作为一个参考。
public class OpcuaNode
{
//节点路径(逐级拼接)
public string NodePath { get; set; }
//父节点路径(逐级拼接)
public string ParentPath { get; set; }
//节点编号 (在我的业务系统中的节点编号并不完全唯一,但是所有测点Id都是不同的)
public int NodeId { get; set; }
//是否端点(最底端子节点)
public string NodeName { get; set; }
//是否端点(最底端子节点)
public bool IsTerminal { get; set; }
//节点类型
public NodeType NodeType { get; set; }
}
public enum NodeType
{
//根节点
Scada = ,
//目录
Channel = ,
//目录
Device = ,
//测点
Measure =
}
客户端读取历史数据
这个部分我也没有见到实际的应用,也不太清楚具体应该是怎么实现的,仅凭我的想象,我做如下的理解:
这些历史数据也是需要我们根据条件从数据源中查询出来,查询历史数据,就必然需要限定一个时间范围,所以我的实现代码如下:
public override void HistoryRead(OperationContext context, HistoryReadDetails details,
TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints,
IList<HistoryReadValueId> nodesToRead, IList<HistoryReadResult> results, IList<ServiceResult> errors)
{
ReadProcessedDetails readDetail = details as ReadProcessedDetails;
//假设查询历史数据 都是带上时间范围的
if (readDetail == null || readDetail.StartTime == DateTime.MinValue || readDetail.EndTime == DateTime.MinValue)
{
errors[] = StatusCodes.BadHistoryOperationUnsupported;
return;
}
for (int ii = ; ii < nodesToRead.Count; ii++)
{
int sss = readDetail.StartTime.Millisecond;
double res = sss + DateTime.Now.Millisecond;
//这里 返回的历史数据可以是多种数据类型 请根据实际的业务来选择
Opc.Ua.KeyValuePair keyValue = new Opc.Ua.KeyValuePair()
{
Key = new QualifiedName(nodesToRead[ii].NodeId.Identifier.ToString()),
Value = res
};
results[ii] = new HistoryReadResult()
{
StatusCode = StatusCodes.Good,
HistoryData = new ExtensionObject(keyValue)
};
errors[ii] = StatusCodes.Good;
//切记,如果你已处理完了读取历史数据的操作,请将Processed设为true,这样OPC-UA类库就知道你已经处理过了 不需要再进行检查了
nodesToRead[ii].Processed = true;
}
}
客户端写入数据
在创建节点时,绑定节点的数据写入事件就可以实现客户端向服务端写入数据。当然,关于这些数据要怎么保存,需要根据实际的业务来做具体的实现。
private ServiceResult OnWriteDataValue(ISystemContext context, NodeState node,
NumericRange indexRange, QualifiedName dataEncoding,
ref object value, ref StatusCode statusCode, ref DateTime timestamp)
{
BaseDataVariableState variable = node as BaseDataVariableState;
try
{
//验证数据类型
TypeInfo typeInfo = TypeInfo.IsInstanceOfDataType(
value,
variable.DataType,
variable.ValueRank,
context.NamespaceUris,
context.TypeTable); if (typeInfo == null || typeInfo == TypeInfo.Unknown)
{
return StatusCodes.BadTypeMismatch;
}
if (typeInfo.BuiltInType == BuiltInType.Double)
{
double number = Convert.ToDouble(value);
value = TypeInfo.Cast(number, typeInfo.BuiltInType);
}
return ServiceResult.Good;
}
catch (Exception)
{
return StatusCodes.BadTypeMismatch;
}
}
启动服务端
当我们把OPC-UA服务端需要的功能都准备完成后,那就剩最后一步了:启动你的服务端。
var config = new ApplicationConfiguration()
{
ApplicationName = "AxiuOpcua",
ApplicationUri = Utils.Format(@"urn:{0}:AxiuOpcua", System.Net.Dns.GetHostName()),
ApplicationType = ApplicationType.Server,
ServerConfiguration = new ServerConfiguration()
{
BaseAddresses = { "opc.tcp://localhost:8020/AxiuOpcua/DemoServer", "https://localhost:8021/AxiuOpcua/DemoServer" },
MinRequestThreadCount = ,
MaxRequestThreadCount = ,
MaxQueuedRequestCount =
},
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 = },
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = },
TraceConfiguration = new TraceConfiguration()
};
config.Validate(ApplicationType.Server).GetAwaiter().GetResult();
if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
{
config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted); };
} var application = new ApplicationInstance
{
ApplicationName = "AxiuOpcua",
ApplicationType = ApplicationType.Server,
ApplicationConfiguration = config
};
//application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult();
bool certOk = application.CheckApplicationInstanceCertificate(false, ).Result;
if (!certOk)
{
Console.WriteLine("证书验证失败!");
} // start the server.
application.Start(new AxiuOpcuaServer()).Wait();
总结
我也是第一次接触OPC-UA,所做的这个服务端并不完善,只是提出来希望大家一起讨论,互相学习一下。毕竟我觉得C#在物联网方面的内容还是太少了。
关于示例程序的源码地址如下:
https://github.com/axiu233/AxiuOpcua.ServerDemo
利用C#实现OPC-UA服务端的更多相关文章
- winform客户端利用webClient实现与Web服务端的数据传输
由于项目需要,最近研究了下WebClient的数据传输.关于WebClient介绍网上有很多详细介绍,大概就是利用WebClient可以实现对Internet资源的访问.无外乎客户端发送请求,服务端处 ...
- 如何利用cURL和python对服务端和web端进行接口测试
工具描述 cURL是利用URL语法在命令行方式下工作的文件传输工具,是开源爱好者编写维护的免费工具,支持包括Windows.Linux.Mac等数十个操作系统,最新版本为7.27.0,但是我推荐大家使 ...
- 利用控制台承载SignalR作为服务端、及第三方推送信息
一 首先建立一个控制台需要引用一些组件 特别要注意引用Microsoft.Owin.Host.HttpListener别忘了这个组件,不引用他可能程序正常运行不会报错,但服务器一直开启失败(我之前就是 ...
- 利用IDEA创建Web Service服务端和客户端的详细过程
创建服务端 一.file–>new–>project 二.点击next后输入服务端名,点击finish,生成目录如下 三.在 HelloWorld.Java 文件中右击,选 WebServ ...
- 利用WebSocket和EventSource实现服务端推送
可能有很多的同学有用 setInterval 控制 ajax 不断向服务端请求最新数据的经历(轮询)看下面的代码: setInterval(function() { $.get('/get/data- ...
- 利用sorket实现聊天功能-服务端实现
工具包 package loaderman.im.util; public class Constants { public static final String SERVER_IP = " ...
- 用“MEAN”技术栈开发web应用(二)express搭建服务端框架
上一篇我们讲了如何使用angular搭建起项目的前端框架,前端抽象出一个service层来向后端发送请求,后端则返回相应的json数据.本篇我们来介绍一下,如何在nodejs环境下利用express来 ...
- 远程控制服务(SSH)之Linux环境下客户端与服务端的远程连接
本篇blog将讲述sshd服务提供的两种安全验证的方法,并且通过这两种方法进行两台Linux虚拟机之间的远程登陆. 准备工作: (1) 准备两台安装有Linux系统的虚拟机,虚拟机软件采用VM ...
- Python中的Tcp协议应用之TCP服务端-线程版
利用线程实现,一个服务端同时服务多个客户端的需求. TCP服务端-线程版代码实现: import socket import threading def handle_client_socket(ne ...
随机推荐
- ES搜索引擎-一篇文章就够了
toc: true title: 一周一个中间件-ES搜索引擎 date: 2019-09-19 18:43:36 tags: - 中间件 - 搜索引擎 前言 在众多搜索引擎中,solr,es是我所知 ...
- 谁来教我渗透测试——VMware工具安装和使用
今天我们继续渗透测试学习系列了,昨天我们看了基础概念,今天我们来学习一下渗透测试必备的功能VMware安装. 首先我们下载好VMware workstation Pro的安装包.可以在百度上直接百度下 ...
- JDK动态代理和 CGLIB 代理
JDK动态代理和 CGLIB 代理 JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期期间创建一个接口的实现类来完成对目标对象的代理. 代码示例 接口 public interface ...
- LQB201803乘积尾零
果然是练思维呀!!要是我的话估计就能挨个算一算呜呜呜 分解成 2和5相乘的式子 #include <iostream> using namespace std; //快速幂运算 int m ...
- three.js 制作一个三维的推箱子游戏
今天郭先生发现大家更喜欢看我发的three.js小作品,今天我就发一个3d版本推箱子的游戏,其实webGL有很多框架,three.js并不合适做游戏引擎,但是可以尝试一些小游戏.在线案例请点击博客原文 ...
- MacOS 键盘符号和修饰键说明
原文链接:https://www.cnblogs.com/exmyth/p/5949192.html Mac键盘符号和修饰键说明 ⌘ Command ⇧ Shift ⌥ Option ⌃ Cont ...
- Redis 的 KEYS 命令不能乱用啊
KESY 命令 时间复杂度: O(N) , 假设Redis中的键名和给定的模式的长度有限的情况下,N为数据库中key的个数. Redis Keys 命令用于查找所有符合给定模式 pattern 的 k ...
- PHP uasort() 函数
------------恢复内容开始------------ 实例 使用用户自定义的比较函数对数组 $arr 中的元素按键值进行排序: <?phpfunction my_sort($a,$b){ ...
- C/C++编程笔记:C语言贪吃蛇源代码控制台(二),分数和食物!
接上文<C/C++编程笔记:C语言贪吃蛇源代码控制台(一),会动的那种哦!>如果你在学习C语言开发贪吃蛇的话,零基础建议从上一篇开始哦!接下来正式开始吧! 三.蛇的运动 上次我已经教大家画 ...
- 牛客练习赛64 如果我让你查回文你还爱我吗 线段树 树状数组 manacher 计数 区间本质不同回文串个数
LINK:如果我让你查回文你还爱我吗 了解到了这个模板题. 果然我不会写2333... 考试的时候想到了一个非常辣鸡的 线段树合并+莫队的做法 过不了不再赘述. 当然也想到了manacher不过不太会 ...