基于OPC UA基金会:设计OPC UA 服务端-技术文档
目录
1. 工程概述
本项目是一个基于 OPC Foundation .NET Standard SDK 的 OPC UA 服务端 WinForms 程序。
核心功能:
- 启动/停止 OPC UA 服务器(可配置端口和应用名)
- 动态创建/删除/更新 OPC UA 变量节点
- 500ms 轮询刷新 UI 显示(客户端写入自动同步)
- 运行日志(终端风格黑底绿字)
- 支持本地和远程客户端连接(UaExpert、自定义客户端等)
目标框架: .NET Framework 4.8
SDK 版本: OPCFoundation.NetStandard.Opc.Ua v1.5.376.244
2. 环境依赖
2.1 NuGet 包
Install-Package OPCFoundation.NetStandard.Opc.Ua.Server -Version 1.5.376.244
Install-Package OPCFoundation.NetStandard.Opc.Ua.Configuration -Version 1.5.376.244
Install-Package OPCFoundation.NetStandard.Opc.Ua.Security.Certificates -Version 1.5.376.244
Server 包会自动拉取 Core、Client、BouncyCastle、Newtonsoft.Json 等。
2.2 引用命名空间
using Opc.Ua; // OPC UA 基础类型
using Opc.Ua.Server; // 服务端 API(StandardServer, CustomNodeManager2, INodeManager 等)
using Opc.Ua.Configuration; // 应用配置(ApplicationConfiguration, CertificateFactory)
3. 项目文件结构
OPC_UA_Claude_Server/
├── Program.cs // 程序入口
├── OPC_UA_Server.cs // ★ 主窗体(启停逻辑 + 变量管理 UI)★
├── OPC_UA_Server.Designer.cs // 窗体控件声明
└── MyNodeManager.cs // ★ 自定义节点管理器(地址空间核心)★
核心就两个文件:OPC_UA_Server.cs(UI 层)和 MyNodeManager.cs(OPC UA 地址空间层)。
4. UI 界面布局
┌──────────────────────────────────────────────────────────┐
│ 服务端控制 │
│ 端口: [4841] 应用名: [MyOpcUaServer] │
│ [启动服务] [停止服务] ● 已停止/运行中 │
│ 端点: [opc.tcp://DESKTOP-XXX:4841] │
├─────────────────────────┬────────────────────────────────┤
│ 变量管理 │ 运行日志 │
│ ┌──────────┬──────┬───┐ │ ┌────────────────────────────┐ │
│ │ 变量名 │ 类型 │值 │ │ │ [10:30:01] 服务器已启动 │ │
│ │(只读表格) │ │ │ │ │ [10:30:15] 客户端已连接 │ │
│ │Tempera..│Int32│25 │ │ │ [10:30:20] 写入: Temp=30 │ │
│ └──────────┴──────┴───┘ │ └────────────────────────────┘ │
│ [删除选中] │ │
│ ◆ 新建变量 │ │
│ 变量名:[__]类型:[Int32▼]│ │
│ 初始值:[__] [添加] │ │
│ ◆ 更新变量值 │ │
│ 当前值:[___]新值:[___] │ │
│ [更新] │ │
└─────────────────────────┴────────────────────────────────┘
5. 核心概念
5.1 OPC UA 服务端架构
┌──────────────────────────────────────────┐
│ StandardServer(服务端主类) │
│ ├── 管理 TCP 监听器 │
│ ├── 管理安全通道 │
│ └── 管理会话 │
├──────────────────────────────────────────┤
│ MasterNodeManager(主节点管理器) │
│ ├── CoreNodeManager(核心命名空间 ns=0)│
│ │ └── Objects, Types, Views 等 │
│ ├── MyNodeManager(自定义命名空间 ns=2)│
│ │ └── MyVariables → 变量节点 │
│ └── 协调跨命名空间引用 │
└──────────────────────────────────────────┘
5.2 地址空间树结构
Root (ns=0)
├── Views (ns=0, 核心管理)
├── Objects (ns=0, 核心管理)
│ ├── Server (ns=0)
│ └── My Variables (ns=2, 自定义节点管理器) ← 通过跨空间引用挂载
│ ├── Temperature (ns=2, Organizes 引用)
│ ├── Pressure (ns=2)
│ └── Status (ns=2)
└── Types (ns=0)
5.3 关键对象说明
| 对象 | 类型 | 说明 |
|---|---|---|
m_server |
StandardServer |
OPC UA 服务端主实例 |
m_nodeManager |
MyNodeManager |
自定义节点管理器 |
m_configuration |
ApplicationConfiguration |
应用配置 |
FolderState |
FolderState |
文件夹节点(容器) |
BaseDataVariableState |
BaseDataVariableState |
变量节点(可读写) |
INodeManagerFactory |
INodeManagerFactory |
工厂接口,StandardServer 通过它创建节点管理器 |
Organizes |
ReferenceTypeIds.Organizes |
组织引用类型(文件夹→内容) |
5.4 命名空间
OPC UA 每个节点管理器管理一个或多个命名空间。不同命名空间的节点通过"外部引用"链接:
| 命名空间索引 | 命名空间 URI | 管理者 |
|---|---|---|
| 0 | http://opcfoundation.org/UA/ |
CoreNodeManager(SDK 内置) |
| 1 | http://opcfoundation.org/UA/Diagnostics |
SDK 内置 |
| 2 | http://myopcuaserver/MyNodeManager/ |
MyNodeManager(自定义) |
5.5 NodeId 命名规则
本服务端变量 NodeId:
ns=2;s=Temperature → 命名空间2,字符串型 "Temperature"
ns=2;s=Pressure → 命名空间2,字符串型 "Pressure"
ns=2;s=MyVariables → 命名空间2,字符串型 "MyVariables"(根文件夹)
6. 服务启动流程
用户点击 [启动服务]
│
├── 步骤1: BuildServerConfiguration()
│ ├── 创建 ApplicationConfiguration
│ │ ├── 应用名 + URI + 端口
│ │ ├── 安全策略(None + SignAndEncrypt)
│ │ ├── 证书配置
│ │ └── 传输配额 + 线程池设置
│ ├── config.Validate(ApplicationType.Server)
│ └── FindOrCreateCertificate() → 创建/查找自签名证书
│
├── 步骤2: 收集 DataGridView 变量
│ 遍历表格每一行 → 构建 List<VariableDef>
│ VariableDef.Name = 变量名 (如 "Temperature")
│ VariableDef.Type = BuiltInType.Int32
│ VariableDef.Value = 25
│
├── 步骤3: 创建服务器并注册节点管理器
│ m_server = new StandardServer()
│ factory = new MyNodeManagerFactory(initialVars)
│ m_server.AddNodeManager(factory)
│
├── 步骤4: m_server.Start(m_configuration)
│ 内部流程:
│ MasterNodeManager.Startup()
│ → 创建 CoreNodeManager(管理 ns=0 核心节点)
│ → factory.Create(server, config)
│ → new MyNodeManager(server, config, initialVars)
│ → nodeManager.CreateAddressSpace(externalReferences)
│ → base.CreateAddressSpace(externalReferences)
│ → LoadPredefinedNodes(context)
│ → 我们的重写:创建 MyVariables 文件夹 + 变量节点
│ → AddPredefinedNode(每个节点) ← 递归处理子节点
│ → AddReverseReferences(externalReferences)
│ → 扫描节点引用 → 发现跨命名空间引用
│ → 填充 externalReferences 字典
│ → 处理 externalReferences(核心里添加 Organizes 引用)
│ → 启动 TCP 监听器
│
└── 步骤5: 更新 UI + 启动刷新定时器
6.1 BuildServerConfiguration — 构建配置
private void BuildServerConfiguration(string appName, int port)
{
string machineName = Environment.MachineName;
m_configuration = new ApplicationConfiguration
{
ApplicationName = appName,
ApplicationUri = $"urn:{machineName}:{appName}",
ApplicationType = ApplicationType.Server, // ★ 标记为服务端
ProductUri = "https://github.com",
SecurityConfiguration = new SecurityConfiguration
{
AutoAcceptUntrustedCertificates = false,
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.X509Store,
StorePath = "CurrentUser\\My", // 证书存 Windows 个人存储
SubjectName = $"CN={appName}"
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.X509Store,
StorePath = "CurrentUser\\Root" // 信任客户端证书的位置
}
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 360000,
MaxStringLength = 67108864
},
// ★ 服务端专属配置 ★
ServerConfiguration = new ServerConfiguration
{
// 监听地址:opc.tcp://主机名:端口
BaseAddresses = new StringCollection
{
$"opc.tcp://{machineName}:{port}"
},
// 支持的安全策略
SecurityPolicies = new ServerSecurityPolicyCollection
{
new ServerSecurityPolicy
{
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri =
"http://opcfoundation.org/UA/SecurityPolicy#None"
},
new ServerSecurityPolicy
{
SecurityMode = MessageSecurityMode.SignAndEncrypt,
SecurityPolicyUri =
"http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"
}
},
MinRequestThreadCount = 5,
MaxRequestThreadCount = 100,
MaxQueuedRequestCount = 200
}
};
// 验证配置 + 创建证书
m_configuration.Validate(ApplicationType.Server);
FindOrCreateCertificate(appName);
}
6.2 服务端证书
服务端必须有自己的 X.509 证书用于 TLS 握手和身份标识。逻辑与客户端创建证书相同:
private void FindOrCreateCertificate(string appName)
{
var certId = m_configuration.SecurityConfiguration.ApplicationCertificate;
var certTask = certId.Find(true);
certTask.Wait();
if (certTask.Result != null) return; // 已有证书
// 创建 RSA 2048 位自签名证书,有效期 24 个月
var localIps = new List<string>();
// ... 收集本机 IP ...
var builder = CertificateFactory.CreateCertificate(
m_configuration.ApplicationUri, appName, null, localIps);
X509Certificate2 newCert = builder
.SetNotBefore(DateTime.Now)
.SetNotAfter(DateTime.Now.AddMonths(24))
.SetHashAlgorithm(X509Utils.GetRSAHashAlgorithmName(256))
.SetRSAKeySize(2048)
.CreateForRSA();
newCert.FriendlyName = appName;
newCert.AddToStore("X509Store", "CurrentUser\\My", null);
}
6.3 启动服务器
// 从 DataGridView 收集初始变量
var initialVars = new List<VariableDef>();
foreach (DataGridViewRow row in Variable_dGV.Rows)
{
if (row.IsNewRow) continue;
initialVars.Add(new VariableDef
{
Name = row.Cells[0].Value?.ToString(),
Type = MyNodeManager.MapBuiltInType(row.Cells[1].Value?.ToString()),
Value = MyNodeManager.ConvertValue(row.Cells[2].Value?.ToString(), type)
});
}
// 创建服务器 + 注册节点管理器工厂 + 启动
m_server = new StandardServer();
var factory = new MyNodeManagerFactory(initialVars);
m_server.AddNodeManager(factory); // ← 注册工厂(不是节点管理器本身!)
m_server.Start(m_configuration); // ← 内部:factory.Create → CreateAddressSpace
m_nodeManager = factory.NodeManager; // ← 启动后才能拿到节点管理器实例
关键注意:
AddNodeManager()参数类型是INodeManagerFactory,不是INodeManager- 真正的节点管理器在
server.Start()内部通过工厂创建 - 启动后才能从
factory.NodeManager获取实例引用
7. 地址空间架构
7.1 CustomNodeManager2 继承链
INodeManager (接口)
└── CustomNodeManager2 (SDK 提供的简化基类)
└── MyNodeManager (我们的实现)
CustomNodeManager2 提供了:
AddPredefinedNode()— 将节点注册到地址空间AddReverseReferences()— 自动处理跨命名空间引用PredefinedNodes— 节点存储字典(NodeId → NodeState)SystemContext— 服务端系统上下文Lock— 线程安全锁NamespaceIndex— 当前命名空间索引
7.2 节点创建流程(官方标准模式)
LoadPredefinedNodes(context) ← 我们重写此方法
↓
返回 NodeStateCollection(包含所有要创建的节点)
↓
SDK 框架对每个节点调用 AddPredefinedNode()
├── 节点加入 PredefinedNodes 字典
├── 如果是类型节点 → 注册到类型树
└── 递归处理子节点
↓
SDK 框架调用 AddReverseReferences(externalReferences)
├── 扫描所有 PredefinedNodes 的引用
├── 发现跨命名空间引用 → 添加到 externalReferences
└── 核心节点管理器读取 externalReferences 添加引用
7.3 引用类型对比
| 引用类型 | 用途 | 浏览可见? |
|---|---|---|
Organizes |
文件夹→内容(组织关系) | 是(HierarchicalReferences) |
HasComponent |
对象→组件(包含关系) | 是(HierarchicalReferences子类型) |
HasProperty |
对象→属性 | 是 |
使用 Organizes:本服务端使用 Organizes 管理文件夹和变量之间的层级关系。这是 OPC UA 标准推荐的方式:
MyVariables → Organizes(inverse) → Objects(MyVariables 是 Objects 的子节点)MyVariables → Organizes(forward) → Temperature(Temperature 是 MyVariables 的子节点)
8. 节点管理器详解
8.1 完整代码(带注释)
public class MyNodeManager : CustomNodeManager2
{
private Dictionary<string, BaseDataVariableState> m_variables;
private FolderState m_rootFolder;
private List<VariableDef> m_initialVars;
// ★ 构造函数通过 base() 注册命名空间 URI ★
public MyNodeManager(IServerInternal server, ApplicationConfiguration config,
List<VariableDef> initialVars)
: base(server, config, "http://myopcuaserver/MyNodeManager/")
{
m_variables = new Dictionary<string, BaseDataVariableState>();
m_initialVars = initialVars ?? new List<VariableDef>();
}
// ★ 核心方法:SDK 框架调用,返回要创建的所有节点 ★
protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
{
var nodes = new NodeStateCollection();
// 1. 创建根文件夹 "My Variables"
m_rootFolder = new FolderState(null) // null = 顶层节点
{
NodeId = new NodeId("MyVariables", NamespaceIndex),
BrowseName = new QualifiedName("MyVariables", NamespaceIndex),
DisplayName = new LocalizedText("My Variables"),
TypeDefinitionId = ObjectTypeIds.FolderType
};
// ★ 反向引用:MyVariables → Organizes(inverse) → Objects
// 这样浏览 Objects 时能看到 MyVariables 文件夹
m_rootFolder.AddReference(
ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder);
nodes.Add(m_rootFolder);
// 2. 创建变量节点
foreach (var def in m_initialVars)
{
// ★ 重要:先 null 父节点创建,设好 NodeId,再手动加引用
// 不能 new BaseDataVariableState(parent),因为构造器内
// parent.AddChild(this) 会在 C# 初始化器赋值 NodeId 之前执行
var variable = new BaseDataVariableState(null)
{
NodeId = new NodeId(def.Name, NamespaceIndex),
BrowseName = new QualifiedName(def.Name, NamespaceIndex),
DisplayName = new LocalizedText(def.Name),
DataType = (uint)def.Type, // 数据类型
Value = def.Value, // 初始值
ValueRank = ValueRanks.Scalar, // 标量(非数组)
AccessLevel = AccessLevels.CurrentReadOrWrite,
UserAccessLevel = AccessLevels.CurrentReadOrWrite
};
// ★ 双向引用:MyVariables ← Organizes → variable ★
m_rootFolder.AddReference(
ReferenceTypeIds.Organizes, false, variable.NodeId);
variable.AddReference(
ReferenceTypeIds.Organizes, true, m_rootFolder.NodeId);
nodes.Add(variable);
m_variables[def.Name] = variable;
}
return nodes;
}
// ★ CreateAddressSpace 只需要调用 base,框架内部处理所有逻辑 ★
public override void CreateAddressSpace(
IDictionary<NodeId, IList<IReference>> externalReferences)
{
lock (Lock) { base.CreateAddressSpace(externalReferences); }
}
}
8.2 关键坑点:NodeId 初始化顺序
// ★★★ 错误写法 ★★★
// C# 初始化器 { } 在构造函数之后执行,但 BaseDataVariableState(parent)
// 构造函数内部就调用了 parent.AddChild(this),此时 NodeId 还是默认值!
var variable = new BaseDataVariableState(m_rootFolder) // ← 构造器调用 AddChild(NodeId=null)
{
NodeId = new NodeId("Temperature", NamespaceIndex), // ← 初始化器:此时才赋值 NodeId
Value = 25
};
// ★★★ 正确写法 ★★★
// 先 null 父节点创建(不会触发 AddChild),设好所有属性,再手动挂到父节点
var variable = new BaseDataVariableState(null) // ← 构造器不调用 AddChild(父为 null)
{
NodeId = new NodeId("Temperature", NamespaceIndex), // ← 赋值完成
Value = 25
};
// 手动添加引用(此时 NodeId 已经正确赋值)
m_rootFolder.AddReference(ReferenceTypeIds.Organizes, false, variable.NodeId);
variable.AddReference(ReferenceTypeIds.Organizes, true, m_rootFolder.NodeId);
8.3 跨命名空间引用机制
我们的节点 (ns=2) 核心节点 (ns=0)
│ │
│ m_rootFolder.AddReference │
│ (Organizes, true, │
│ ObjectIds.ObjectsFolder) │
│─────────────────────────────→│ 标记了"MyVariables 属于 Objects"
│ │
│ AddReverseReferences 扫描 │
│ 发现这个跨空间引用 │
│ │
│ externalReferences 字典 │
│ [ObjectsFolder] = [ │
│ Organizes(forward) → │
│ MyVariables │
│ ] │
│ │
│ MasterNodeManager 调用 │
│ CoreNodeManager. │
│ AddReferences( │
│ externalReferences) │
│─────────────────────────────→│ Objects 添加了
│ │ Organizes → MyVariables
│ │
↓ ↓
客户端浏览 Objects 时能看到 MyVariables 文件夹!
9. 变量生命周期管理
9.1 启动时创建(LoadPredefinedNodes)
服务启动时,SDK 调用 LoadPredefinedNodes() → 创建所有初始变量。
9.2 运行时动态添加(AddVariable)
服务器运行期间可以动态添加变量:
public void AddVariable(string name, BuiltInType dataType, object initialValue)
{
lock (Lock)
{
if (m_variables.ContainsKey(name)) return;
var variable = new BaseDataVariableState(null)
{
NodeId = new NodeId(name, NamespaceIndex),
// ... 设置属性
};
// 挂到 MyVariables 文件夹
m_rootFolder.AddReference(
ReferenceTypeIds.Organizes, false, variable.NodeId);
variable.AddReference(
ReferenceTypeIds.Organizes, true, m_rootFolder.NodeId);
// ★ 注册到地址空间(使客户端可发现)
AddPredefinedNode(SystemContext, variable);
m_variables[name] = variable;
}
}
9.3 运行时更新值(UpdateVariableValue)
public void UpdateVariableValue(string name, object value)
{
lock (Lock)
{
if (m_variables.TryGetValue(name, out var variable))
{
variable.Value = value;
// ★ 清除变更掩码 → 通知所有订阅此变量的客户端 ★
variable.ClearChangeMasks(SystemContext, false);
}
}
}
9.4 运行时删除(RemoveVariable)
public void RemoveVariable(string name)
{
lock (Lock)
{
if (m_variables.TryGetValue(name, out var variable))
{
PredefinedNodes.Remove(variable.NodeId); // 从地址空间移除
m_variables.Remove(name); // 从本地字典移除
}
}
}
10. UI 与 OPC UA 数据同步
10.1 数据流向
┌───────────────────────────────────────────────────────┐
│ 服务端 UI │
│ │
│ 服务端手动更新值 │
│ [更新] 按钮 │
│ → m_nodeManager.UpdateVariableValue(name, value) │
│ → 客户端订阅者自动收到推送 │
│ │
│ 客户端写入数据 │
│ → OPC UA 栈自动更新节点值 │
│ → 500ms 定时器轮询 │
│ → RefreshTimer_Tick() │
│ → m_nodeManager.GetVariableValue(name) │
│ → 更新 DataGridView │
└───────────────────────────────────────────────────────┘
10.2 轮询定时器
// 构造器中初始化
m_refreshTimer = new Timer();
m_refreshTimer.Interval = 500; // 每 500ms 刷新一次
m_refreshTimer.Tick += RefreshTimer_Tick;
m_refreshTimer.Start(); // 服务启动时开始
m_refreshTimer.Stop(); // 服务停止时停止
// 定时器回调
private void RefreshTimer_Tick(object sender, EventArgs e)
{
if (m_nodeManager == null) return;
foreach (DataGridViewRow row in Variable_dGV.Rows)
{
if (row.IsNewRow) continue;
string name = row.Cells[0].Value?.ToString();
if (string.IsNullOrEmpty(name)) continue;
// 从 OPC UA 节点读取当前值
object val = m_nodeManager.GetVariableValue(name);
string display = val?.ToString() ?? "";
if (val is bool b) display = b ? "True" : "False";
// 有变化才更新(减少闪烁)
if (row.Cells[2].Value?.ToString() != display)
row.Cells[2].Value = display;
}
}
10.3 DataGridView 设为只读
this.Variable_dGV.ReadOnly = true; // 防止用户直接在表格编辑
变量的值只能通过"更新变量值"区域的按钮更新,或者通过 OPC UA 客户端写入后由定时器同步。
11. 证书管理
11.1 证书存储位置
| 证书类型 | Windows 存储路径 | 说明 |
|---|---|---|
| 服务端证书 | CurrentUser\My |
标识服务端身份的自签名证书 |
| 受信任的客户端 | CurrentUser\Root |
被信任的客户端证书 |
可通过 certmgr.msc 查看和管理证书。
11.2 防火墙配置
服务端监听 TCP 端口,需要 Windows 防火墙允许入站连接:
netsh advfirewall firewall add rule name="OPC UA Server 4841" dir=in action=allow protocol=TCP localport=4841
12. 完整代码清单
MyNodeManager.cs — 自定义节点管理器
核心要点:
- 继承
CustomNodeManager2 - 构造函数通过
base(server, config, "namespaceUri")注册命名空间 - 重写
LoadPredefinedNodes(context)返回NodeStateCollection - 变率先用 null 父节点创建,再手动加
Organizes引用 - 重写
CreateAddressSpace只需调用base.CreateAddressSpace
MyNodeManagerFactory — 工厂类
public class MyNodeManagerFactory : INodeManagerFactory
{
private List<VariableDef> m_initialVars;
public MyNodeManager NodeManager { get; private set; }
public MyNodeManagerFactory(List<VariableDef> initialVars)
{
m_initialVars = initialVars;
}
// ★ 返回命名空间 URI 列表(服务端据此注册命名空间)
public StringCollection NamespacesUris
{
get { return new StringCollection { "http://myopcuaserver/MyNodeManager/" }; }
}
// ★ SDK 框架调用:创建节点管理器实例
public INodeManager Create(IServerInternal server, ApplicationConfiguration config)
{
NodeManager = new MyNodeManager(server, config, m_initialVars);
return NodeManager;
}
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)