新通讯图像协议对接成功
This commit is contained in:
@@ -1,194 +0,0 @@
|
||||
//using System.Text;
|
||||
//using MessagePack;
|
||||
//using Microsoft.Extensions.Hosting;
|
||||
//using NetMQ;
|
||||
//using NetMQ.Monitoring; // ★ 1. 必须引用 Monitoring 命名空间
|
||||
//using NetMQ.Sockets;
|
||||
//using SHH.CameraSdk;
|
||||
//using SHH.Contracts;
|
||||
|
||||
//namespace SHH.CameraService;
|
||||
|
||||
//public class CommandClientWorker : BackgroundService
|
||||
//{
|
||||
// private readonly ServiceConfig _config;
|
||||
// private readonly CommandDispatcher _dispatcher;
|
||||
// private readonly InterceptorPipeline _pipeline;
|
||||
|
||||
// // 管理多个 Socket
|
||||
// private readonly List<DealerSocket> _sockets = new();
|
||||
|
||||
// // ★ 2. 新增:保存 Monitor 列表,防止被 GC 回收
|
||||
// private readonly List<NetMQMonitor> _monitors = new();
|
||||
|
||||
// private NetMQPoller? _poller;
|
||||
|
||||
// public CommandClientWorker(
|
||||
// ServiceConfig config,
|
||||
// CommandDispatcher dispatcher,
|
||||
// InterceptorPipeline pipeline)
|
||||
// {
|
||||
// _config = config;
|
||||
// _dispatcher = dispatcher;
|
||||
// _pipeline = pipeline;
|
||||
// }
|
||||
|
||||
// protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
// {
|
||||
// await Task.Yield();
|
||||
|
||||
// if (!_config.ShouldConnect || _config.CommandEndpoints.Count == 0) return;
|
||||
|
||||
// _poller = new NetMQPoller();
|
||||
|
||||
// // -------------------------------------------------------------
|
||||
// // 核心修改区:建立连接并挂载监控器
|
||||
// // -------------------------------------------------------------
|
||||
// foreach (var ep in _config.CommandEndpoints)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var socket = new DealerSocket();
|
||||
// socket.Options.Identity = Encoding.UTF8.GetBytes(_config.AppId);
|
||||
|
||||
// var monitorUrl = $"inproc://monitor_{Guid.NewGuid():N}";
|
||||
// var monitor = new NetMQMonitor(socket, monitorUrl, SocketEvents.Connected);
|
||||
|
||||
// monitor.Connected += async (s, args) =>
|
||||
// {
|
||||
// Console.WriteLine($"[指令] 网络连接建立: {ep.Uri} -> 正在补发注册包...");
|
||||
// await SendRegisterAsync(socket);
|
||||
// };
|
||||
|
||||
// // ★★★ 修正点:使用 AttachToPoller 代替 Add ★★★
|
||||
// // 错误写法: _poller.Add(monitor);
|
||||
// monitor.AttachToPoller(_poller);
|
||||
|
||||
// // 依然需要保存引用,防止被 GC 回收
|
||||
// _monitors.Add(monitor);
|
||||
|
||||
// socket.Connect(ep.Uri);
|
||||
// socket.ReceiveReady += OnSocketReceiveReady;
|
||||
|
||||
// _sockets.Add(socket);
|
||||
// _poller.Add(socket);
|
||||
|
||||
// Console.WriteLine($"[指令] 通道初始化完成: {ep.Uri} (带自动重连监控)");
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"[指令] 连接初始化异常: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (_sockets.Count == 0) return;
|
||||
|
||||
// // =================================================================
|
||||
// // 6. 绑定 ACK 逻辑 (保持不变)
|
||||
// // =================================================================
|
||||
// _dispatcher.OnResponseReady += async (result) =>
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// byte[] resultBytes = MessagePackSerializer.Serialize(result);
|
||||
// var ctx = await _pipeline.ExecuteSendAsync(ProtocolHeaders.CommandResult, resultBytes);
|
||||
|
||||
// if (ctx != null)
|
||||
// {
|
||||
// foreach (var socket in _sockets)
|
||||
// {
|
||||
// socket.SendMoreFrame(ctx.Protocol).SendFrame(ctx.Data);
|
||||
// }
|
||||
// Console.WriteLine($"[指令] ACK 已广播 (ID: {result.RequestId})");
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"[ACK] 发送失败: {ex.Message}");
|
||||
// }
|
||||
// };
|
||||
|
||||
// // =================================================================
|
||||
// // 7. 启动 Poller
|
||||
// // =================================================================
|
||||
// // 注意:我们不需要手动发第一次注册包了,
|
||||
// // 因为 Poller 启动后,底层 TCP 会建立连接,从而触发 monitor.Connected 事件,
|
||||
// // 事件里会自动发送注册包。这就是“自动档”的好处。
|
||||
// _poller.RunAsync();
|
||||
|
||||
// // 阻塞直到取消
|
||||
// while (!stoppingToken.IsCancellationRequested)
|
||||
// {
|
||||
// await Task.Delay(1000, stoppingToken);
|
||||
// }
|
||||
|
||||
// // 清理
|
||||
// _poller.Stop();
|
||||
// _poller.Dispose();
|
||||
// foreach (var m in _monitors) m.Dispose(); // 释放监控器
|
||||
// foreach (var s in _sockets) s.Dispose();
|
||||
// }
|
||||
|
||||
// // =================================================================
|
||||
// // ★ 8. 抽离出的注册包发送逻辑 (供 Monitor 调用)
|
||||
// // =================================================================
|
||||
// private async Task SendRegisterAsync(DealerSocket targetSocket)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var registerPayload = new RegisterPayload
|
||||
// {
|
||||
// Protocol = ProtocolHeaders.ServerRegister,
|
||||
// InstanceId = _config.AppId,
|
||||
// ProcessId = Environment.ProcessId,
|
||||
// Version = "1.0.0",
|
||||
// ServerIp = "127.0.0.1", // 建议优化:获取本机真实IP
|
||||
// WebApiPort = _config.BasePort,
|
||||
// StartTime = DateTime.Now
|
||||
// };
|
||||
|
||||
// byte[] regData = MessagePackSerializer.Serialize(registerPayload);
|
||||
|
||||
// // 执行拦截器
|
||||
// var ctx = await _pipeline.ExecuteSendAsync(ProtocolHeaders.ServerRegister, regData);
|
||||
|
||||
// if (ctx != null)
|
||||
// {
|
||||
// // 直接向触发事件的那个 Socket 发送
|
||||
// // DealerSocket 允许在连接未完全就绪时 Send,它会缓存直到网络通畅
|
||||
// targetSocket.SendMoreFrame(ctx.Protocol).SendFrame(ctx.Data);
|
||||
// // Console.WriteLine($"[指令] 身份注册包已推入队列: {targetSocket.Options.Identity}");
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"[指令] 注册包发送失败: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async void OnSocketReceiveReady(object? sender, NetMQSocketEventArgs e)
|
||||
// {
|
||||
// NetMQMessage incomingMsg = new NetMQMessage();
|
||||
// if (e.Socket.TryReceiveMultipartMessage(ref incomingMsg))
|
||||
// {
|
||||
// if (incomingMsg.FrameCount >= 2)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// string rawProtocol = incomingMsg[0].ConvertToString();
|
||||
// byte[] rawData = incomingMsg[1].ToByteArray();
|
||||
|
||||
// var ctx = await _pipeline.ExecuteReceiveAsync(rawProtocol, rawData);
|
||||
// if (ctx != null)
|
||||
// {
|
||||
// await _dispatcher.DispatchAsync(ctx.Protocol, ctx.Data);
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"[指令] 处理异常: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,91 +0,0 @@
|
||||
// 文件: Core\CmdClients\CommandDispatcher.cs
|
||||
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SHH.Contracts;
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
public class CommandDispatcher
|
||||
{
|
||||
// 1. 注入路由表
|
||||
private readonly Dictionary<string, ICommandHandler> _handlers;
|
||||
|
||||
// 2. 定义回执事件 (ACK闭环的核心)
|
||||
public event Action<CommandResult>? OnResponseReady;
|
||||
|
||||
// 3. 构造函数:注入所有 Handler
|
||||
public CommandDispatcher(IEnumerable<ICommandHandler> handlers)
|
||||
{
|
||||
// 将注入的 Handler 转换为字典,Key = ActionName (e.g. "SyncCamera")
|
||||
_handlers = handlers.ToDictionary(h => h.ActionName, h => h, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async Task DispatchAsync(string protocol, byte[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只处理 COMMAND 协议
|
||||
if (protocol != ProtocolHeaders.Command) return;
|
||||
|
||||
// 反序列化信封
|
||||
var envelope = MessagePackSerializer.Deserialize<CommandPayload>(data);
|
||||
if (envelope == null) return;
|
||||
|
||||
string cmdCode = envelope.CmdCode; // e.g. "SyncCamera"
|
||||
Console.WriteLine($"[分发] 收到指令: {cmdCode} (ID: {envelope.RequestId})");
|
||||
|
||||
bool isSuccess = true;
|
||||
string message = "OK";
|
||||
|
||||
// --- 路由匹配逻辑 ---
|
||||
if (_handlers.TryGetValue(cmdCode, out var handler))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 数据适配:你的 Handler 需要 JToken
|
||||
// 如果 envelope.JsonParams 是空的,传个空对象防止报错
|
||||
var jsonStr = string.IsNullOrEmpty(envelope.JsonParams) ? "{}" : envelope.JsonParams;
|
||||
var token = JToken.Parse(jsonStr);
|
||||
|
||||
// ★★★ 核心:调用 SyncCameraHandler.ExecuteAsync ★★★
|
||||
await handler.ExecuteAsync(token);
|
||||
|
||||
message = $"Executed {cmdCode}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
isSuccess = false;
|
||||
message = $"Handler Error: {ex.Message}";
|
||||
Console.WriteLine($"[业务异常] {message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isSuccess = false;
|
||||
message = $"No handler found for {cmdCode}";
|
||||
Console.WriteLine($"[警告] {message}");
|
||||
}
|
||||
|
||||
// --- ACK 闭环逻辑 ---
|
||||
if (envelope.RequireAck)
|
||||
{
|
||||
var result = new CommandResult
|
||||
{
|
||||
Protocol = ProtocolHeaders.CommandResult,
|
||||
RequestId = envelope.RequestId, // 必须带回 ID
|
||||
Success = isSuccess,
|
||||
Message = message,
|
||||
Timestamp = DateTime.Now.Ticks
|
||||
};
|
||||
// 触发事件
|
||||
OnResponseReady?.Invoke(result);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Dispatcher] 致命错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// 在线客户端信息模型 (已更新)
|
||||
/// </summary>
|
||||
public class ConnectedClient
|
||||
{
|
||||
/// <summary> 唯一标识 (AppId) </summary>
|
||||
public string ServiceId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> 版本号 </summary>
|
||||
public string Version { get; set; } = "1.0.0";
|
||||
|
||||
/// <summary> 远程进程 ID </summary>
|
||||
public int Pid { get; set; }
|
||||
|
||||
/// <summary> 客户端 IP </summary>
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> WebAPI 端口 (Dashboard 调用 REST 接口用) </summary>
|
||||
public int WebPort { get; set; }
|
||||
|
||||
/// <summary> 该客户端正在推流的目标地址 </summary>
|
||||
public List<string> TargetVideoNodes { get; set; } = new List<string>();
|
||||
|
||||
public DateTime LastHeartbeat { get; set; }
|
||||
|
||||
// 辅助属性:拼接出完整的 API BaseUrl
|
||||
public string WebApiUrl => $"http://{Ip}:{WebPort}";
|
||||
}
|
||||
66
SHH.CameraService/Core/CommandDispatcher.cs
Normal file
66
SHH.CameraService/Core/CommandDispatcher.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SHH.Contracts.Grpc;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC 指令分发器
|
||||
/// 职责:接收从 GrpcCommandReceiverWorker 传入的 Proto 消息,解析参数并路由至具体的 Handler。
|
||||
/// </summary>
|
||||
public class CommandDispatcher
|
||||
{
|
||||
private readonly Dictionary<string, ICommandHandler> _handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数:通过 DI 注入所有已注册的处理器 (SyncCameraHandler, RemoveCameraHandler 等)
|
||||
/// </summary>
|
||||
public CommandDispatcher(IEnumerable<ICommandHandler> handlers)
|
||||
{
|
||||
// 将处理器列表转换为字典,方便 O(1) 查询
|
||||
_handlers = handlers.ToDictionary(
|
||||
h => h.ActionName,
|
||||
h => h,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行指令分发
|
||||
/// </summary>
|
||||
/// <param name="protoMsg">从 gRPC Server Streaming 接收到的原始 Proto 指令对象</param>
|
||||
public async Task DispatchAsync(CommandPayloadProto protoMsg)
|
||||
{
|
||||
if (protoMsg == null) return;
|
||||
|
||||
string cmdCode = protoMsg.CmdCode; // 例如 "Sync_Camera"
|
||||
Console.WriteLine($"[Dispatcher] 收到远程指令: {cmdCode}, 请求ID: {protoMsg.RequestId}");
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 查找对应的处理器
|
||||
if (_handlers.TryGetValue(cmdCode, out var handler))
|
||||
{
|
||||
// 2. 参数转换:将 Proto 里的 JSON 字符串转换为原有 Handler 需要的 JToken
|
||||
// 这样你之前的 SyncCameraHandler 代码不需要做任何逻辑改动即可直接复用
|
||||
var jsonStr = string.IsNullOrWhiteSpace(protoMsg.JsonParams) ? "{}" : protoMsg.JsonParams;
|
||||
var token = JToken.Parse(jsonStr);
|
||||
|
||||
// 3. 调用具体业务执行
|
||||
await handler.ExecuteAsync(token);
|
||||
|
||||
Console.WriteLine($"[Dispatcher] 指令 {cmdCode} 执行成功。");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[Dispatcher Warning] 未找到指令处理器: {cmdCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Dispatcher Error] 执行指令 {cmdCode} 异常: {ex.Message}");
|
||||
}
|
||||
|
||||
// 注意:关于 ACK (require_ack)
|
||||
// 在 NetMQ 时代需要手动回发结果,在 gRPC Server Streaming 模式下,
|
||||
// 建议通过 Unary RPC (例如另设一个 ReportCommandResult 方法) 异步上报执行结果。
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//using MessagePack;
|
||||
//using NetMQ;
|
||||
//using SHH.Contracts;
|
||||
|
||||
//namespace SHH.CameraService
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 负责将业务契约转换为 ZeroMQ 传输协议
|
||||
// /// </summary>
|
||||
// public static class NetMQProtocolExtensions
|
||||
// {
|
||||
// private const string PROTOCOL_HEADER = "SHH_V1";
|
||||
|
||||
// /// <summary>
|
||||
// /// 扩展方法:将 Payload 转为 NetMQMessage
|
||||
// /// 使用方法:var msg = payload.ToNetMqMessage();
|
||||
// /// </summary>
|
||||
// public static NetMQMessage ToNetMqMessage(this VideoPayload payload)
|
||||
// {
|
||||
// var msg = new NetMQMessage();
|
||||
|
||||
// // Frame 0: 协议魔数
|
||||
// msg.Append(PROTOCOL_HEADER);
|
||||
|
||||
// ////// Frame 1: 元数据 JSON
|
||||
// ////msg.Append(payload.GetMetadataJson());
|
||||
|
||||
// // ★★★ 修复点:在序列化之前,手动更新 Payload 的标志位 ★★★
|
||||
// payload.HasOriginalImage = (payload.OriginalImageBytes != null && payload.OriginalImageBytes.Length > 0);
|
||||
// payload.HasTargetImage = (payload.TargetImageBytes != null && payload.TargetImageBytes.Length > 0);
|
||||
|
||||
// // Frame 1: Metadata (MessagePack)
|
||||
// byte[] metaBytes = MessagePackSerializer.Serialize(payload);
|
||||
// msg.Append(metaBytes);
|
||||
|
||||
// // Frame 2: 原始图 (保持帧位对齐,无数据则发空帧)
|
||||
// if (payload.HasOriginalImage && payload.OriginalImageBytes != null)
|
||||
// msg.Append(payload.OriginalImageBytes);
|
||||
// else
|
||||
// msg.Append(Array.Empty<byte>());
|
||||
|
||||
// // Frame 3: 处理图
|
||||
// if (payload.HasTargetImage && payload.TargetImageBytes != null)
|
||||
// msg.Append(payload.TargetImageBytes);
|
||||
// else
|
||||
// msg.Append(Array.Empty<byte>());
|
||||
|
||||
// return msg;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 扩展方法:从 NetMQMessage 还原 Payload
|
||||
// /// </summary>
|
||||
// public static VideoPayload ToVideoPayload(this NetMQMessage msg)
|
||||
// {
|
||||
// if (msg == null || msg.FrameCount < 2) return null;
|
||||
|
||||
// // Frame 0 Check
|
||||
// if (msg[0].ConvertToString() != PROTOCOL_HEADER) return null;
|
||||
|
||||
// //// Frame 1: Metadata
|
||||
// //string json = msg[1].ConvertToString();
|
||||
// //var payload = VideoPayload.FromMetadataJson(json);
|
||||
|
||||
// // [新代码] 直接从二进制还原
|
||||
// // ToByteArray() 虽然会产生一次拷贝,但对于 Metadata 这种小数据影响微乎其微
|
||||
// // 相比 JSON 解析 String 的开销,这已经非常快了
|
||||
// var payload = MessagePackSerializer.Deserialize<VideoPayload>(msg[1].ToByteArray());
|
||||
// if (payload == null) return null;
|
||||
|
||||
// // Frame 2: Raw Image
|
||||
// // 利用 BufferSize 避免不必要的内存拷贝,如果长度为0则跳过
|
||||
// if (payload.HasOriginalImage && msg[2].BufferSize > 0)
|
||||
// {
|
||||
// payload.OriginalImageBytes = msg[2].ToByteArray();
|
||||
// }
|
||||
|
||||
// // Frame 3: Processed Image
|
||||
// if (payload.HasTargetImage && msg[3].BufferSize > 0)
|
||||
// {
|
||||
// payload.TargetImageBytes = msg[3].ToByteArray();
|
||||
// }
|
||||
|
||||
// return payload;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,123 +0,0 @@
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SHH.CameraSdk;
|
||||
using SHH.Contracts;
|
||||
using SHH.Contracts.Grpc; // 引用 Proto 生成的命名空间
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SHH.CameraService
|
||||
{
|
||||
/// <summary>
|
||||
/// gRPC 指令接收后台服务
|
||||
/// 负责:1. 逻辑注册 2. 维持指令长连接 3. 指令分发
|
||||
/// </summary>
|
||||
public class GrpcCommandReceiverWorker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<GrpcCommandReceiverWorker> _logger;
|
||||
private readonly ServiceConfig _config;
|
||||
private readonly IEnumerable<ICommandHandler> _handlers; // 自动注入所有指令处理器
|
||||
|
||||
public GrpcCommandReceiverWorker(
|
||||
ILogger<GrpcCommandReceiverWorker> logger,
|
||||
ServiceConfig config,
|
||||
IEnumerable<ICommandHandler> handlers)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_handlers = handlers;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// 给 SDK 和数据库留出几秒钟的加载时间
|
||||
_logger.LogInformation("[gRPC Bus] 后台 Worker 准备就绪,3秒后发起连接...");
|
||||
await Task.Delay(3000, stoppingToken);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 地址预处理 (将 127.0.0.1 强制转换为 localhost 解决 Unimplemented 问题)
|
||||
var ep = _config.CommandEndpoints.First();
|
||||
string targetUrl = ep.Uri.Replace("tcp://", "http://").Replace("127.0.0.1", "localhost");
|
||||
|
||||
using var channel = GrpcChannel.ForAddress(targetUrl);
|
||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
// --- 第一步:发起逻辑注册 (Unary) ---
|
||||
_logger.LogInformation("[gRPC Bus] 正在发起逻辑注册: {Url}", targetUrl);
|
||||
var regResp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
{
|
||||
InstanceId = _config.AppId,
|
||||
Version = "2.0.0-grpc",
|
||||
ServerIp = "127.0.0.1",
|
||||
StartTimeTicks = DateTime.Now.Ticks
|
||||
}, cancellationToken: stoppingToken);
|
||||
|
||||
if (regResp.Success)
|
||||
{
|
||||
_logger.LogInformation("[gRPC Bus] 逻辑注册成功。正在开启长连接指令通道...");
|
||||
|
||||
// --- 第二步:开启物理指令流 (Server Streaming) ---
|
||||
using var call = client.OpenCommandChannel(new CommandStreamRequest
|
||||
{
|
||||
InstanceId = _config.AppId
|
||||
}, cancellationToken: stoppingToken);
|
||||
|
||||
// --- 第三步:阻塞式监听服务端推送 ---
|
||||
// 只要服务端通过 responseStream.WriteAsync 发消息,这里就会命中
|
||||
while (await call.ResponseStream.MoveNext(stoppingToken))
|
||||
{
|
||||
var protoMsg = call.ResponseStream.Current;
|
||||
_logger.LogInformation("[gRPC Bus] 收到远程指令: {CmdCode}", protoMsg.CmdCode);
|
||||
|
||||
// 异步分发,不阻塞接收循环
|
||||
_ = DispatchCommandAsync(protoMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("[gRPC Bus] 链路异常,5秒后重试: {Msg}", ex.Message);
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指令分发逻辑
|
||||
/// </summary>
|
||||
private async Task DispatchCommandAsync(CommandPayloadProto msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 寻找匹配的处理器 (SyncCameraHandler / RemoveCameraHandler)
|
||||
var handler = _handlers.FirstOrDefault(h => h.ActionName == msg.CmdCode);
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
// 2. 将 Proto 的参数转为 JToken,保持与原有处理器兼容
|
||||
var jsonParams = JToken.Parse(msg.JsonParams);
|
||||
await handler.ExecuteAsync(jsonParams);
|
||||
_logger.LogInformation("[gRPC Bus] 指令 {CmdCode} 执行完成", msg.CmdCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("[gRPC Bus] 未找到处理 {CmdCode} 的处理器", msg.CmdCode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[gRPC Bus] 指令执行失败: {CmdCode}", msg.CmdCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace SHH.CameraService;
|
||||
/// <summary>
|
||||
/// 同步设备配置处理器
|
||||
/// </summary>
|
||||
public class SyncCameraHandler : ICommandHandler
|
||||
public class DeviceConfigHandler : ICommandHandler
|
||||
{
|
||||
private readonly CameraManager _cameraManager;
|
||||
|
||||
@@ -20,7 +20,7 @@ public class SyncCameraHandler : ICommandHandler
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="cameraManager"></param>
|
||||
public SyncCameraHandler(CameraManager cameraManager)
|
||||
public DeviceConfigHandler(CameraManager cameraManager)
|
||||
{
|
||||
_cameraManager = cameraManager;
|
||||
}
|
||||
@@ -13,11 +13,11 @@ namespace SHH.CameraService;
|
||||
/// 设备状态监控工作者 (gRPC 版)
|
||||
/// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRPC 批量上报至所有配置的端点
|
||||
/// </summary>
|
||||
public class DeviceStateMonitorWorker : BackgroundService
|
||||
public class DeviceStatusHandler : BackgroundService
|
||||
{
|
||||
private readonly CameraManager _manager;
|
||||
private readonly ServiceConfig _config;
|
||||
private readonly ILogger<DeviceStateMonitorWorker> _logger;
|
||||
private readonly ILogger<DeviceStatusHandler> _logger;
|
||||
|
||||
// 状态存储:CameraId -> 状态载荷
|
||||
private readonly ConcurrentDictionary<string, StatusEventPayload> _stateStore = new();
|
||||
@@ -25,10 +25,10 @@ public class DeviceStateMonitorWorker : BackgroundService
|
||||
private volatile bool _isDirty = false;
|
||||
private long _lastSendTick = 0;
|
||||
|
||||
public DeviceStateMonitorWorker(
|
||||
public DeviceStatusHandler(
|
||||
CameraManager manager,
|
||||
ServiceConfig config,
|
||||
ILogger<DeviceStateMonitorWorker> logger)
|
||||
ILogger<DeviceStatusHandler> logger)
|
||||
{
|
||||
_manager = manager;
|
||||
_config = config;
|
||||
@@ -104,7 +104,6 @@ public class DeviceStateMonitorWorker : BackgroundService
|
||||
// 1. 构建 gRPC 请求包
|
||||
var request = new StatusBatchRequest
|
||||
{
|
||||
Protocol = "GRPC",
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
|
||||
@@ -116,7 +115,6 @@ public class DeviceStateMonitorWorker : BackgroundService
|
||||
CameraId = item.CameraId,
|
||||
IsOnline = item.IsOnline,
|
||||
Reason = item.Reason,
|
||||
Timestamp = item.Timestamp
|
||||
});
|
||||
}
|
||||
|
||||
102
SHH.CameraService/GrpcImpls/Handlers/GatewayService.cs
Normal file
102
SHH.CameraService/GrpcImpls/Handlers/GatewayService.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SHH.CameraSdk;
|
||||
using SHH.Contracts.Grpc; // 引用 Proto 生成的命名空间
|
||||
|
||||
namespace SHH.CameraService
|
||||
{
|
||||
/// <summary>
|
||||
/// gRPC 指令接收后台服务
|
||||
/// 职责:
|
||||
/// 1. 维护与 AiVideo 的 gRPC 长连接。
|
||||
/// 2. 完成节点逻辑注册。
|
||||
/// 3. 监听 Server Streaming 指令流并移交给 Dispatcher。
|
||||
/// </summary>
|
||||
public class GatewayService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<GatewayService> _logger;
|
||||
private readonly ServiceConfig _config;
|
||||
private readonly CommandDispatcher _dispatcher;
|
||||
|
||||
public GatewayService(
|
||||
ILogger<GatewayService> logger,
|
||||
ServiceConfig config,
|
||||
CommandDispatcher dispatcher)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// 预留系统启动缓冲时间,确保数据库和 SDK 已就绪
|
||||
_logger.LogInformation("[gRPC Bus] 指令接收服务启动,等待环境预热...");
|
||||
await Task.Delay(3000, stoppingToken);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 地址适配:将 tcp 转换为 http,并将 127.0.0.1 修正为 localhost 解决 Unimplemented 异常
|
||||
var ep = _config.CommandEndpoints.First();
|
||||
string targetUrl = ep.Uri.Replace("tcp://", "http://").Replace("127.0.0.1", "localhost");
|
||||
|
||||
using var channel = GrpcChannel.ForAddress(targetUrl);
|
||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
// --- 第一步:发起节点逻辑注册 (Unary) ---
|
||||
_logger.LogInformation("[gRPC Bus] 正在发起逻辑注册: {Url}", targetUrl);
|
||||
var regResp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
{
|
||||
InstanceId = _config.AppId,
|
||||
Version = "2.0.0-grpc",
|
||||
ServerIp = "127.0.0.1",
|
||||
StartTimeTicks = DateTime.Now.Ticks
|
||||
}, cancellationToken: stoppingToken);
|
||||
|
||||
if (regResp.Success)
|
||||
{
|
||||
_logger.LogInformation("[gRPC Bus] 注册成功。正在建立双向指令通道...");
|
||||
|
||||
// --- 第二步:开启 Server Streaming 指令流 ---
|
||||
using var call = client.OpenCommandChannel(new CommandStreamRequest
|
||||
{
|
||||
InstanceId = _config.AppId
|
||||
}, cancellationToken: stoppingToken);
|
||||
|
||||
// --- 第三步:循环读取服务端推送的指令 ---
|
||||
// 只要服务端流未断开,此处会一直阻塞等待新消息
|
||||
while (await call.ResponseStream.MoveNext(stoppingToken))
|
||||
{
|
||||
var protoMsg = call.ResponseStream.Current;
|
||||
|
||||
// 核心变更:不再直接处理业务,而是通过分发器进行路由
|
||||
// 使用 _ = 异步处理,避免某个 Handler 执行过慢导致指令流阻塞
|
||||
_ = _dispatcher.DispatchAsync(protoMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 响应系统正常退出信号
|
||||
break;
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
_logger.LogError("[gRPC Bus] RPC 异常 (Status: {Code}): {Msg}", ex.StatusCode, ex.Message);
|
||||
// 链路异常,进入重连等待阶段
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("[gRPC Bus] 非预期链路异常: {Msg},5秒后尝试重连", ex.Message);
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Diagnostics;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
public class NetworkStreamingWorker : BackgroundService
|
||||
public class ImageMonitorController : BackgroundService
|
||||
{
|
||||
// 注入所有注册的目标(云端、大屏等),实现动态分发
|
||||
private readonly IEnumerable<StreamTarget> _targets;
|
||||
@@ -16,7 +16,7 @@ public class NetworkStreamingWorker : BackgroundService
|
||||
// 如果您确实需要 100,请注意带宽压力。此处我保留您要求的 100,但建议未来调优。
|
||||
private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 };
|
||||
|
||||
public NetworkStreamingWorker(IEnumerable<StreamTarget> targets)
|
||||
public ImageMonitorController(IEnumerable<StreamTarget> targets)
|
||||
{
|
||||
_targets = targets;
|
||||
}
|
||||
@@ -46,10 +46,17 @@ public class GrpcSenderWorker : BackgroundService
|
||||
// 3. 核心搬运循环:从内存队列 (Channel) 读取数据
|
||||
await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
// 【畅通保障】检查数据时效性:丢弃超过 1 秒的积压帧
|
||||
var delay = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - payload.CaptureTimestamp;
|
||||
if (delay > 1000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 将业务 DTO 转换为 gRPC 原生 Request
|
||||
var request = new VideoFrameRequest
|
||||
{
|
||||
CameraId = payload.CameraId ?? "Unknown",
|
||||
CameraId = payload.CameraId ?? "0",
|
||||
CaptureTimestamp = payload.CaptureTimestamp,
|
||||
OriginalWidth = payload.OriginalWidth,
|
||||
OriginalHeight = payload.OriginalHeight,
|
||||
@@ -66,6 +73,8 @@ public class GrpcSenderWorker : BackgroundService
|
||||
: ByteString.Empty
|
||||
};
|
||||
|
||||
request.SubscriberIds.AddRange(payload.SubscriberIds);
|
||||
|
||||
// 处理诊断信息 map<string, string>
|
||||
if (payload.Diagnostics != null)
|
||||
{
|
||||
@@ -49,7 +49,7 @@ public class Program
|
||||
InstanceId = config.AppId,
|
||||
Version = "2.0.0-grpc",
|
||||
ServerIp = "127.0.0.1",
|
||||
WebApiPort = config.BasePort,
|
||||
WebapiPort = config.BasePort,
|
||||
StartTimeTicks = DateTime.Now.Ticks,
|
||||
ProcessId = Environment.ProcessId,
|
||||
Description = "Camera Service"
|
||||
@@ -79,9 +79,9 @@ public class Program
|
||||
builder.Services.AddHostedService<CameraEngineWorker>();
|
||||
|
||||
// ★ 注册 gRPC 版本的状态监控工作者 (不讲道理,直接注册)
|
||||
builder.Services.AddHostedService<DeviceStateMonitorWorker>();
|
||||
builder.Services.AddHostedService<DeviceStatusHandler>();
|
||||
builder.Services.AddHostedService<ParentProcessSentinel>();
|
||||
builder.Services.AddHostedService<GrpcCommandReceiverWorker>();
|
||||
builder.Services.AddHostedService<GatewayService>();
|
||||
|
||||
// =============================================================
|
||||
// 5. 视频流 Target 注册 (gRPC 模式)
|
||||
@@ -100,7 +100,7 @@ public class Program
|
||||
}
|
||||
}
|
||||
builder.Services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
||||
builder.Services.AddHostedService<NetworkStreamingWorker>();
|
||||
builder.Services.AddHostedService<ImageMonitorController>();
|
||||
|
||||
// 为每个 Target 绑定一个 gRPC 流发送者
|
||||
foreach (var target in netTargets)
|
||||
@@ -112,7 +112,7 @@ public class Program
|
||||
// 注册指令分发 (不再使用 NetMQ 的 CommandClientWorker)
|
||||
builder.Services.AddSingleton<InterceptorPipeline>();
|
||||
builder.Services.AddSingleton<CommandDispatcher>();
|
||||
builder.Services.AddSingleton<ICommandHandler, SyncCameraHandler>();
|
||||
builder.Services.AddSingleton<ICommandHandler, DeviceConfigHandler>();
|
||||
builder.Services.AddSingleton<ICommandHandler, RemoveCameraHandler>();
|
||||
|
||||
ConfigureWebServices(builder, config);
|
||||
|
||||
@@ -18,64 +18,7 @@ service GatewayProvider {
|
||||
rpc OpenCommandChannel (CommandStreamRequest) returns (stream CommandPayloadProto);
|
||||
}
|
||||
|
||||
// --- 1. 注册相关 ---
|
||||
message RegisterRequest {
|
||||
// 进程 ID (用于区分同一台机器上的多个实例)
|
||||
int32 process_id = 1;
|
||||
// 调用进程句柄
|
||||
int32 invoke_process_id = 2;
|
||||
// 实例唯一标识符 (例如 "Stream_1")
|
||||
string instance_id = 3;
|
||||
// 软件版本号
|
||||
string version = 4;
|
||||
// 软件所在的局域网 IP
|
||||
string server_ip = 5;
|
||||
// WebAPI 监听端口
|
||||
int32 webapi_port = 6;
|
||||
// Grpc通讯端口
|
||||
int32 grpc_port = 7;
|
||||
// 启动时间
|
||||
int64 start_time_ticks = 9;
|
||||
// 描述信息
|
||||
string description = 10;
|
||||
}
|
||||
|
||||
// --- 2. 状态上报相关 ---
|
||||
message StatusBatchRequest {
|
||||
string protocol = 1;
|
||||
int64 timestamp = 2;
|
||||
repeated StatusEventItem items = 3;
|
||||
}
|
||||
|
||||
message StatusEventItem {
|
||||
string camera_id = 1;
|
||||
bool is_online = 2;
|
||||
string reason = 3;
|
||||
int64 timestamp = 4;
|
||||
}
|
||||
|
||||
// --- 3. 视频流相关 ---
|
||||
message VideoFrameRequest {
|
||||
string camera_id = 1;
|
||||
int64 capture_timestamp = 2;
|
||||
int64 dispatch_timestamp = 3;
|
||||
int32 original_width = 4;
|
||||
int32 original_height = 5;
|
||||
int32 target_width = 6;
|
||||
int32 target_height = 7;
|
||||
repeated string subscriber_ids = 8;
|
||||
map<string, string> diagnostics = 9;
|
||||
bool has_original_image = 10;
|
||||
bool has_target_image = 11;
|
||||
bytes original_image_bytes = 12;
|
||||
bytes target_image_bytes = 13;
|
||||
}
|
||||
|
||||
// --- 4. 指令下发相关 (对应 C# CommandPayload) ---
|
||||
message CommandStreamRequest {
|
||||
string instance_id = 1; // 告知服务端我是哪个节点
|
||||
}
|
||||
|
||||
// --- 通用指令推送通道 ---
|
||||
message CommandPayloadProto {
|
||||
string protocol = 1; // 协议类型,默认 "COMMAND"
|
||||
string cmd_code = 2; // 指令代码,如 "Sync_Camera"
|
||||
@@ -88,6 +31,57 @@ message CommandPayloadProto {
|
||||
int64 expire_time = 9; // 过期时间戳
|
||||
}
|
||||
|
||||
// --- 1. 注册相关 ---
|
||||
message RegisterRequest {
|
||||
int32 process_id = 1; // 进程 ID (用于区分同一台机器上的多个实例)
|
||||
int32 invoke_process_id = 2; // 调用进程句柄
|
||||
string instance_id = 3; // 实例唯一标识符 (例如 "Stream_1")
|
||||
string version = 4; // 软件版本号
|
||||
string server_ip = 5; // 软件所在的局域网 IP
|
||||
int32 webapi_port = 6; // WebAPI 监听端口
|
||||
int32 grpc_port = 7; // Grpc通讯端口
|
||||
int64 start_time_ticks = 9; // 启动时间
|
||||
string description = 10; // 描述信息
|
||||
}
|
||||
|
||||
// --- 2. 状态上报相关 ---
|
||||
message StatusBatchRequest {
|
||||
int64 timestamp = 1; // 上报时间戳
|
||||
repeated StatusEventItem items = 2; // 状态事件列表
|
||||
}
|
||||
|
||||
// 设备状态变更通知包
|
||||
message StatusEventItem {
|
||||
string camera_id = 1; // 摄像头ID
|
||||
bool is_online = 2; // 是否在线
|
||||
string reason = 3; // 状态变更原因描述
|
||||
}
|
||||
|
||||
// --- 3. 视频流传输协议 ---
|
||||
// 职责:承载高频传输的实时视频帧、算法处理图及相关的 AI 诊断元数据
|
||||
message VideoFrameRequest {
|
||||
|
||||
string camera_id = 1; // 摄像头唯一物理标识符
|
||||
int64 capture_timestamp = 2; // 图像在传感器端的原始采集时间戳 (Ticks/Unixms)
|
||||
int64 dispatch_timestamp = 3; // 图像在分析节点端的分发/外传时间戳 (用于测量网络传输耗时)
|
||||
int32 original_width = 4; // 原始采集图像的宽度
|
||||
int32 original_height = 5; // 原始采集图像的高度
|
||||
int32 target_width = 6; // 算法处理(如缩放或裁剪)后的目标图像宽度
|
||||
int32 target_height = 7; // 算法处理后的目标图像高度
|
||||
repeated string subscriber_ids = 8; // 订阅此帧的应用标识列表 (例如: "UI", "AI", "Record")
|
||||
map<string, string> diagnostics = 9; // 诊断与扩展元数据 键值对存储:例如 {"fps": "25", "bitrate": "4Mbps", "algo_latency": "12ms"}
|
||||
bool has_original_image = 10; // 状态标志:包内是否包含原始图像二进制数据
|
||||
bool has_target_image = 11; // 状态标志:包内是否包含算法处理图(或带 OSD 渲染的图)
|
||||
bytes original_image_bytes = 12; // 原始图像二进制数据 (通常为 JPG/NV12 格式)
|
||||
bytes target_image_bytes = 13; // 算法处理图/标注图二进制数据
|
||||
}
|
||||
|
||||
// --- 4. 指令下发相关 (对应 C# CommandPayload) ---
|
||||
message CommandStreamRequest {
|
||||
string instance_id = 1; // 告知服务端我是哪个节点
|
||||
int32 process_id = 2; // 告知服务端我是哪个进程
|
||||
}
|
||||
|
||||
message GenericResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
|
||||
Reference in New Issue
Block a user