124 lines
4.2 KiB
C#
124 lines
4.2 KiB
C#
using System.Drawing;
|
||
using System.Net.NetworkInformation;
|
||
|
||
namespace SHH.CameraSdk;
|
||
|
||
/// <summary>
|
||
/// [状态代理] 网络连通性哨兵
|
||
/// 特性:
|
||
/// 1. 低耦合:不依赖具体驱动,只依赖接口
|
||
/// 2. 高性能:使用 Parallel.ForEachAsync 实现受控并行
|
||
/// 3. 智能策略:播放中不Ping,空闲时才Ping
|
||
/// </summary>
|
||
public class ConnectivitySentinel
|
||
{
|
||
private readonly CameraManager _manager; // [cite: 329]
|
||
private readonly PeriodicTimer _timer;
|
||
private readonly CancellationTokenSource _cts = new();
|
||
|
||
// [关键] 状态缓存:用于“去重”。
|
||
// 只有当状态真的从 true 变 false (或反之) 时,才通知 Manager。
|
||
// 防止每 3 秒发一次 "在线" 骚扰上层。
|
||
private readonly ConcurrentDictionary<long, bool> _lastStates = new();
|
||
|
||
// [关键配置] 最大并发度
|
||
// 建议值:CPU 核心数 * 4,或者固定 16-32
|
||
// 50 个摄像头,设为 16,意味着分 4 批完成,总耗时极短
|
||
private const int MAX_PARALLELISM = 16;
|
||
|
||
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. 获取当前所有设备的快照
|
||
// CameraManager.GetAllDevices() 返回的是 BaseVideoSource,它实现了 IDeviceConnectivity
|
||
var devices = _manager.GetAllDevices().Cast<IDeviceConnectivity>();
|
||
|
||
// 2. [核心回答] 受控并行执行
|
||
// .NET 6+ 提供的超级 API,专门解决“一下子 50 个”的问题
|
||
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)
|
||
{
|
||
bool isAlive = false;
|
||
|
||
// [智能策略]:如果设备正在取流,直接检查帧心跳(省流模式)
|
||
if (device.Status == VideoSourceStatus.Playing || device.Status == VideoSourceStatus.Streaming)
|
||
{
|
||
long now = Environment.TickCount64;
|
||
// 5秒内有帧,就算在线
|
||
isAlive = (now - device.LastFrameTick) < 5000;
|
||
}
|
||
else
|
||
{
|
||
// [主动探测]:空闲或离线时,发射 ICMP Ping
|
||
isAlive = await PingAsync(device.IpAddress);
|
||
}
|
||
|
||
// [状态注入]:将探测结果“注入”回设备
|
||
device.SetNetworkStatus(isAlive);
|
||
|
||
// 3. [状态去重与上报]
|
||
// 获取上一次的状态,如果没记录过,假设它之前是反状态(强制第一次上报)
|
||
bool lastState = _lastStates.TryGetValue(device.Id, out bool val) ? val : !isAlive;
|
||
|
||
if (lastState != isAlive)
|
||
{
|
||
// 记录新状态
|
||
_lastStates[device.Id] = isAlive;
|
||
|
||
// ★★★ 核心动作:只通知 Manager,不做任何网络操作 ★★★
|
||
_manager.NotifyStatusChange(device.Id, isAlive, "网络连通性哨兵检测结论");
|
||
|
||
// Console.WriteLine($"[Sentinel] 诊断变化: {device.Id} -> {isAlive}");
|
||
}
|
||
}
|
||
|
||
// 纯粹的 Ping 逻辑
|
||
private async Task<bool> PingAsync(string ip)
|
||
{
|
||
try
|
||
{
|
||
using var ping = new Ping();
|
||
// 超时设为 800ms,快速失败,避免拖慢整体批次
|
||
var reply = await ping.SendPingAsync(ip, 800);
|
||
return reply.Status == IPStatus.Success;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public void Stop()
|
||
{
|
||
_cts.Cancel();
|
||
_timer.Dispose();
|
||
}
|
||
} |