跳到主要内容

冗余 Server 自动切换

工业现场通常配主备 (Hot Standby) Server. 主故障时客户端切到备, 业务继续. 用 C# 示例.

配套示例

完整代码

using DarraOpcUa_Client;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

class FailoverClient : IDisposable
{
private readonly string[] _endpoints;
private readonly IReadOnlyList<string> _tags;
private DarraOpcUa _ua;
private OpcUaSubscription _sub;
private int _currentIdx = -1;
private readonly object _lock = new();

public FailoverClient(IReadOnlyList<string> tags, params string[] endpoints)
{
_tags = tags;
_endpoints = endpoints;
}

public void Connect()
{
lock (_lock)
{
// 从下一个 endpoint 开始尝试 (避免反复连同一个故障点)
for (int tries = 0; tries < _endpoints.Length; tries++)
{
_currentIdx = (_currentIdx + 1) % _endpoints.Length;
var ep = _endpoints[_currentIdx];
try
{
_ua = new DarraOpcUa(ep);
_ua.Events.CommunicationError += OnCommError;
_ua.Connect();
Console.WriteLine($"[OK] 已连接 {ep}");
Resubscribe();
return;
}
catch (OpcUaException ex)
{
Console.WriteLine($" {ep} 连接失败: {ex.StatusCode}");
_ua?.Dispose();
_ua = null;
}
}
throw new Exception("所有 endpoint 都不可达");
}
}

private void Resubscribe()
{
_sub = _ua.CreateSubscription(500);
_sub.DataChanged += (s, e) =>
Console.WriteLine($" {e.NodeId} = {e.ValueString} [{e.Status}]");
_sub.AddMany(_tags);
}

private void OnCommError(object sender, CommunicationErrorEventArgs e)
{
Console.WriteLine($"\n[!!] 通讯异常 {e.StatusCode}, 切换 server...");
Task.Run(() =>
{
try { _sub?.Dispose(); } catch { }
try { _ua?.Dispose(); } catch { }
Thread.Sleep(2000); // 等故障检测稳定, 避免反复切换抖动
try { Connect(); }
catch (Exception ex) { Console.WriteLine($"切换失败: {ex.Message}, 30 秒后重试"); }
});
}

public void Dispose() { _sub?.Dispose(); _ua?.Dispose(); }
}

class Program
{
static async Task Main()
{
var tags = new[] { "ns=2;s=Boiler1.Temperature", "ns=2;s=Boiler1.Pressure" };

using var client = new FailoverClient(
tags,
"opc.tcp://primary:4840",
"opc.tcp://backup:4840");

client.Connect();

Console.WriteLine("\n运行中. 拔主 server 网线测试切换. Ctrl+C 退出.\n");
await Task.Delay(Timeout.Infinite);
}
}

分段说明

第 1 步: CommunicationError 事件触发切换

_ua.Events.CommunicationError += OnCommError;

SDK 内部 KeepAlive 检测到通讯异常时, 触发 CommunicationError 事件. 不需要主动 ping 服务端.

第 2 步: 切换前等 2 秒

Thread.Sleep(2000);  // 等故障检测稳定

关键: 故障可能是网络抖动 (瞬断), 立即切换会反复抖动 (主→备→主→备). 加 2 秒 backoff 让网络稳定. 实际项目可以加指数退避.

第 3 步: 切换后必须重建订阅

private void Resubscribe()
{
_sub = _ua.CreateSubscription(500);
_sub.AddMany(_tags);
}

切到新 Server 后, 旧 Session / Subscription 全部失效. 必须重建订阅 + 重新 AddMany. 高级用法: 如果主备 Server 共享 SubscriptionId (Hot Standby 配置), 可以用 TransferSubscriptions 接管旧订阅, 不用重建 MI. 大多数实际部署达不到这个一致性, 重建更可靠.

注意事项

  • 主备 Server 必须同 NamespaceArray (NodeId 解析一致), 否则切换后 ns=2;... 在新 server 可能映射到完全不同的节点.
  • KeepAliveCount 默认 ~10, 检测延迟 = publishingInterval * KeepAliveCount (默认 ~5 秒). 要更快检测就调小, 但容易误判.
  • 长期运行还要处理"新主 (原备) 也挂了"的连环失败, 上面代码用循环索引 + 异常重试覆盖.

相关链接