📘 OPC UA 服务端
命名空间: Snet.Opc.ua.service | 类: OpcUaServiceOperate | 基类: CoreUnify<OpcUaServiceOperate, OpcUaServiceData.Basics> | 接口: IOn, IOff, IRead, IWrite, IStatus, IDisposable, IAsyncDisposable
基于 OPC Foundation UA SDK 实现的内置 OPC UA 服务端。可在代码中创建自定义地址空间——文件夹与地址节点——并开放给任意标准 OPC UA 客户端浏览、读取与写入。服务端同时管理会话与订阅(会话生命周期事件、订阅创建/删除事件、心跳均通过 OnInfoEventAsync 抛出)。
服务端端点为 opc.tcp://{IpAddress}:{Port}/{Tag},默认即 opc.tcp://127.0.0.1:6688/Opc.Ua.Service —— 与 Snet OPC UA 客户端 的默认连接地址一致。
快速开始
using Snet.Opc.ua.service;
using Snet.Opc.core;
using Snet.Model.data;
using Snet.Model.@enum;
using System.Collections.Concurrent;
using Opc.Ua;
var server = await OpcUaServiceOperate.InstanceAsync(new OpcUaServiceData.Basics
{
Tag = "Opc.Ua.Service",
IpAddress = "127.0.0.1",
Port = 6688,
AType = AuType.UserName,
UserName = "snet",
Password = "123456"
});
await server.OnAsync(); // 端点: opc.tcp://127.0.0.1:6688/Opc.Ua.Service
// 创建文件夹——CreateFolder 的 ResultData 即 FolderState
FolderState folder = server.CreateFolder("Factory1").GetSource<FolderState>();
// 在文件夹下创建地址节点
server.CreateAddress(new List<AddressBody>
{
new AddressBody { AddressName = "Temperature", DataType = BuiltInType.Float },
new AddressBody { AddressName = "Pressure", DataType = BuiltInType.Float },
new AddressBody { AddressName = "Status", DataType = BuiltInType.Boolean }
});
// 读取:服务端自身作为数据源(外部 UA 客户端读取这些节点)
var readAddr = new Address(new AddressDetails("Temperature", "Temperature", DataType.Float));
var result = await server.ReadAsync(readAddr);
if (result.Status)
Console.WriteLine($"Temperature = {result.GetSource<ConcurrentDictionary<string, AddressValue>>()["Temperature"].ResultValue}");
// 写入:外部客户端(或自身代码)写入服务端地址
await server.WriteAsync(new ConcurrentDictionary<string, object> { ["Temperature"] = 25.6f });
await server.OffAsync();
AddressBody/NodeBody定义在Snet.Opc.core。AddressBody没有构造函数——请用对象初始化器设置属性:AddressName、Dynamic、Description、AccessLevel、DefaultValue、DataType。
安装
dotnet add package Snet.Opc
UA 服务端协议栈随包内置。应用证书自动生成到 {BaseDir}/cer/app(也可通过 Cer/SecreKey 指定)。客户端(Snet UA 客户端或其他)连接时需信任服务端证书(Snet 客户端默认自动接受不受信任证书)。
配置(OpcUaServiceData.Basics)
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
SN |
string |
Guid.NewGuid().ToUpperNString() |
唯一标识符 |
Tag |
string |
"Opc.Ua.Service" |
服务端标识;同时是端点路径最后一段(opc.tcp://ip:port/{Tag}) |
IpAddress |
string? |
"127.0.0.1" |
绑定 IP(正则校验 IPv4)。0.0.0.0 绑定所有网卡 |
Port |
int |
6688 |
绑定端口 |
AType |
AuType |
AuType.UserName |
认证类型:Anonymous、UserName、Certificate |
UserName |
string? |
"snet" |
服务端用户名(UserName 认证与演示用户) |
Password |
string? |
"123456" |
服务端密码 |
AddressSpaceName |
string? |
"Snet" |
地址空间显示名称;同时是 CreateFolder 的根键前缀 |
Cer |
string? |
null |
服务端应用证书路径(含私钥的 PFX,可选)。必须包含 IpAddress/localhost 的 SAN 及 OPC UA ApplicationUri 扩展。为空时自动生成证书 |
SecreKey |
string? |
null |
PFX 私钥密码。必须与 Cer 成对配置(要么都配,要么都不配) |
TrustedUserPath |
string |
Path.Combine(AppContext.BaseDirectory, "cer", "trusted") |
受信任用户公钥存储路径(AType = Certificate 时使用) |
AutoCreateAddress |
bool |
true |
服务启动时自动创建地址节点(由内部节点管理器驱动) |
端点: opc.tcp://{IpAddress}:{Port}/{Tag} → 默认 opc.tcp://127.0.0.1:6688/Opc.Ua.Service。
API 参考
地址空间管理
| 方法 | 描述 |
|---|---|
OperateResult CreateFolder(string folderName, FolderState? fs = null) |
在地址空间根(fs = null)或指定文件夹下创建文件夹。重名失败。ResultData:创建的 FolderState(GetSource<FolderState>())。内部键为 {AddressSpaceName}.{name} 或 {fs.NodeId.IdentifierAsString}.{name} |
OperateResult RemoveFolder(List<NodeId> folderNameArray) |
按 NodeId 移除文件夹,并清理内部登记 |
OperateResult CreateAddress(List<AddressBody> addresses, FolderState? folderState = null) |
在文件夹(或根)下按 AddressBody 创建地址节点 |
OperateResult IncAddress(NodeBody node, FolderState? folder = null) |
按 NodeBody 结构导入单个地址(支持嵌套 Nodes)。ResultData:创建的 FolderState |
OperateResult GetAddressArray() |
获取全部已注册地址名。ResultData:List<string>(无地址时失败) |
OperateResult RemoveAddress(List<AddressBody> addressNames) |
移除地址节点 |
DAQ 操作
| 方法 | 描述 |
|---|---|
Task<OperateResult> OnAsync(CancellationToken token = default) |
构建服务端配置、生成/加载应用证书、应用用户令牌策略并启动 UA 服务端 |
Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default) |
清理地址空间、停止服务并释放资源。Dispose() / DisposeAsync() 也会调用 |
Task<OperateResult> ReadAsync(Address address, CancellationToken token = default) |
读取本服务端管理的地址节点值。ResultData:以 AddressName 为键的 ConcurrentDictionary<string, AddressValue> |
Task<OperateResult> WriteAsync(ConcurrentDictionary<string, (object value, EncodingType? encodingType)> values, CancellationToken token = default) |
写入(元组形式,委托给 object 字典重载) |
Task<OperateResult> WriteAsync(ConcurrentDictionary<string, object> values, CancellationToken token = default) |
直接写入 |
Task<OperateResult> WriteAsync(ConcurrentDictionary<string, WriteModel> values, CancellationToken token = default) |
按 WriteModel 写入(按 AddressDataType 自动转换:Byte、Bool、String、Char、Double、Float、Int16/32/64、UInt16/32/64、DateTime 等) |
Task<OperateResult> GetStatusAsync(CancellationToken token = default) |
true = "已启动"(服务端运行中) |
bool IsStart { get; private set; } |
服务端当前是否已启动 |
| 同步包装 | On()、Off(bool hardClose = false)、Read(Address)、Write(...) ×3、GetStatus() |
Address/AddressDetails/WriteAsync 键中的 AddressName 指 CreateAddress 时注册的 AddressName(如 "Temperature"),不是 NodeId——由节点管理器解析。
会话与订阅监控
会话创建/激活/结束事件、订阅创建/删除事件、以及每 5 秒的会话心跳均通过 OnInfoEventAsync(this, new EventInfoResult(true, message)) 上报,包含会话名称与 ID。
AddressBody 参考
Snet.Opc.core.AddressBody(CreateAddress 使用):
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
AddressName |
string |
string.Empty |
地址名称(节点的浏览名) |
Dynamic |
bool |
false |
是否为动态地址(值派生,不存储) |
Description |
string |
string.Empty |
节点描述 |
AccessLevel |
byte |
3 |
1 = 只读;2 = 只写;3 = 读写 |
DefaultValue |
object |
null |
初始值(Dynamic 为 true 时无需赋值) |
DataType |
BuiltInType |
— | UA 内置数据类型(Float、Bool 等) |
证书认证
客户端用户证书(AType = Certificate)
服务端按信任存储校验用户证书:
- 设置
AType = AuType.Certificate。 - 将客户端证书公钥导出为
.der,放入受信任用户存储的certs子目录:{TrustedUserPath}\certs\(SDK 目录存储要求certs子目录,否则客户端被拒BadIdentityTokenRejected)。 - 签发 CA 证书放入同级
issuer目录({TrustedUserPath 父目录}\issuer)。 - 未受信任的客户端证书在会话激活阶段被拒绝。
服务端应用证书(Cer/SecreKey)
需要使用固定服务端证书而非自动生成时:
Cer(PFX 路径)与SecreKey(PFX 密码)必须成对配置——只配其一启动失败。- 证书必须包含:私钥、有效期内、密钥长度 ≥ 2048、SAN 覆盖监听地址或
localhost、以及 OPC UA ApplicationUri 扩展(uri:{...})——严格客户端会拒绝缺失该扩展的证书。 - 启动时证书经校验后复制到 SDK 证书存储(
{cerRoot}/certs公钥、{cerRoot}/private私钥),随后用于签名。
Snet OPC UA 客户端 可以证书模式(AType.Certificate + 自身的 Cer/SecreKey)连接本服务端。
服务端限制(默认值)
最大会话数 75、最大订阅数 100、发布间隔 50/3600000 毫秒、会话超时上限 3600000 毫秒、最大发布请求数 20、支持持久订阅,节点管理器保存文件 {Tag}.Nodes.Json。
代码示例
嵌套文件夹
using Opc.Ua; // FolderState、NodeId
var factory1 = server.CreateFolder("Factory1").GetSource<FolderState>();
var line1 = server.CreateFolder("Line1", factory1).GetSource<FolderState>();
server.CreateAddress(new List<AddressBody>
{
new AddressBody { AddressName = "Temperature", DataType = BuiltInType.Float },
new AddressBody { AddressName = "Setpoint", DataType = BuiltInType.Float }
});
// 列出所有已注册地址
var names = server.GetAddressArray().GetSource<List<string>>();
使用 WriteModel 写入(自动类型转换)
await server.WriteAsync(new ConcurrentDictionary<string, WriteModel>
{
["Setpoint"] = new WriteModel { Value = 80.5, AddressDataType = DataType.Float }
});
与外部 UA 客户端的数据流
服务端是被动数据源:通过 WriteAsync(或创建时的 DefaultValue)把值推进服务端,外部 OPC UA 客户端读取并订阅变化。反向连接请使用 OPC UA 客户端。
常见问题
Q: UA 客户端用什么地址连接?
A: opc.tcp://{IpAddress}:{Port}/{Tag}——如 opc.tcp://127.0.0.1:6688/Opc.Ua.Service。确保客户端信任服务端证书。
Q: CreateFolder 提示"文件夹创建失败,已存在此同名文件夹"。
A: 同一父级下的文件夹名必须唯一(根下键为 {AddressSpaceName}.{name},嵌套为 {parent.NodeId.IdentifierAsString}.{name})。
Q: 外部客户端连不上/被拒绝。
A: 检查绑定 IP(跨网卡用 0.0.0.0)、防火墙放行 TCP 端口、客户端信任服务端证书。Certificate 认证模式下,把客户端公钥放入 {TrustedUserPath}\certs\。
Q: 服务端里的值从哪来?
A: AddressBody 创建时设置 DefaultValue,或调用 WriteAsync(自身代码或具有写权限的外部客户端均可)。
Q: 使用 Cer/SecreKey 启动报证书错误。
A: 检查 PFX 是否含私钥、是否过期、密钥长度 ≥ 2048、SAN 是否覆盖监听 IP 或 localhost、证书是否包含 OPC UA ApplicationUri 扩展(uri:...)。
相关文档
- OPC UA 客户端 — 连接本服务端的 Snet 客户端
- OPC 协议族 — 四个 OPC 组件
- 接口目录 — IOn、IOff、IRead、IWrite、IStatus
- 地址模型 — Address、AddressDetails、AddressValue
