视频在线率统计——基于驱动总线设备的领域驱动设计方法落地
视频在线率统计——基于驱动总线设备的领域驱动设计方法落地
1.应用背景
本司智能信息箱产品是管控摄像头电源,监控摄像头视频在线率的一个有效运维工具。因为统计视频在线率是业主十分关心的问题,所以如何有效地统计视频在线率是工程师需要努力解决的问题。
2.各视频在线率统计方法比较
方案 | 是否需要摄像头密码 | 是否能与摄像头交互信息 | 是否能知道摄像头的网络状态 |
---|---|---|---|
ping | 否 | 否 | 是 |
onvif | 是 | 是 | 是 |
ffmpeg | 是 | 是 | 是 |
ping,onvif,ffmpeg三种协议应用场合不同,各有优劣。onvif会多出用户名,密码字段,方法上会多StartStreaming,StopStreaming,及识别视频的编码分辨率等信息,从而从媒体信息地址URI获取视频流;ffmpeg则更进一步,可以直接调用方法分析视频质量等等。
3.本文侧重点
这里需要声明本文侧重点有两方面:
- 面向领域编程,不面向数据库,下文会做详解;
- 第2点的三种协议都可以借鉴linux的设备,总线,驱动与摄像头之间的交互协议设计思想,只是创建子领域对象时onvif会多用户名,密码字段,方法上会多StartStreaming,StopStreaming。ffmpeg则可以直接将需要的方法包装到子领域进行调用。因此本文侧重讲解设备,总线,驱动来开发与硬件设备交互的思想。
4.基于领域驱动来设计摄像头网络状态这一领域
4.1 值对象driverContext
driverContext是用来配置ping驱动软件(new System.Net.Ping())接口正常工作的上下文配置。这里主要是interval,timeout,minSuccess三个字段,其中timeout是驱动内配置,interval,minSuccess为驱动外配置,下文6.1中会有详细举例,按下不表。
3个字段含义详见注释。
public class PingDriverContext
{
///可以增加name字段,表示驱动名称。
/// <summary>
/// ping间隔
/// </summary>
public int interval { get; set; }
/// <summary>
/// ping超时时间
/// </summary>
public int timeout { get; set; }
/// <summary>
/// ping成功最小次数
/// </summary>
public int minSuccess { get; set; }
}
4.2 子领域CameraPingDM
Camera表示摄像头,ping表示检查摄像头网络状态的驱动,DM表示Domain Model即领域模型。
4.2.1 枚举类型摄像头网络状态CameraState
public enum CameraState
{
Online = 0,
Offline = 1,
Unknown = 2
}
4.2.2 属性
private PingDriverContext _driverContext;
private Timer _timer;
private readonly object _lock = new object();
private readonly ReadOnlyMemory<byte> _buffer;
public int CurrentSuccess { get; private set; }
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; }
///// <summary>
///// 状态
///// </summary>
private CameraState cameraState;
public CameraState CameraState
{
get { return cameraState; }
set { cameraState = value; }
}
/// <summary>
/// 摄像头状态更新时间
/// </summary>
public DateTime UpdateTime { get; set; }
- _driverContext
值对象,每个摄像头的timeout,interval,minSuccess都可以配置不同,这里在总线类文件里写死成
interval = 3,//每个摄像头间隔3秒ping一次
timeout = 200,//单次ping等待返回结果200ms超时
minSuccess = 0,//一次ping成功即可认为online
- _lock
多线程并发互斥锁 - _buffer
ping发送的数据包。 - CurrentSuccess
当前ping成功次数,根据minSuccess进行适当的计数清零。 - cameraState
网络状态,详见4.2.1 - UpdateTime
摄像头状态更新时间。
4.2.3 子领域的划分
4.2.3.1析构体
在析构体中,主要传入创建子领域对象所必须的参数。
#region Constructors
/// <summary>
/// 根据IP,以及driverContext创建摄像头领域模型
/// </summary>
/// <param name="driverContext">聚合CameraPingBus中的驱动上下文driverContext</param>
/// <param name="iP">数据库中的摄像头的IP地址</param>
public CameraPingDM(PingDriverContext driverContext, string iP)
{
string data = "ping-pong";
_buffer = Encoding.ASCII.GetBytes(data);
Ip = iP;
_driverContext = driverContext;
LoggerHelper.Debug($"Ping camera IPList every {_driverContext.interval}s");
}
//Dapper数据模型需要
public CameraPingDM(){ }
#endregion
4.2.3.2 创建子领域对象
这里就是传入所需要的参数直接new子领域对象。一般是直接调用,所以它是静态方法。
=>这里是lambda表达式的语法糖,表示返回一个创建好的CameraPingDM子领域对象。
#region Creations
public static CameraPingDM Create(PingDriverContext driverContext, string iP) => new CameraPingDM(driverContext, iP);
#endregion
4.2.3.3 子领域对象内的修改属性行为
主要表现为修改属性值等。这里CameraStateUpdate方法更新摄像头网络状态,同时保存更新时间。
#region Behaviors
/// <summary>
/// 更新摄像头网络状态,同时保存更新时间
/// </summary>
/// <param name="_cameraState"></param>
public void CameraStateUpdate(CameraState _cameraState)
{
cameraState = _cameraState;
UpdateTime = DateTime.Now;
}
#endregion
4.2.3.4 摄像头的网络驱动——ping驱动相关的行为
#region Behaviors with Ping
/// <summary>
/// 表示为ping单个摄像头,检查其网络状态。
/// </summary>
/// <returns></returns>
public async Task<bool> Start()
{
if (_driverContext.interval >= 0)
{
var interval = Convert.ToInt32(TimeSpan.FromSeconds(_driverContext.interval).TotalMilliseconds);
_timer = new Timer(state =>
{
lock (_lock)
{
DoPing();
}
}, null, interval, interval);
}
return true;
}
/// <summary>
/// 根据ping对应IP返回的结果来对当前ping成功次数计数,满足要求为online,否则为offline。
/// </summary>
private void DoPing()
{
var pingSender = new Ping();
var options = new PingOptions
{
//不分包
DontFragment = true
};
try
{
PingReply reply = pingSender.Send(IPAddress.Parse(Ip), _driverContext.timeout, _buffer.ToArray(), options);
LoggerHelper.Debug($"Ping reply for {Ip} is {reply.Status}");
if (reply?.Status == IPStatus.Success)
{
Increment();
}
else
{
Decrement();
}
}
catch (Exception)
{
LoggerHelper.Debug($"Ping reply for {Ip} failed");
Decrement();
}
}
/// <summary>
/// 当前ping成功次数CurrentSuccess减1,CurrentSuccess为非负数
/// </summary>
private void Decrement()
{
if (CurrentSuccess <= 0)
{
CurrentSuccess = 0;
CameraStateUpdate(CameraState.Offline);
}
else
{
CurrentSuccess--;
}
}
/// <summary>
/// 当前ping成功次数CurrentSuccess+1,如果大于等于设置的最小ping成功次数,则更新摄像头的网络状态
/// </summary>
private void Increment()
{
if (CurrentSuccess >= _driverContext.minSuccess)
{
CameraStateUpdate(CameraState.Online);
}
else
{
CurrentSuccess++;
}
}
/// <summary>
/// 定时ping定时器关闭
/// </summary>
/// <returns></returns>
public async Task<bool> Stop()
{
_timer?.Dispose();
return true;
}
#endregion
所有方法的作用详见注释,不明白的可以在评论区评论,我会耐心解答,有更好建议的恳请提出。
这里上层聚合CameraPingBus主要调用的就是Start()表示开始ping对应ip的摄像头,根据ping结果刷新摄像头网络状态更新时间;Stop()方法停止ping。这里Timer定时器会在4.3.5中详细介绍,按下不表。
4.3 聚合CameraPingBus
也可称之为CameraPingBus领域,也就是需要我们去解决与摄像头协议交互查看摄像头是否在线的问题域。领域是从需要解决的问题域命名,聚合是从功能角度命名,该类是聚合了许多子领域CameraPingDM,它是去ping 摄像头Camera的行为,返回的是online/offline网络状态值,通过子领域聚合而解决了一整个问题域。
4.3.1 属性
private Timer _dbTimer;
ICamera_Services _camera_Services;
public IList<CameraPingDM> CameraPingDMList = new List<CameraPingDM>();
///可以增加name字段,表示驱动名称。
///写一个IP地址 对应状态变化的方法,将有变化的ADD进差异集合。 如果差异集合不为空,再保存进数据库。
///通过winform修改pingDriverContext 3个参数
//默认参数5/100/0
static PingDriverContext pingDriverContext = new PingDriverContext()
{
interval = 3,
timeout = 200,
minSuccess = 0,
};
- _dbTimer
定时器,数据库定时4秒保存一下摄像头的网络状态。 - _camera_Services
摄像头的数据库数据模型读写服务,依赖注入,析构体调用,简单,如果对依赖注入有疑问可以参照笔者的在net Core3.1上基于winform实现依赖注入实例,这里不做赘述。 - CameraPingDMList
ping摄像头子领域的集合,也就是将所有的CameraPingDM子领域挂载到了Bus总线上。可通过该集合调用CameraPingDM子领域的ping Start, Stop方法。 - pingDriverContext
值对象,这里将所有摄像头的ping驱动配置为同样参数去创建CameraPingDM子领域对象。
4.3.2 析构函数
析构函数调用_camera_Services
public CameraPingBus(ICamera_Services camera_Services)
{
_camera_Services = camera_Services;
}
4.3.3 与CameraPingDM所有子领域相关的行为
#region Behaviors with all the CameraPingDMList
/// <summary>
/// 从数据库的数据模型获取所有摄像头的IP地址加载到CameraPingDM对象集合,由Dapper完成数据模
/// 型的IP到CameraPingDM领域模型IP赋值的转换工作,启动所有CameraPingDM对象集合的ping方法
/// </summary>
/// <returns></returns>
public async Task<bool> CreateAndStartAllCameraPing()
{
var CameraIpList = await GetCameraIpList();
try
{
foreach (var item in CameraIpList)
{
var cameraPingDM = CameraPingDM.Create(pingDriverContext, item.Ip);
await cameraPingDM.Start();
CameraPingDMList.Add(cameraPingDM);
}
}
catch { return false; }
return true;
}
/// <summary>
/// 停止所有CameraPingDM对象集合的ping方法
/// </summary>
/// <returns></returns>
public async Task<bool> StopPing()
{
try
{
foreach (var item in CameraPingDMList)
{
await item.Stop();
}
}
catch { return false; }
return true;
}
/// <summary>
/// 异步获取CameraPingDM对象集合元素数量
/// </summary>
/// <returns></returns>
public async Task<int> CameraIpCount()
{
var CameraIpList = await _camera_Services.GetAllCameraIPAsync();
return CameraIpList.Count();
}
#endregion
所用方法的作用我都做了详细的注释,详见注释。有问题可在评论区提出,我会耐心解答。
4.3.4 领域模型字段在数据库中的读写行为
#region Behaviors with DataBase
/// <summary>
/// 从数据库中加载所有摄像头IP地址到CameraPingDM的IP字段
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<CameraPingDM>> GetCameraIpList()
{
return await _camera_Services.GetAllCameraIPAsync();
}
/// <summary>
/// 将所有Cameara的在线状态根据IP地址匹配定时5秒更新到数据库
/// </summary>
/// <returns></returns>
public async Task<bool> Save2DbTimerStart()
{
_dbTimer = new Timer(state =>
{
_camera_Services.UpdateList(CameraPingDMList);
}, null, 4000, 4000);
return true;
}
/// <summary>
/// 关闭数据库定时保存定时器
/// </summary>
/// <returns></returns>
public async Task<bool> Save2DbTimerStop()
{
_dbTimer?.Dispose();
return true;
}
#endregion
所用方法的作用我都做了详细的注释,详见注释。有问题可在评论区提出,我会耐心解答。
这里需要注意的是由Dapper完成数据模型的IP到CameraPingDM领域模型IP赋值的转换工作,保存也是由Dapper进行了从领域模型的IP,CameraState到数据模型的无缝对接,碍于篇幅过长,时间也很晚了,感兴趣的请在评论区留言。笔者将根据读者反馈情况看是否有必要另起一篇,写一下基于Dapper进行数据模型与领域模型之间的互相转换。
4.3.5 定时器timer介绍
4.3.5.1 定义一个定时器的引用类
用来指向下面的定时器实例。
using System.Threading;
private Timer _dbTimer;
4.3.5.2 定时器使用
定时器的引用类型指向new Timer()实例,目的是为了去写定时器的关闭方法。
_dbTimer = new Timer(state =>
{
_camera_Services.UpdateList(CameraPingDMList);
}, null, 4000, 4000);
这里定时器有4个参数,F12可得如下
// callback:
// A System.Threading.TimerCallback delegate representing a method to be executed.
//
// state:
// An object containing information to be used by the callback method, or null.
//
// dueTime:
// The amount of time to delay before callback is invoked, in milliseconds. Specify
// System.Threading.Timeout.Infinite to prevent the timer from starting. Specify
// zero (0) to start the timer immediately.
//
// period:
// The time interval between invocations of callback, in milliseconds. Specify System.Threading.Timeout.Infinite
// to disable periodic signaling.
- 第一个参数callback即回调函数,也就是定时执行的方法;
- 第二个参数state回调函数的包含信息,这里为null即可;
- 第三个参数dueTime,定时器启动之后延迟调用回调函数的毫秒数;
- 第四个参数period定时周期。
4.3.5.3 定时器的关闭
_dbTimer?.Dispose();
4.3.5.4 与Java的调度器ScheduledExecutorService相比
熟悉Java的道友有没有发现,C#里的Timer与Java的ScheduledExecutorService很相似,也不知道是谁抄谁,或者是异曲同工之妙吧。
import java.util.concurrent.ScheduledExecutorService;
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(DeviceSetting.MAX_GROUP_ID);
executorService.scheduleWithFixedDelay(() -> {
try {
BaseMsg baseMsg = deque.take();
Thread.sleep(AWAKE_TO_PROCESS_INTERVAL);
Channel channel = touchChannel(channelId);
if (channel == null || !channel.isActive()) {
logger.warn("「Channel」" + " Channel「" + channelId + "」无效,无法下发该帧");
removeChannelCompleted(channel);
deque.clear();
return;
}
}, channelId, CommSetting.FRAME_SEND_INTERVAL, TimeUnit.MILLISECONDS);
4.3.5.5 Timers调用库的问题
注意:这里必须要调用System.Threading库里的定时器可以多线程并发执行回调方法,否则的话,将没有此功能,System.Timers定时器使用较为复杂,且无法多线程并发,需要自己写多线程并发的方法,System.Timers定时器只能提供定时功能。
5.依赖注入CameraPingBus,窗体程序析构法调用
5.1 CameraPingBus总线依赖注入
//Domain
services.AddScoped(typeof(CameraPingBus));
5.2 窗体程序析构法调用
public PingSetting(CameraPingBus cameraPingBus)
{
_cameraPingBus = cameraPingBus;
InitializeComponent();
LoggerHelper.Debug("视频在线率配置工具启动");
}
5.3 CameraPingBus使用实例
/// <summary>
/// 按下启动按钮执行操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{
//IP地址从数据库数据模型赋值到领域模型的IP字段,并且每隔3秒开始ping摄像头,保存其网络状态
await _cameraPingBus.CreateAndStartAllCameraPing();
//每隔4s将摄像头网络状态更新到IP地址相等的数据库数据模型中去
await _cameraPingBus.Save2DbTimerStart();
}
/// <summary>
/// 按下停止按钮执行操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button2_Click(object sender, EventArgs e)
{
//停止摄像头定时ping行为
await _cameraPingBus.StopPing();
//停止保存摄像头网络状态到数据库
await _cameraPingBus.Save2DbTimerStop();
}
各调用方法含义详见注释。
5.4 基于net Core3.1的winform工具效果图
工具图示如下:
6.总线,驱动,设备
6.1.驱动
驱动即软件接口,ping是驱动,modbus协议库也是驱动,驱动配置(driverContext)分为驱动内配置与驱动外配置。值对象驱动上下文driverContext就是包含了驱动内配置与驱动外配置。
6.1.1驱动内配置举例
var pingSender = new Ping();
PingReply reply = pingSender.Send(IPAddress.Parse(Ip), _driverContext.timeout, _buffer.ToArray(), options);
_driverContext.timeout即为ping驱动内配置。
6.1.2驱动外配置举例
if (CurrentSuccess >= _driverContext.minSuccess)
{
CameraStateUpdate(CameraState.Online);
}
_driverContext.minSuccess即为驱动外配置。
if (_driverContext.interval >= 0)
{
var interval = Convert.ToInt32(TimeSpan.FromSeconds(_driverContext.interval).TotalMilliseconds);
_timer = new Timer(state =>
{
lock (_lock)
{
DoPing();
}
}, null, interval, interval);
}
_driverContext.interval也为驱动外配置。
6.2 设备
摄像头即设备,这里驱动跟设备是1对1的关系,驱动是设备的一个被动行为,SC平台通过加载驱动的所需配置(driverContext)来获取对应设备的数据(信号或者说状态)。
6.3 总线
总线就像高速公路,他需要有名称,是否关闭,起点,终点,限速(接口参数)。所以这里的IP地址就好比是终点地址,故这里的摄像头IP是属于总线的概念范畴。
具体驱动协议的上一层,一根总线可以对应多个驱动,也可以对应多个设备。
6.4 类比
设备上的信号值(网络状态值)相当于是要寄的快递,驱动相当于是运快递的车,保持车间距,按时到达终点,而总线相当于是车开着的高速路。
7.多线程并发ping摄像头效果图
7.1分析日志记录内容1时刻值
得到结果ping行为为并发
7.2分析日志记录内容2线程号T
得到结果为多线程ping
7.3分析日志记录内容3消息
ping成功与超时与实际在线离线IP地址结果相符。
8. 小结
- 如果设备端是Modbus协议类比可得:Modbus驱动需要加载它的配置信息(所属总线ID,使能,驱动名称,协议,设备地址,发送间隔,接收超时时间,接收超时告警次数,对应设备的寄存器地址等),也即driverContext,加载到Modbus的驱动,驱动配置会包含设备地址,每台设备都会有他自己的驱动配置,将Modbus驱动(也就是new ping())封装到设备的方法里面去(可以封装成AI,DI,AO,DO),将这些配置信息装载到设备的驱动方法里,即可从设备返回值,而新建信号时,就会对该值定义(也或者可以模板形式解析值的显示含义),而这就是最顶层的用户工作组,也就是最大的聚合,也可建立总线,将各类驱动进行分类。以后有时间也会分享相关的信息,不过会稍微复杂一些,但是道理思想类似。
- 一个项目中不一定只有一个聚合像我现在做的智能箱它就需要两个聚合,就有两个问题域
一个是智能设备箱(也就是点位),所有的AI,DI,DO,AO(包含历史数据),摄像头,A接口配置数据,用户,角色,升级,运维公司,运维人员,区域,设备箱告警,协议模板,历史告警都是它的子领域。
摄像头也是一个聚合,摄像头的告警(离线,停电告警),历史告警,摄像头的型号,摄像头的厂商,区域,设备箱,运维公司,运维人员,摄像头驱动,摄像头总线都是摄像头的子领域。
- driver与driverContext
driver就是Ping驱动。
var pingSender = new Ping();
驱动上下文driverContext的字段(配置信息)会加载到驱动pingSender上去,去获取所需要的值,即为软件接口
PingReply reply = pingSender.Send(IPAddress.Parse(Ip), _timeout, _buffer.ToArray(), options);
9.最终
自己对于领域驱动设计的理解并不深刻,但是凭着对设备域,以及协议,总线,驱动的甚微了解,以及看了不少开源项目,不断地学习同行的数据库,硬生生地拼凑成了此文,可能有些概念上或者实现上会有不合适的地方,请路过的高手们不吝赐教。当然如果你有不明白的地方也请提出,我也会耐心解答。
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名JerryMouseLi(包含链接:https://www.cnblogs.com/JerryMouseLi/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。 ———————————————— 版权声明:本文为博客园博主「JerryMouseLi」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://www.cnblogs.com/JerryMouseLi/p/12381098.html