Safe API — 永不抛异常变体
概述
OPC UA 服务端在网络抖动 / 节点删除 / 权限变更等情况下会让 SDK 抛 OpcUaException。
UI 主线程和批处理 (上千节点循环 Read) 不希望被一条失败中断, 所以 SDK 为常用 RPC 各提供一个 Safe 版本 — 任何异常都折叠为 StatusCode 返回, 业务侧用元组解构判读, 不再写 try/catch。
对应规范段:
- Browse — Part 4 §5.8.2
- Read — Part 4 §5.10.2
- Write — Part 4 §5.10.4
- Call — Part 4 §5.11.2
- HistoryRead — Part 11 §6.5
API
| 方法 | 类别 | 读写 | 说明 |
|---|---|---|---|
| BrowseSafe(nodeId, filter) | 浏览 | 读 | 返回 (StatusCode, IReadOnlyList<OpcUaReference>) |
| ReadSafe(nodeId, attribute) | 读 | 读 | 返回 (StatusCode, DataValue), 失败时 Value=null |
| WriteSafe(nodeId, value, attribute) | 写 | 写 | 返回 StatusCode |
| CallSafe(objectId, methodId, inputs) | 方法 | 写 | 返回 (StatusCode, IReadOnlyList<Variant>) |
| ReadHistorySafe(nodeId, t0, t1, max) | 历史 | 读 | 返回 (StatusCode, IReadOnlyList<DataValue>) |
返回值约定:
StatusCode == Good— 业务成功, 数据有效StatusCode == Bad...— 失败, 数据集合为空 /Value=null, 不会 throw- 内部异常 (反射 / native 层 crash) → 兜底
BadInternalError (0x80020000)
代码示例
using DarraOpcUa_Client;
using var ua = new DarraOpcUa("opc.tcp://localhost:4840");
ua.Connect();
// 1) BrowseSafe — UI 树异步刷新, 节点被删不让全树报错
var (st, refs) = ua.BrowseSafe("ns=2;s=Boiler1");
if (st != StatusCode.Good)
Console.WriteLine($"Browse 失败 {st}, 跳过此分支"); // 例如 BadNodeIdUnknown
else
foreach (var r in refs) Console.WriteLine(r.BrowseName);
// 2) ReadSafe — 批量轮询时不让单个失败打断
foreach (var nid in nodeList)
{
var (rs, dv) = ua.ReadSafe(nid);
if (rs == StatusCode.Good && dv != null)
using (dv) Console.WriteLine($"{nid} = {dv.Value}");
else
Console.WriteLine($"{nid} = <{rs}>");
}
// 3) WriteSafe — 批量下发参数
foreach (var (nid, val) in setpoints)
{
var rc = ua.WriteSafe(nid, new Variant(val));
if (rc != StatusCode.Good) Console.WriteLine($"写 {nid} 失败 {rc}");
}
// 4) CallSafe — 调用方法, 不允许异常吞掉用户的"按钮点击"
var (cst, outs) = ua.CallSafe("ns=2;s=Robot", "ns=2;s=Robot.Home");
if (cst != StatusCode.Good) MessageBox.Show($"Home 失败: {cst}");
// 5) ReadHistorySafe — 历史曲线一段失败不让整个对话框崩
var (hst, vals) = ua.ReadHistorySafe("ns=2;s=Temp", t0, t1, maxValues: 5000);
与重试机制的关系
普通 Read/Write/Browse/Call/ReadHistory 内部都包了 RetryOnSessionFault,
session 级故障 (BadSessionClosed) 会自动重连一次再重试。
Safe 变体调用的也是同一套核心方法, 所以 Safe 也会受益于自动重连, 只是把"重连后仍然失败"的异常折叠为 StatusCode。
最佳实践
- UI 主线程一律用 Safe — UI 不允许异常逃逸, 否则进程挂掉
- 批处理 / 巡检循环用 Safe — 单点失败不阻塞整体
- 业务关键路径 (例如启停设备前的预检) 仍用普通版本, 让异常显式暴露, 由上层决定 retry / abort
- 不要
if (status == Good) ... else throw— 那等于把 Safe 变回不安全, 直接用普通版本即可
跨语言对照
| C# | Python | Java | C++ | Rust | C |
|---|---|---|---|---|---|
| BrowseSafe | browse_safe | browseSafe | BrowseSafe | browse_safe | DarraUa_Session_BrowseSafe |
| ReadSafe | read_safe | readSafe | ReadSafe | read_safe | DarraUa_Session_ReadSafe |
| WriteSafe | write_safe | writeSafe | WriteSafe | write_safe | DarraUa_Session_WriteSafe |
| CallSafe | call_safe | callSafe | CallSafe | call_safe | DarraUa_Session_CallSafe |
| ReadHistorySafe | read_history_safe | readHistorySafe | ReadHistorySafe | read_history_safe | DarraUa_Session_ReadHistorySafe |