1. 程式人生 > >視訊線上率統計——基於驅動匯流排裝置的領域驅動設計方法落地

視訊線上率統計——基於驅動匯流排裝置的領域驅動設計方法落地

目錄

  • 視訊線上率統計——基於驅動匯流排裝置的領域驅動設計方法落地
  • 1.應用背景
  • 2.各視訊線上率統計方法比較
  • 3.本文側重點
  • 4.基於領域驅動來設計攝像頭網路狀態這一領域
    • 4.1 值物件driverContext
    • 4.2 子領域CameraPingDM
      • 4.2.1 列舉型別攝像頭網路狀態CameraState
      • 4.2.2 屬性
      • 4.2.3 子領域的劃分
    • 4.3 聚合CameraPingBus
      • 4.3.1 屬性
      • 4.3.3 與CameraPingDM所有子領域相關的行為
      • 4.3.4 領域模型欄位在資料庫中的讀寫行為
      • 4.3.5 定時器timer介紹
  • 5.依賴注入CameraPingBus,窗體程式析構法呼叫
    • 5.1 CameraPingBus匯流排依賴注入
    • 5.2 窗體程式析構法呼叫
    • 5.3 CameraPingBus使用例項
    • 5.4 基於net Core3.1的winform工具效果圖
  • 6.匯流排,驅動,裝置
    • 6.1.驅動
      • 6.1.1驅動內配置舉例
      • 6.1.2驅動外配置舉例
    • 6.2 裝置
    • 6.3 匯流排
    • 6.4 類比
  • 7.多執行緒併發ping攝像頭效果圖
    • 7.1分析日誌記錄內容1時刻值
    • 7.2分析日誌記錄內容2執行緒號T
    • 7.3分析日誌記錄內容3訊息
  • 8. 小結
  • 9.最終

視訊線上率統計——基於驅動匯流排裝置的領域驅動設計方法落地

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.