2026-01-12 18:27:58 +08:00
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.Drawing;
|
2026-01-09 12:30:36 +08:00
|
|
|
|
using System.Net.NetworkInformation;
|
2025-12-26 16:58:12 +08:00
|
|
|
|
|
|
|
|
|
|
namespace SHH.CameraSdk;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [状态代理] 网络连通性哨兵
|
|
|
|
|
|
/// 特性:
|
|
|
|
|
|
/// 1. 低耦合:不依赖具体驱动,只依赖接口
|
|
|
|
|
|
/// 2. 高性能:使用 Parallel.ForEachAsync 实现受控并行
|
|
|
|
|
|
/// 3. 智能策略:播放中不Ping,空闲时才Ping
|
2026-01-12 18:27:58 +08:00
|
|
|
|
/// 4. 稳定性:基于“持续断联时间”判定离线,防止网络瞬抖
|
2025-12-26 16:58:12 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class ConnectivitySentinel
|
|
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
private readonly CameraManager _manager; //
|
2025-12-26 16:58:12 +08:00
|
|
|
|
private readonly PeriodicTimer _timer;
|
|
|
|
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// [关键] 状态缓存:用于“去重”上报
|
2026-01-09 12:30:36 +08:00
|
|
|
|
private readonly ConcurrentDictionary<long, bool> _lastStates = new();
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// [新增] 故障计时器:记录设备“首次探测失败”的时间点
|
|
|
|
|
|
// Key: DeviceId, Value: 首次失败时间
|
|
|
|
|
|
private readonly ConcurrentDictionary<long, DateTime> _failureStartTimes = new();
|
|
|
|
|
|
|
2025-12-26 16:58:12 +08:00
|
|
|
|
// [关键配置] 最大并发度
|
|
|
|
|
|
private const int MAX_PARALLELISM = 16;
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// [配置] 判定离线的持续时间阈值 (秒)
|
|
|
|
|
|
// 只有连续 Ping 不通超过 30秒,才认定为断线
|
|
|
|
|
|
private const int OFFLINE_DURATION_THRESHOLD = 30;
|
|
|
|
|
|
|
|
|
|
|
|
// [配置] 单次 Ping 的超时时间 (毫秒)
|
|
|
|
|
|
// 设为 1000ms,保证一轮检查快速结束,不依赖 Ping 的默认 5秒 超时
|
|
|
|
|
|
private const int PING_TIMEOUT = 1000;
|
|
|
|
|
|
|
2025-12-26 16:58:12 +08:00
|
|
|
|
public ConnectivitySentinel(CameraManager manager)
|
|
|
|
|
|
{
|
|
|
|
|
|
_manager = manager;
|
|
|
|
|
|
// 每 3 秒执行一轮全量巡检
|
|
|
|
|
|
_timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
|
|
|
|
|
|
|
|
|
|
|
|
// 启动后台任务(不阻塞主线程)
|
|
|
|
|
|
_ = RunLoopAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RunLoopAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 等待下一个 3秒 周期
|
|
|
|
|
|
while (await _timer.WaitForNextTickAsync(_cts.Token))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 获取当前所有设备的快照
|
|
|
|
|
|
var devices = _manager.GetAllDevices().Cast<IDeviceConnectivity>();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. [核心回答] 受控并行执行
|
|
|
|
|
|
await Parallel.ForEachAsync(devices, new ParallelOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
MaxDegreeOfParallelism = MAX_PARALLELISM,
|
|
|
|
|
|
CancellationToken = _cts.Token
|
|
|
|
|
|
},
|
|
|
|
|
|
async (device, token) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 对每个设备执行独立检查
|
|
|
|
|
|
await CheckSingleDeviceAsync(device);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException) { /* 正常停止 */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task CheckSingleDeviceAsync(IDeviceConnectivity device)
|
|
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 1. 获取“瞬时”连通性 (Raw Status)
|
|
|
|
|
|
bool isResponsive = false;
|
2025-12-26 16:58:12 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// [智能策略]:如果设备正在取流,优先检查帧心跳
|
2025-12-26 16:58:12 +08:00
|
|
|
|
if (device.Status == VideoSourceStatus.Playing || device.Status == VideoSourceStatus.Streaming)
|
|
|
|
|
|
{
|
|
|
|
|
|
long now = Environment.TickCount64;
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 5秒内有帧,就算瞬时在线
|
|
|
|
|
|
isResponsive = (now - device.LastFrameTick) < 5000;
|
|
|
|
|
|
|
|
|
|
|
|
// [双重保障] 如果帧心跳断了,立即 Ping 确认,防止只是解码卡死而非断网
|
|
|
|
|
|
if (!isResponsive)
|
|
|
|
|
|
{
|
|
|
|
|
|
isResponsive = await PingAsync(device.IpAddress);
|
|
|
|
|
|
}
|
2025-12-26 16:58:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// [主动探测]:空闲或离线时,发射 ICMP Ping
|
2026-01-12 18:27:58 +08:00
|
|
|
|
isResponsive = await PingAsync(device.IpAddress);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. [核心逻辑] 基于持续时间的稳定性判定 (Stable Status)
|
|
|
|
|
|
bool isLogicallyOnline;
|
|
|
|
|
|
|
|
|
|
|
|
if (isResponsive)
|
|
|
|
|
|
{
|
|
|
|
|
|
// --- 情况 A: 瞬时探测通了 ---
|
|
|
|
|
|
// 只要通一次,立即清除故障计时,认为设备在线
|
|
|
|
|
|
_failureStartTimes.TryRemove(device.Id, out _);
|
|
|
|
|
|
isLogicallyOnline = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// --- 情况 B: 瞬时探测失败 ---
|
|
|
|
|
|
// 记录或获取“首次失败时间”
|
|
|
|
|
|
var nowTime = DateTime.Now;
|
|
|
|
|
|
var firstFailureTime = _failureStartTimes.GetOrAdd(device.Id, nowTime);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算已经持续失败了多久
|
|
|
|
|
|
var failureDuration = (nowTime - firstFailureTime).TotalSeconds;
|
|
|
|
|
|
|
|
|
|
|
|
if (failureDuration >= OFFLINE_DURATION_THRESHOLD)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 只有持续失败超过 30秒,才“真的”判定为离线
|
|
|
|
|
|
isLogicallyOnline = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 还没到 30秒,处于“抖动观察期”
|
|
|
|
|
|
// 策略:维持上一次的已知状态(如果之前是在线,就假装还在线;之前是离线,就继续离线)
|
|
|
|
|
|
// 这样可以防止网络微小抖动导致的 Status 频繁跳变
|
|
|
|
|
|
isLogicallyOnline = _lastStates.TryGetValue(device.Id, out bool last) ? last : true;
|
|
|
|
|
|
|
|
|
|
|
|
// 调试日志 (可选)
|
|
|
|
|
|
// Console.WriteLine($"[Sentinel] 设备 {device.Id} 瞬时异常,观察中: {failureDuration:F1}s / {OFFLINE_DURATION_THRESHOLD}s");
|
|
|
|
|
|
}
|
2025-12-26 16:58:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// [状态注入]:将经过时间滤波后的“稳定状态”注入回设备
|
|
|
|
|
|
device.SetNetworkStatus(isLogicallyOnline);
|
2026-01-09 12:30:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 3. [状态去重与上报]
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 获取上一次上报的状态,默认为反状态以触发首次上报
|
|
|
|
|
|
bool lastReported = _lastStates.TryGetValue(device.Id, out bool val) ? val : !isLogicallyOnline;
|
2026-01-09 12:30:36 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
if (lastReported != isLogicallyOnline)
|
2026-01-09 12:30:36 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 记录新状态
|
2026-01-12 18:27:58 +08:00
|
|
|
|
_lastStates[device.Id] = isLogicallyOnline;
|
2026-01-09 12:30:36 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 构造原因描述
|
|
|
|
|
|
string reason = isLogicallyOnline
|
|
|
|
|
|
? "网络探测恢复"
|
|
|
|
|
|
: $"持续断连超过{OFFLINE_DURATION_THRESHOLD}秒";
|
2026-01-09 12:30:36 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// ★★★ 核心动作:通知 Manager ★★★
|
2026-01-21 19:03:59 +08:00
|
|
|
|
_manager.NotifyStatusChange(device.Id, device.IpAddress, isLogicallyOnline, reason);
|
2026-01-09 12:30:36 +08:00
|
|
|
|
}
|
2025-12-26 16:58:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 纯粹的 Ping 逻辑
|
|
|
|
|
|
private async Task<bool> PingAsync(string ip)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var ping = new Ping();
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// [修改] 超时设为 1000ms (1秒)
|
|
|
|
|
|
// 理由:我们要快速探测,不要等待 5秒。
|
|
|
|
|
|
// 即使 Ping 因为网络延迟用了 4秒 才返回,Ping 类也会在 1秒 时抛出超时,
|
|
|
|
|
|
// 这会被视为一次“瞬时失败”,然后由外层的 30秒 时间窗口来容错。
|
|
|
|
|
|
var reply = await ping.SendPingAsync(ip, PING_TIMEOUT);
|
2025-12-26 16:58:12 +08:00
|
|
|
|
return reply.Status == IPStatus.Success;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Stop()
|
|
|
|
|
|
{
|
|
|
|
|
|
_cts.Cancel();
|
|
|
|
|
|
_timer.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|