📘 OPC UA 客户端
命名空间: Snet.Opc.ua.client | 类: OpcUaClientOperate | 基类: DaqAbstract<OpcUaClientOperate, OpcUaClientData.Basics> | 接口: IDaq + IOu
基于 OPC Foundation UA SDK 实现的完整 OPC UA 客户端。可连接任意标准 OPC UA 服务端,浏览节点树、读写节点值、订阅数据变化。支持匿名、用户名、X.509 证书三种认证方式,会话保活与自动重连,重连后自动恢复订阅。
Snet.Opc.ua.client
├── OpcUaClientOperate // 客户端(入口)
├── OpcUaClientData // Basics 配置 + Steps 枚举
├── OpcUaClientController // 多会话控制器,用于大规模点位
└── unility/ // 内部工具
快速开始
using Snet.Opc.ua.client;
using Snet.Model.data;
using Snet.Model.@enum;
using Snet.Utility;
using System.Collections.Concurrent;
using Opc.Ua;
var op = await OpcUaClientOperate.InstanceAsync(new OpcUaClientData.Basics
{
ServerUrl = "opc.tcp://127.0.0.1:6688/Opc.Ua.Service",
AType = AuType.UserName,
UserName = "snet",
Password = "123456"
});
await op.OnAsync();
// 读取多个节点——AddressName 即 UA NodeId 字符串
var address = new Address(new List<AddressDetails>
{
new("Temperature", DataType.Float),
new("Pressure", DataType.Float)
});
var result = await op.ReadAsync(address);
if (result.Status)
{
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
// 写入——字典键为 NodeId 字符串
var writeResult = await op.WriteAsync(new ConcurrentDictionary<string, object>
{
["ns=2;s=Temperature"] = 25.6f
});
// 订阅——先绑定事件再订阅
op.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"事件数据: {kv.Key} = {kv.Value.ResultValue}");
}
};
await op.SubscribeAsync(new Address(new AddressDetails("Temperature", DataType.Float)));
// OPC UA 专用:浏览节点树、获取节点元数据
object? nodeId = await op.GetNodeIDAsync(); // ObjectsFolder NodeId(未连接时为 null)
var references = await op.GetAllNodeAsync(nodeId!); // object → GetSource<List<ReferenceDescription>>()
var firstNode = references.GetSource<List<ReferenceDescription>>()[0];
var iconType = await op.GetNodeIconTypeAsync(firstNode, nodeId!);
await op.DisposeAsync();
安装
dotnet add package Snet.Opc
OPC UA 协议栈(OPC Foundation UA SDK)随包内置,无需单独安装。首次连接时应用证书会自动生成到应用证书目录({BaseDir}/cer/app)。
配置(OpcUaClientData.Basics)
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
SN |
string |
Guid.NewGuid().ToUpperNString() |
唯一标识符 |
ServerUrl |
string |
"opc.tcp://127.0.0.1:6688/Opc.Ua.Service" |
服务端端点地址(opc.tcp://主机:端口/路径) |
AType |
AuType |
AuType.UserName |
认证类型:Anonymous、UserName、Certificate |
UserName |
string? |
"snet" |
用户名(AType = UserName 时必填) |
Password |
string? |
"123456" |
密码(AType = UserName 时必填) |
Cer |
string? |
null |
客户端证书路径(AType = Certificate 时必填) |
SecreKey |
string? |
null |
证书私钥密码 |
CustomName |
string? |
Guid.NewGuid().ToLowerNString() |
客户端应用名称(唯一),显示在服务端 |
Timeout |
int |
5000 |
操作超时(毫秒) |
SessionLifeTime |
uint |
60000 |
会话生命周期(毫秒) |
InterruptReconnection |
bool |
true |
断线自动重连(保活机制监控) |
ReconnectionInterval |
int |
5000 |
重连间隔(毫秒);同时作为会话保活间隔 |
SamplingInterval |
int |
100 |
监控项采样间隔(毫秒) |
PublishingInterval |
int |
100 |
订阅发布间隔(毫秒) |
KeepAliveCount |
int |
10 |
发布周期内最多可跳过的次数,超过后服务端必须发送空保活消息 |
LifetimeCount |
int |
30 |
无新请求时最多允许的发布周期数,超过后服务端判定客户端失活并清除订阅。必须至少为 KeepAliveCount 的 3 倍 |
QueueSize |
int |
1020(byte.MaxValue * 4) |
监控项队列大小 |
SubscribeSingleGroupMaxCount |
int |
0 |
订阅单组最大数;0 = 不分组(单组),如 100 = 1000 个点位自动分成 10 组订阅 |
TaskNumber |
int |
5 |
订阅通知处理任务数(并发) |
AuType(Snet.Opc.core.Data.AuType):Anonymous — 匿名;UserName — 使用 UserName/Password;Certificate — 使用 Cer/SecreKey 指定的 X.509 客户端证书。
API 参考
IDaq 生命周期(继承)
| 方法 | 描述 |
|---|---|
Task<OperateResult> OnAsync(CancellationToken token = default) |
创建会话并连接。构建 UA 应用、安全配置与用户身份,选择最佳匹配端点,启动通知任务池 |
Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default) |
关闭会话、移除全部订阅、停止任务并释放遥测资源 |
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) |
批量写入节点值;键为 NodeId 字符串(支持虚拟地址) |
Task<OperateResult> SubscribeAsync(Address address, CancellationToken token = default) |
订阅节点变化。设置 SubscribeSingleGroupMaxCount 时自动分组;先绑定 OnDataEventAsync |
Task<OperateResult> UnSubscribeAsync(Address address, CancellationToken token = default) |
移除指定节点的订阅 |
Task<OperateResult> GetStatusAsync(CancellationToken token = default) |
会话已连接时返回 true("已连接") |
Task<OperateResult> GetBaseObjectAsync(CancellationToken token = default) |
ResultData:底层 ISession |
ISessionFactory SessionFactory { get; set; } |
会话工厂(默认为 DefaultSessionFactory,可注入以便测试) |
IOu — OPC UA 专用方法
IOu(Snet.Model.@interface)继承 IDaq,增加 7 个 UA 专用方法——全部为异步(返回 Task;参数与结果以 object 传递,用 GetSource<T>() 解包):
| 方法 | 返回 | 描述 |
|---|---|---|
Task<object?> GetNodeIDAsync() |
NodeId 或 null |
服务端的 ObjectsFolder NodeId(未连接时返回 null) |
Task<object> GetAllNodeAsync(object nodeId) |
ReferenceDescriptionCollection |
浏览 nodeId 的子节点(正向 Aggregates + Organizes 引用,Object/Variable/Method 类) |
Task<string> GetNodeIconTypeAsync(object target, object sourceId) |
string |
节点图标类型:"Scalar" / "OneDimension" / "TwoDimensions"(变量)、"ObjectsFolder"(根)、"Method"、"Default" |
Task<object> DetailedReadAllNodeDataAsync(object nodeIds) |
DataValue[] |
批量读取 List<NodeId> 的 NodeClass、Value、AccessLevel、DisplayName、Description |
Task<string> GetAccessLevelAsync(object value) |
string |
数据值的访问级别:None、CurrentRead、CurrentWrite、CurrentReadOrWrite、HistoryRead、HistoryWrite、HistoryReadOrWrite、SemanticChange、StatusWrite、TimestampWrite |
Task<DataType> TypeConvertAsync(object builtInType) |
DataType |
UA BuiltInType → Snet DataType(Boolean→Bool、Byte→Byte、Int16/32/64、UInt16/32/64、Float、Double,其余→String) |
Task<object> GetNodeValueTypeAsync(object nodeId) |
BuiltInType |
节点的 UA 内置数据类型(需已连接会话) |
OpcUaClientOperate 类还保留同名去掉 Async 后缀的同步包装(GetNodeID()、GetAllNode(...)、GetNodeIconType(...)、DetailedReadAllNodeData(...)、GetAccessLevel(...)、TypeConvert(...)、GetNodeValueType(...)),它们转发到异步实现;另有接收真实 UA 类型与 CancellationToken 的类型化重载——参见 其他公共成员。
其他公共成员
| 成员 | 描述 |
|---|---|
Task<OperateResult> AddSubscribeAsync(ConcurrentDictionary<string, Address> param, CancellationToken token = default) / AddSubscribe(...) |
批量订阅:字典键为订阅标签(一个标签可容纳多个节点;重复添加会跳过已订阅的启用节点) |
Task<OperateResult> RemoveSubscribeAsync(Address nodes, CancellationToken token = default) / RemoveSubscribe(...) |
按节点移除订阅;节点不存在也返回成功 |
Task<OperateResult> DeleteNodeAsync(string key, CancellationToken token = default) / DeleteNode(...) |
删除服务端节点(服务端允许时) |
Task<object> GetNodeIdValue(NodeId nodeId, CancellationToken ct) |
读取单个节点的当前值 |
Task<object?> GetNodeIDAsync(CancellationToken ct = default) |
GetNodeIDAsync() 的类型化重载(ObjectsFolder NodeId,未连接时为 null) |
Task<List<ReferenceDescription>> GetAllNodeAsync(NodeId nodeId, CancellationToken ct = default) |
GetAllNodeAsync(object) 的类型化重载:浏览 nodeId 的子节点并返回类型化列表(未连接时抛出 BadServerNotConnected) |
Task<string> GetNodeIconTypeAsync(ReferenceDescription target, NodeId sourceId, CancellationToken ct = default) |
GetNodeIconTypeAsync(object, object) 的类型化重载 |
Task<DataValue[]> DetailedReadAllNodeDataAsync(List<NodeId> nodeIds, CancellationToken ct = default) |
DetailedReadAllNodeDataAsync(object) 的类型化重载(未连接时抛出 BadServerNotConnected) |
Task<string> GetAccessLevelAsync(DataValue value, CancellationToken ct = default) |
GetAccessLevelAsync(object) 的类型化重载 |
Task<DataType> TypeConvertAsync(BuiltInType builtInType, CancellationToken ct = default) |
TypeConvertAsync(object) 的类型化重载 |
Task<object> GetNodeValueTypeAsync(NodeId nodeId, CancellationToken ct = default) |
GetNodeValueTypeAsync(object) 的类型化重载 |
dynamic GetValue(string value, BuiltInType builtInType) |
将字符串按 UA 内置类型解析为对应 CLR 类型(用于写入准备) |
dynamic GetArrayValue(IList<string> values, BuiltInType builtInType) |
将字符串列表解析为对应 CLR 数组 |
OpcUaClientController — 多会话扩展
当单会话无法承载大规模点位时,OpcUaClientController 将读取与订阅节点分摊到多个 OpcUaClientOperate 会话:
var controller = new OpcUaClientController(
ServerBasics: basics, // OpcUaClientData.Basics
ServerMaxSession: 10, // UA 服务端允许的最大会话数
Nodes: readAddress, // Address
SubscriptionNodes: subAddress); // Address(可选)
ConcurrentDictionary<OpcUaClientOperate, object> clients = controller.Get();
// key = 客户端实例,value = Address 或 (readAddress, subAddress) 元组
foreach (var client in clients.Keys)
await client.OnAsync();
代码示例
浏览节点树
using Opc.Ua; // ReferenceDescription、NodeId
using Snet.Utility; // 对 object 结果调用 GetSource<T> 的扩展方法
object? nodeId = await op.GetNodeIDAsync();
if (nodeId == null) return;
// ObjectsFolder(根)的子节点
var references = await op.GetAllNodeAsync(nodeId);
foreach (var reference in references.GetSource<List<ReferenceDescription>>())
{
Console.WriteLine($"NodeId={reference.NodeId} Name={reference.DisplayName.Text} Class={reference.NodeClass}");
// 树形控件用的图标类型
string icon = await op.GetNodeIconTypeAsync(reference, nodeId);
Console.WriteLine($" icon: {icon}");
}
节点元数据与数据类型
using Opc.Ua; // NodeId、DataValue、BuiltInType
using Snet.Utility; // 对 object 结果调用 GetSource<T> 的扩展方法
var nodeId = new NodeId("ns=2;s=Temperature");
// 详细属性:NodeClass、Value、AccessLevel、DisplayName、Description
DataValue[] values = (await op.DetailedReadAllNodeDataAsync(new List<NodeId> { nodeId }))
.GetSource<DataValue[]>();
Console.WriteLine($"访问级别: {await op.GetAccessLevelAsync(values[2])}");
// 节点数据类型
BuiltInType type = (BuiltInType)await op.GetNodeValueTypeAsync(nodeId);
DataType dataType = await op.TypeConvertAsync(type);
// 从字符串准备正确类型的写入值
dynamic parsed = op.GetValue("25.6", type);
await op.WriteAsync(new ConcurrentDictionary<string, object> { ["ns=2;s=Temperature"] = parsed });
分组订阅
var op = await OpcUaClientOperate.InstanceAsync(new OpcUaClientData.Basics
{
ServerUrl = "opc.tcp://127.0.0.1:6688/Opc.Ua.Service",
AType = AuType.UserName,
UserName = "snet",
Password = "123456",
SubscribeSingleGroupMaxCount = 100 // 自动按 100 个节点一组拆分
});
await op.OnAsync();
op.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {kv.Key} = {kv.Value.ResultValue}");
}
};
// 1000 个点位 → 10 个组
await op.SubscribeAsync(new Address(all1000AddressDetails));
混合虚拟地址
AddressName 注册为虚拟地址的节点在本地读写,不访问 UA 服务端;虚拟地址与真实节点可混在同一个 Address / WriteAsync 调用中。参见 地址系统。
常见问题
Q: 连接失败,提示"应用实例证书无效"。
A: 应用证书自动生成在 {BaseDir}/cer/app。首次连接时如服务端要求信任其证书请确认信任;确保 AppCerPath 目录可写。
Q: 节点标识符格式是什么?
A: AddressName 必须是合法的 UA NodeId 字符串,如 ns=2;s=Temperature、ns=2;i=1001 或数值形式 i=2253。可用 GetAllNodeAsync 浏览目标服务端的真实标识符。
Q: 订阅收不到事件。
A: 必须在调用 SubscribeAsync 之前绑定 OnDataEventAsync(或 OnDataEvent)。确认 GetStatusAsync 为连接状态——重连后订阅会自动重建恢复。
Q: 如何以正确类型写入值?
A: 用 GetNodeValueTypeAsync → TypeConvertAsync 获取节点类型,或用 GetValue(string, BuiltInType) / GetArrayValue(...) 准备值,客户端会发送类型正确的 Variant。
Q: 服务端失去响应,客户端能恢复吗?
A: InterruptReconnection = true(默认)时,保活失败会触发重连循环(每 ReconnectionInterval 毫秒重试);重连成功后自动重建全部订阅并恢复事件。
Q: 点位太多导致读取慢。
A: 提高 TaskNumber,或使用 OpcUaClientController 将读写/订阅分摊到多个会话(ServerMaxSession 不能超过服务端会话上限)。
相关文档
- OPC UA 服务端 — 本客户端可连接的 Snet UA 服务端
- OPC 协议族 — 四个 OPC 组件
- DaqAbstract 基类 — 生命周期与同步包装
- IDaq 接口 · IOu 接口 · 地址模型
