完善契约与客户端、服务端的收发代码

This commit is contained in:
2026-01-03 00:16:28 +08:00
parent d039559402
commit dcf424a86e
30 changed files with 3292 additions and 349 deletions

View File

@@ -1,69 +1,147 @@
using System;
using NetMQ;
using NetMQ.Sockets;
using Newtonsoft.Json;
using SHH.Contracts;
namespace SHH.NetMQ
{
/// <summary>
/// 视频转发客户端 (Pusher)
/// 特性:主动推送、断线重连、非阻塞
/// 视频转发客户端 (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
{
private PushSocket _pushSocket;
private readonly object _lock = new object();
private bool _isInitialized = false;
#region --- ---
// 同样设置 50 帧的缓存限制
private const int HWM_LIMIT = 50;
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.IsNullOrEmpty(remoteAddress)) return;
_pushSocket = new PushSocket();
// 1. 防堆积设置
_pushSocket.Options.SendHighWatermark = HWM_LIMIT;
if (string.IsNullOrWhiteSpace(remoteAddress))
throw new ArgumentNullException(nameof(remoteAddress));
try
{
// NetMQ 会自动在后台处理重连,无需人工干预
_pushSocket = new PushSocket();
// 1. 配置防堆积策略 (必须在 Connect 之前设置)
_pushSocket.Options.SendHighWatermark = HWM_LIMIT;
// 2. 配置 Linger (逗留时间)
// 设为 0 表示:当 Dispose 时,如果队列里还有没发完的数据,直接扔掉,不要等待。
// 避免关程序时卡死。
_pushSocket.Options.Linger = TimeSpan.Zero;
// 3. 建立连接 (NetMQ 会自动在后台处理断线重连)
_pushSocket.Connect(remoteAddress);
_isInitialized = true;
}
catch (Exception ex)
{
Console.WriteLine($"[Client Error] 连接失败: {ex.Message}");
_isInitialized = false;
// 构造函数异常通常是致命的,向上抛出让启动流程感知
throw new InvalidOperationException($"[ForwarderClient] 初始化失败: {ex.Message}", ex);
}
}
#endregion
#region --- ---
/// <summary>
/// 推送视频帧 (非阻塞,线程安全)
/// </summary>
/// <param name="payload">视频帧载荷</param>
public void Push(VideoPayload payload)
{
if (!_isInitialized || payload == null) return;
if (_isDisposed || payload == null) return;
if (payload.DispatchTime == DateTime.MinValue)
payload.DispatchTime = DateTime.Now;
var msg = new NetMQMessage();
msg.Append(payload.GetMetadataJson());
msg.Append(payload.OriginalImageBytes ?? new byte[0]);
msg.Append(payload.TargetImageBytes ?? new byte[0]);
lock (_lock)
try
{
// 2. 非阻塞推送
// 如果对方挂了,或者网络断了,缓冲区满后这里的 TrySend 会立即返回 false
// 保证 SDK 采集不受影响
bool sent = _pushSocket.TrySendMultipartMessage(TimeSpan.Zero, msg);
// 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()
{
_pushSocket?.Dispose();
if (_isDisposed) return;
lock (_lock)
{
if (_isDisposed) return;
_isDisposed = true;
try
{
_pushSocket?.Close();
_pushSocket?.Dispose();
}
catch { /* 忽略关闭时的异常 */ }
}
}
#endregion
}
}