Files
Ayay/SHH.NetMQ/ForwarderClient.cs

147 lines
5.5 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 System;
using NetMQ;
using NetMQ.Sockets;
using Newtonsoft.Json;
using SHH.Contracts;
namespace SHH.NetMQ
{
/// <summary>
/// 视频转发客户端 (Fusion Version)
/// <para>核心特性:</para>
/// <para>1. 主动推送 (Push Mode)</para>
/// <para>2. 带宽保护 (HWM=2, 拥堵丢弃)</para>
/// <para>3. 高效传输 (Multipart, 避免 Base64)</para>
/// <para>4. 线程安全 (Lock 保护)</para>
/// </summary>
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 --- ---
/// <summary>
/// 推送视频帧 (非阻塞,线程安全)
/// </summary>
/// <param name="payload">视频帧载荷</param>
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<byte>());
// 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
}
}