using System; using NetMQ; using NetMQ.Sockets; using Newtonsoft.Json; using SHH.Contracts; namespace SHH.NetMQ { /// /// 视频转发客户端 (Fusion Version) /// 核心特性: /// 1. 主动推送 (Push Mode) /// 2. 带宽保护 (HWM=2, 拥堵丢弃) /// 3. 高效传输 (Multipart, 避免 Base64) /// 4. 线程安全 (Lock 保护) /// public class ForwarderClient : IDisposable { #region --- 字段与配置 --- private readonly PushSocket _pushSocket; private readonly object _lock = new object(); private bool _isDisposed = false; // ★★★ 核心策略:高水位线 (High Water Mark) ★★★ // 设置为 2,意味着内存队列中最多只允许堆积 2 帧图片。 // 第 3 帧到来时,如果网速不够发不出去,这第 3 帧会被直接丢弃。 // 收益:永远只发最新的图,永远不挤占物理带宽,彻底杜绝延迟累积。 private const int HWM_LIMIT = 2; #endregion #region --- 构造函数 --- public ForwarderClient(string remoteAddress) { if (string.IsNullOrWhiteSpace(remoteAddress)) throw new ArgumentNullException(nameof(remoteAddress)); try { _pushSocket = new PushSocket(); // 1. 配置防堆积策略 (必须在 Connect 之前设置) _pushSocket.Options.SendHighWatermark = HWM_LIMIT; // 2. 配置 Linger (逗留时间) // 设为 0 表示:当 Dispose 时,如果队列里还有没发完的数据,直接扔掉,不要等待。 // 避免关程序时卡死。 _pushSocket.Options.Linger = TimeSpan.Zero; // 3. 建立连接 (NetMQ 会自动在后台处理断线重连) _pushSocket.Connect(remoteAddress); } catch (Exception ex) { // 构造函数异常通常是致命的,向上抛出让启动流程感知 throw new InvalidOperationException($"[ForwarderClient] 初始化失败: {ex.Message}", ex); } } #endregion #region --- 核心发送逻辑 --- /// /// 推送视频帧 (非阻塞,线程安全) /// /// 视频帧载荷 public void Push(VideoPayload payload) { if (_isDisposed || payload == null) return; try { // 1. 准备多帧消息 (Multipart Message) // 这种方式比把 byte[] 转成 Base64 字符串塞进 JSON 要高效得多 (减少 33% 体积,且无 GC 压力) var msg = new NetMQMessage(); // --- 第一帧:元数据 (JSON) --- // 我们使用匿名对象来生成 JSON,刻意排除 byte[] 数组 // 这样生成的 JSON 非常小,只有几十字节 var metaJson = JsonConvert.SerializeObject(new { payload.CameraId, payload.CaptureTime, payload.DispatchTime, payload.OriginalWidth, payload.OriginalHeight, // 如果有订阅者ID列表也带上 payload.SubscriberIds }); msg.Append(metaJson); // --- 第二帧:原始图像数据 (Binary) --- // 直接追加二进制数据,实现 Zero-Copy (零拷贝) // NetMQ 底层会直接搬运这段内存,不会产生临时的 Base64 字符串 msg.Append(payload.OriginalImageBytes ?? Array.Empty()); // 2. 线程安全发送 // NetMQ 的 Socket 实例不是线程安全的,多线程同时调用 Push 必须加锁 lock (_lock) { // 3. 非阻塞尝试发送 (TrySend) // TimeSpan.Zero 表示:如果队列满了 (超过 HWM),立刻返回 false,不要等待。 // 这实现了 "拥堵即丢弃" 的保护机制。 if (!_pushSocket.TrySendMultipartMessage(TimeSpan.Zero, msg)) { // 返回 false 说明触发了 HWM 保护 // 此时我们选择静默丢弃,或者仅在调试模式下打印日志 // Console.WriteLine($"[Drop] 网络拥堵,丢弃帧: {payload.CameraId}"); } } } catch (Exception ex) { // 发送过程中的异常 (通常是 ObjectDisposedException 或 NetMQException) // 捕获它以防止单个帧的发送失败导致整个服务崩溃 Console.WriteLine($"[ForwarderClient] 推送异常: {ex.Message}"); } } #endregion #region --- 资源释放 --- public void Dispose() { if (_isDisposed) return; lock (_lock) { if (_isDisposed) return; _isDisposed = true; try { _pushSocket?.Close(); _pushSocket?.Dispose(); } catch { /* 忽略关闭时的异常 */ } } } #endregion } }