前言:
上一篇文章《海康威視頻監控設備Web查看系統(一):概要篇》籠統的介紹了關於海康視頻中轉方案的思路,本文將一步步實現方案中的視頻中轉服務端。文中會涉及到一些.net socket處理和基礎的多線程操作。我用的是SDK版本是SDK_Win32_V4.2.8.1 。大家根據自己實際情況想在相應的SDK,頁面的說明裡有詳細的設備型號列表。
分析官方SDK的Demo:
首先來看看官方SDK中的C#版本的Demo,官方Demo分為兩個版本,分別是“實時預覽示例代碼一”和“實時預覽示例代碼二”,因為有現成的C#版本,所以我們使用示例代碼一中的內容。首先關注名為CHCNetSDK的類,這個類封中裝了SDK中的所有非托管方法接口,我們需要來把這個類以及SDK 中的DLL文件一起引入到我們的項目中,如果有對C#調用C++類庫不了解的朋友請自己Google一下,資料非常多,博客園裡也有很多作者寫過這一類的文章,本文就不就這個內容做深入討論。
調用SDK沒有問題了,接下來看看SDK的使用,根據SDK使用文檔,SDK接口的調用需要通過一個標准流程,流程圖如下:
按照這個流程,我們第一步要做的是初始化SDK,然後是三個可選回調函數的設置,接著要做用戶注冊設備即設備登錄,緊接著就是核心的部分了,根據上一篇文章中講的思路,除了預覽模塊外其他幾個模塊的調用不在我們要解決的問題范疇,因此不予考慮。最後一步是注銷設備,釋放SDK資源。所以,最後根據我們的需求,流程簡化如下:
本欄目
雖然標准流程如此,但是我們的服務端程序只有一個單一的任務,所以也沒有必要對為托管資源進行釋放,因為如果退出程序以後資源就會釋放,不退出程序的話,SDK資源就不應該被釋放。因此再簡化一下流程每個節點都有相應的代碼實現如如下所示:
//初始化SDK CHCNetSDK.NET_DVR_Init(); //用戶登錄 CHCNetSDK.NET_DVR_DEVICEINFO_V30 DeviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V30(); CHCNetSDK.NET_DVR_Login_V30(設備IP地址, 設備端口, 用戶名, 密碼, ref DeviceInfo); //說明:關於設備IP、端口、用戶名及密碼信息請根據自己要訪問設備的設置正確填寫 //預覽模塊 CHCNetSDK.NET_DVR_CLIENTINFO lpClientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO(); lpClientInfo.lChannel = channel; lpClientInfo.lLinkMode = 0x0000; lpClientInfo.sMultiCastIP = ""; m_fRealData = new CHCNetSDK.REALDATACALLBACK(RealDataCallBack); IntPtr pUser = new IntPtr(); CHCNetSDK.NET_DVR_RealPlay_V30(m_lUserID, ref lpClientInfo, m_fRealData, pUser, 1); //說明:這裡的NET_DVR_CLIENTINFO類中缺少預覽窗口的句柄,需要預覽時,要根據自己的項目設置NET_DVR_CLIENTINFO對象的hPlayWnd屬性
可能有朋友看到這裡已經忍受不了了,說好的視頻中轉功能在哪呢?別著急,一切的處理都在回調函數RealDataCallBack中,先耐心看一下這個回調函數的簽名
void RealDataCallBack(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser)
第一個lRealHandle是預覽控件的句柄,第二個參數dwDataType說明回調接收到的數據類型,pBuffer 存放數據的緩沖區指針, dwBufSize 緩沖區大小 ,pUser 用戶數據的句柄。我做的這個視頻的中轉功能其實就是在這個回調函數中實現的。
好了,核心的代碼都摘出來了,大家按照SDK提供的Demo照貓畫虎就可以把預覽功能實現出來了。
服務端設計:
實現了預覽功能,下面看看中轉服務的實現。其中包含三個類:Server,Client以及ClientList類。
Server類主要負責從設備讀取數據並將數據緩存到服務器上,並且作為Socket監聽服務端;ClientList維護一個客戶端列表,並在 Server獲取到設備數據時便利客戶端列表發送數據到客戶端;Client類主要負責將服務端緩存的數據分發到各個終端請求上。
三個類的關系及主要成員請看下圖:
Server類:
class Server { int m_lUserID = -1; //頭數據 byte[] headStream; ClientList clientList = ClientList.GetClientList(); CHCNetSDK.REALDATACALLBACK m_fRealData; Socket listenSocket; Semaphore m_maxNumberAcceptedClients; /// <summary> /// Server構造函數,啟動服務端Socket及海康SDK獲取設備數據 /// </summary> /// <param name="ipPoint">服務端IP配置</param> /// <param name="numConnections">最大客戶端連接數</param> /// <param name="channel">設備監聽通道</param> public Server(IPEndPoint ipPoint, int numConnections, int channel) { if (!InitHK()) { return; } RunGetStream(channel); listenSocket = new Socket(ipPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); listenSocket.Bind(ipPoint); m_maxNumberAcceptedClients = new Semaphore(numConnections, numConnections); listenSocket.Listen(100); Console.WriteLine("開始監聽客戶端連接......"); StartAccept(null); } #region HKSDK private void RunGetStream(int channel) { if (m_lUserID != -1)//初始化成功 { CHCNetSDK.NET_DVR_CLIENTINFO lpClientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO(); lpClientInfo.lChannel = channel; lpClientInfo.lLinkMode = 0x0000; lpClientInfo.sMultiCastIP = ""; m_fRealData = new CHCNetSDK.REALDATACALLBACK(RealDataCallBack); IntPtr pUser = new IntPtr(); int m_lRealHandle = CHCNetSDK.NET_DVR_RealPlay_V30(m_lUserID, ref lpClientInfo, m_fRealData, pUser, 1); Console.WriteLine("開始獲取視頻數據......"); } else//初始化 失敗,因為已經初始化了 { Console.WriteLine("視頻數據獲取失敗......"); } } private bool InitHK() { bool m_bInitSDK = CHCNetSDK.NET_DVR_Init(); if (m_bInitSDK == false) { return false; } else { Console.WriteLine("設備SDK初始化成功......."); CHCNetSDK.NET_DVR_DEVICEINFO_V30 DeviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V30(); m_lUserID = CHCNetSDK.NET_DVR_Login_V30("設備IP", 連接端口, "連接用戶名", "連接密碼", ref DeviceInfo); if (m_lUserID != -1) { Console.WriteLine("監控設備登錄成功......."); return true; } else { Console.WriteLine("監控設備登錄失敗,稍後再試......."); return false; } } } private void RealDataCallBack(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser) { byte[] data = new byte[dwBufSize]; Marshal.Copy(pBuffer, data, 0, (int)dwBufSize); Console.WriteLine("監控設備連接正常......"); if (dwDataType == CHCNetSDK.NET_DVR_SYSHEAD) { headStream = data; } clientList.SetSendData(data); return; } #endregion #region Socket /// <summary> /// 監聽客戶端 /// </summary> /// <param name="acceptEventArg"></param> private void StartAccept(SocketAsyncEventArgs acceptEventArg) { if (acceptEventArg == null) { acceptEventArg = new SocketAsyncEventArgs(); acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed); } else { acceptEventArg.AcceptSocket = null; } m_maxNumberAcceptedClients.WaitOne(); bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArg); if (!willRaiseEvent) { ProcessAccept(acceptEventArg); } } /// <summary> /// 增加客戶端列表 /// </summary> /// <param name="e"></param> private void ProcessAccept(SocketAsyncEventArgs e) { clientList.AddClient(new Client(e.AcceptSocket, headStream)); StartAccept(e); } /// <summary> /// Socket回調函數 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void IO_Completed(object sender, SocketAsyncEventArgs e) { switch (e.LastOperation) { case SocketAsyncOperation.Accept: ProcessAccept(e); break; default: throw new ArgumentException("The last operation completed on the socket was not a receive or send"); } } #endregion } ServerClass
這裡有個細節問題要說明一下,當服務端每次注冊到設備時,設備第一次返回的數據裡面的前40個字節是頭數據,在解碼階段時需要將這40字節數據先發送給解碼程序,否則解碼程序將無法正常操作。所以在Server類中單獨保存了這40字節的頭數據以備分發給各個客戶端。
另外,由於我們的客戶端只需要不停的從服務端接收數據,所以服務端設計時只需要將數據分發給客戶端即可,無需在Server類中維護客戶端狀態,因此,服務端Socket只進行監聽操作,當監聽到有客戶端連接時,將客戶端連接添加到ClientList即可。下面看看ClientList類的實現:
class ClientList { private static ClientList list = null; private ClientList() { } private List<Client> socketList = new List<Client>(); /// <summary> /// 獲取ClientList單例 /// </summary> /// <returns></returns> public static ClientList GetClientList() { if (list == null) list = new ClientList(); return list; } /// <summary> /// 將客戶端增加到ClientList中 /// </summary> /// <param name="client"></param> public void AddClient(Client client) { this.socketList.Add(client); } /// <summary> /// 遍歷發送數據到客戶端 /// </summary> /// <param name="data"></param> public void SetSendData(byte[] data) { socketList.RemoveAll((s) => { return s.SocketError != SocketError.Success; }); PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total"); for (int i = 0; i < socketList.Count; i++) { socketList[i].SetData(data); if (p.NextValue() > 50) Thread.Sleep(10); } } } ClientListClass
在SetSendData方法中遍歷客戶端列表發送數據時,用到了PerformanceCounter對象來控制服務器CPU的使用率,防止 CPU資源過載。在實際運行過程中需要對PerformanceCounter對象獲取的使用率的條件和線程等待時間做適當的微調來達到想要的效果。我這裡的參數是我在PC Server上部署的時候采用的,如果是高CPU配置的話,需要把CPU使用率的判斷條件改小一些,否則會出現服務端單次從設備讀取數據時間過長的問題,在客戶端顯示時出現延時。
最後看看Client類的實現:
class Client { /// <summary> /// 客戶端連接Socket /// </summary> private Socket socket; /// <summary> /// 發送的數據類型 /// </summary> private BufferType type = BufferType.Head; /// <summary> /// 頭數據 /// </summary> private byte[] headStream; private SocketError socketError = SocketError.Success; /// <summary> /// 控制數據發送順序信號量 /// </summary> private ManualResetEvent sendManual = new ManualResetEvent(false); private byte[] sendData; /// <summary> /// 發送數據線程 /// </summary> private Thread sendThread; /// <summary> /// 客戶端構造函數 /// </summary> /// <param name="socket"></param> /// <param name="headStream"></param> public Client(Socket socket, byte[] headStream) { this.headStream = headStream; this.socket = socket; sendThread = new Thread((object arg) => { while (true) { sendManual.WaitOne(); if (socketError == SocketError.Success) { try { Console.WriteLine(sendData.Length); socket.Send(sendData); } catch (Exception) { Distroy(); break; } } sendManual.Reset(); } }); sendThread.IsBackground = true; sendThread.Start(); } /// <summary> /// /// </summary> public SocketError SocketError { get { return socketError; } } /// <summary> /// /// </summary> /// <param name="data"></param> public void SetData(byte[] data) { if (this.socketError != SocketError.Success) { return; } if (type == BufferType.Head && headStream.Length == 40) { sendData = headStream; type = BufferType.Body; } else { sendData = data; } sendManual.Set(); } /// <summary> /// 銷毀Client對象,釋放資源 /// </summary> private void Distroy() { this.sendThread.Abort(); this.socket.Shutdown(SocketShutdown.Both); this.socket.Dispose(); this.socketError = SocketError.ConnectionRefused; } } enum BufferType { Head, Body } ClientClass
簡要說明一下,因為中轉服務的一直處於大量連接數據的發送過程中,所以在Client的構造函數中為每一個實例開了一個本地線程作為數據發送的處理線程,而不是使用線程池來做處理。另外,使用ManualResetEvent實例作為信號量來控制Client實例在發送數據時是按照Server實例從設備采集的數據的順序來一條一條發送的,這樣避免了由於數據流混亂造成的客戶端解碼時出現解碼錯誤或者跳幀等現象。
好了,視頻中轉服務器端的程序已經開發出來了,接下來要做的就是做一個Web插件來接收服務端的數據並解碼播放,這些內容留作下一篇內容。敬請關注!
cnblogs 死如秋葉