OPC UA 服务端 - Snet Docs

📘 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.coreAddressBody 没有构造函数——请用对象初始化器设置属性:AddressNameDynamicDescriptionAccessLevelDefaultValueDataType

安装

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 认证类型:AnonymousUserNameCertificate
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:创建的 FolderStateGetSource<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 键中的 AddressNameCreateAddress 时注册的 AddressName(如 "Temperature"),不是 NodeId——由节点管理器解析。

会话与订阅监控

会话创建/激活/结束事件、订阅创建/删除事件、以及每 5 秒的会话心跳均通过 OnInfoEventAsync(this, new EventInfoResult(true, message)) 上报,包含会话名称与 ID。

AddressBody 参考

Snet.Opc.core.AddressBodyCreateAddress 使用):

属性 类型 默认值 描述
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

服务端按信任存储校验用户证书:

  1. 设置 AType = AuType.Certificate
  2. 将客户端证书公钥导出为 .der,放入受信任用户存储的 certs 子目录:{TrustedUserPath}\certs\(SDK 目录存储要求 certs 子目录,否则客户端被拒 BadIdentityTokenRejected)。
  3. 签发 CA 证书放入同级 issuer 目录({TrustedUserPath 父目录}\issuer)。
  4. 未受信任的客户端证书在会话激活阶段被拒绝。

服务端应用证书(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:...)。

相关文档