Files
Ayay/SHH.CameraDashboard/Invokes/CommandServer.cs
2026-01-05 14:54:06 +08:00

167 lines
5.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using NetMQ;
using NetMQ.Sockets;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace SHH.CameraDashboard.Services;
/// <summary>
/// [Dashboard端] 指令控制服务
/// 职责:双向通信通道。接收 Service 心跳/响应,向 Service 发送控制指令。
/// 核心模式ROUTER (Dashboard) <--> DEALER (Service)
/// </summary>
public class CommandServer : IDisposable
{
// 单例模式
public static CommandServer Instance { get; } = new CommandServer();
// 事件:收到消息时触发 (ServiceId, MessageContent)
public event Action<string, string>? OnMessageReceived;
private RouterSocket? _routerSocket;
private NetMQPoller? _poller;
// 【关键新增】发送队列:用于解决跨线程发送的安全问题
// UI线程 -> Enqueue -> Poller线程 -> Socket.Send
private NetMQQueue<CommandPacket>? _sendQueue;
public int ListenPort { get; private set; }
public bool IsRunning => _poller != null && _poller.IsRunning;
// 在线设备表 (可选,用于记录谁在线)
// Key: ServiceId (Identity字符串)
private readonly ConcurrentDictionary<string, DateTime> _onlineClients = new();
private CommandServer() { }
public void Start(int port)
{
ListenPort = port;
if (IsRunning) return;
try
{
// 1. 初始化 Router Socket
_routerSocket = new RouterSocket();
_routerSocket.Bind($"tcp://*:{ListenPort}");
_routerSocket.ReceiveReady += OnSocketReady;
// 2. 初始化发送队列
_sendQueue = new NetMQQueue<CommandPacket>();
_sendQueue.ReceiveReady += OnQueueReady;
// 3. 启动 Poller (同时监听 Socket 接收 和 队列发送)
_poller = new NetMQPoller { _routerSocket, _sendQueue };
// RunAsync 会自动开启后台线程
_poller.RunAsync();
Console.WriteLine($"[Dashboard] 指令服务启动,监听: tcp://*:{ListenPort}");
}
catch (Exception ex)
{
Console.WriteLine($"[Dashboard] 指令端口绑定失败: {ex.Message}");
throw; // 必须抛出,让 App 感知
}
}
/// <summary>
/// 处理来自 Service 的网络消息 (运行在 Poller 线程)
/// </summary>
private void OnSocketReady(object? sender, NetMQSocketEventArgs e)
{
try
{
// 1. 读取身份帧 (Identity)
// 只要 Service 端 DealerSocket 设置了 Identity这里收到就是那个 ID
var identityBytes = e.Socket.ReceiveFrameBytes();
string serviceId = Encoding.UTF8.GetString(identityBytes);
// 2. 读取内容帧 (假设 Dealer 直接发内容,中间无空帧)
// 如果你使用了 REQ/REP 模式,中间可能会有空帧,需注意兼容
string message = e.Socket.ReceiveFrameString();
// 3. 简单的心跳保活逻辑
_onlineClients[serviceId] = DateTime.Now;
// 4. 触发业务事件
// 注意:这依然在 Poller 线程UI 处理时需 Invoke
Console.WriteLine($"[指令] From {serviceId}: {message}");
OnMessageReceived?.Invoke(serviceId, message);
}
catch (Exception ex)
{
Debug.WriteLine($"[Command Receive Error] {ex.Message}");
}
}
/// <summary>
/// 处理发送队列 (运行在 Poller 线程)
/// </summary>
private void OnQueueReady(object? sender, NetMQQueueEventArgs<CommandPacket> e)
{
try
{
if (_routerSocket == null) return;
// 从队列取出一个包
if (e.Queue.TryDequeue(out var packet, TimeSpan.Zero))
{
// Router 发送标准三步走:
// 1. 发送目标 Identity (More = true)
// 2. 发送空帧 (可选取决于协议约定Router-Dealer 直连通常不需要空帧)
// 3. 发送数据 (More = false)
// 这里我们采用最简协议:[Identity][Data]
_routerSocket.SendMoreFrame(packet.TargetId)
.SendFrame(packet.JsonData);
Console.WriteLine($"[指令] To {packet.TargetId}: {packet.JsonData}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"[Command Send Error] {ex.Message}");
}
}
/// <summary>
/// 发送指令 (线程安全,可由 UI 线程调用)
/// </summary>
public void SendCommand(string targetServiceId, object commandData)
{
if (_sendQueue == null) return;
var json = JsonConvert.SerializeObject(commandData);
// ★★★ 核心修复:不直接操作 Socket而是入队 ★★★
_sendQueue.Enqueue(new CommandPacket
{
TargetId = targetServiceId,
JsonData = json
});
}
public void Dispose()
{
_poller?.Stop();
_poller?.Dispose();
_routerSocket?.Dispose();
_sendQueue?.Dispose();
_poller = null;
_routerSocket = null;
_sendQueue = null;
}
// 内部数据包结构
private class CommandPacket
{
public string TargetId { get; set; } = "";
public string JsonData { get; set; } = "";
}
}