167 lines
5.4 KiB
C#
167 lines
5.4 KiB
C#
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; } = "";
|
||
}
|
||
} |