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; /// /// [Dashboard端] 指令控制服务 /// 职责:双向通信通道。接收 Service 心跳/响应,向 Service 发送控制指令。 /// 核心模式:ROUTER (Dashboard) <--> DEALER (Service) /// public class CommandServer : IDisposable { // 单例模式 public static CommandServer Instance { get; } = new CommandServer(); // 事件:收到消息时触发 (ServiceId, MessageContent) public event Action? OnMessageReceived; private RouterSocket? _routerSocket; private NetMQPoller? _poller; // 【关键新增】发送队列:用于解决跨线程发送的安全问题 // UI线程 -> Enqueue -> Poller线程 -> Socket.Send private NetMQQueue? _sendQueue; public int ListenPort { get; private set; } public bool IsRunning => _poller != null && _poller.IsRunning; // 在线设备表 (可选,用于记录谁在线) // Key: ServiceId (Identity字符串) private readonly ConcurrentDictionary _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(); _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 感知 } } /// /// 处理来自 Service 的网络消息 (运行在 Poller 线程) /// 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}"); } } /// /// 处理发送队列 (运行在 Poller 线程) /// private void OnQueueReady(object? sender, NetMQQueueEventArgs 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}"); } } /// /// 发送指令 (线程安全,可由 UI 线程调用) /// 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; } = ""; } }