NetMQ 协议,支持摄像头增、删、改

This commit is contained in:
2026-01-12 18:27:58 +08:00
parent 031d4f3416
commit 3f8e42e560
20 changed files with 604 additions and 332 deletions

View File

@@ -53,7 +53,7 @@ namespace SHH.CameraService
/// </summary>
public static VideoPayload ToVideoPayload(this NetMQMessage msg)
{
if (msg == null || msg.FrameCount < 4) return null;
if (msg == null || msg.FrameCount < 2) return null;
// Frame 0 Check
if (msg[0].ConvertToString() != PROTOCOL_HEADER) return null;

View File

@@ -19,43 +19,65 @@ public class NetMqSenderWorker : BackgroundService
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine($"[NetMqSender] 正在连接至服务端: {_target.Config.Endpoint} ...");
// ★★★ 修正点:必须使用 PublisherSocket 来配合接收端的 SubscriberSocket ★★★
// 虽然是 Connect 模式Publisher 依然可以 Connect
using var clientSocket = new PublisherSocket();
// 设置高水位 (HWM)
// 对于 Publisher如果队列满了默认行为就是丢弃旧数据这非常符合视频流需求
clientSocket.Options.SendHighWatermark = 1000;
// 主动连接
clientSocket.Connect(_target.Config.Endpoint);
Console.WriteLine("[NetMqSender] 连接成功,开始从通道搬运数据...");
await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken))
// 增加重启保护
while (!stoppingToken.IsCancellationRequested)
{
try
{
var msg = payload.ToNetMqMessage();
Console.WriteLine($"[NetMqSender] 连接至: {_target.Config.Endpoint}");
// 发送消息
// PublisherSocket 的 TrySend 如果没人订阅或者队列满了,通常不会阻塞,而是直接丢弃或返回
// 注意PUB 模式下,第一帧 ("SHH_V1") 会被当作订阅的主题 (Topic)。
// 你的接收端订阅了 "" (空字符串),所以能收到以任何字符串开头的数据。
bool sent = clientSocket.TrySendMultipartMessage(msg);
using var clientSocket = new PublisherSocket();
clientSocket.Options.SendHighWatermark = 1000;
// 关键:增加 TCP 保活,防止防火墙静默断开长连接
clientSocket.Options.TcpKeepalive = true;
clientSocket.Options.TcpKeepaliveIdle = TimeSpan.FromSeconds(5);
if (!sent)
clientSocket.Connect(_target.Config.Endpoint);
int frameCount = 0;
// 使用更稳健的读取方式
await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken))
{
// 这种情况通常意味着网络断了且 HWM 队列也满了
Console.WriteLine($"[NetMqSender] 警告: 发送队列已满,正在丢帧...");
msg.Clear(); // 手动清理(可选)
try
{
// 1. 构造消息 (内部执行了 MessagePack 序列化)
var msg = payload.ToNetMqMessage();
// 2. 发送
bool sent = clientSocket.TrySendMultipartMessage(msg);
if (!sent)
{
Console.WriteLine($"[NetMqSender] 发送缓冲区满,丢弃帧: {payload.CameraId}");
// ★ 如果没有发送成功,建议显式清理消息帧,防止内存滞留
msg.Clear();
}
else
{
frameCount++;
if (frameCount % 100 == 0)
Console.WriteLine($"[NetMqSender] 已搬运 100 帧至缓冲区.");
}
}
catch (Exception ex)
{
Console.WriteLine($"[NetMqSender] 内部循环异常: {ex.Message}");
}
}
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Console.WriteLine($"[NetMqSender] 异常: {ex.Message}");
// ★★★ 核心改进:捕获异常并等待重试 ★★★
// 防止因为一次内存溢出或网络波动导致整个 BackgroundService 永久停止
Console.WriteLine($"[NetMqSender] 发生致命异常5秒后尝试重建连接: {ex.Message}");
await Task.Delay(5000, stoppingToken);
}
finally
{
// 确保每次循环退出(无论是异常还是正常)都清理环境
NetMQConfig.Cleanup(false);
}
}
}

View File

@@ -106,8 +106,12 @@ public class NetworkStreamingWorker : BackgroundService
// 实现了"物理隔离":一个管道满了(云端卡顿),不影响另一个管道(大屏流畅)。
foreach (var target in _targets)
{
// WriteLog 是非阻塞的。满了就丢弃,返回 false。
target.Channel.WriteLog(payload);
bool ok = target.Channel.WriteLog(payload);
if (!ok)
{
// 如果这里打印,说明管道由于某种原因被关闭了(通常是程序正在退出)
Console.WriteLine($"[DEBUG] 管道写入失败,目标: {target.Config.Name}");
}
}
}
catch (Exception ex)

View File

@@ -26,10 +26,11 @@ namespace SHH.CameraService
/// <summary>
/// [生产者] 写入一个封装好的数据包 (非阻塞)
/// </summary>
public void WriteLog(VideoPayload payload)
public bool WriteLog(VideoPayload payload) // 改为返回 bool
{
// TryWrite 永远不会等待,满了就丢旧的写入新的,返回 true
_channel.Writer.TryWrite(payload);
// TryWrite 在 DropOldest 模式下虽然几乎总是返回 true
// 但如果 Channel 被 Complete (关闭) 了,它会返回 false。
return _channel.Writer.TryWrite(payload);
}
/// <summary>