聲明:本系列文章只提供交流與學習使用。文章中全部涉及到海康威視設備的SDK都可在海康威視官方網站下載獲得。文章中全部除官方SDK意外的代碼都可隨意使用,任何涉及到海康威視公司利益的非正常使用由使用者本身負責,與本人無關。html
前言:緩存
上一篇文章《海康威視頻監控設備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一下,資料很是多,博客園裏也有不少做者寫過這一類的文章,本文就不就這個內容作深刻討論。socket
調用SDK沒有問題了,接下來看看SDK的使用,根據SDK使用文檔,SDK接口的調用須要經過一個標準流程,流程圖以下:ide
按照這個流程,咱們第一步要作的是初始化SDK,而後是三個可選回調函數的設置,接着要作用戶註冊設備即設備登陸,緊接着就是核心的部分了,根據上一篇文章中講的思路,除了預覽模塊外其餘幾個模塊的調用不在咱們要解決的問題範疇,所以不予考慮。最後一步是註銷設備,釋放SDK資源。因此,最後根據咱們的需求,流程簡化以下:函數
雖然標準流程如此,可是咱們的服務端程序只有一個單一的任務,因此也沒有必要對爲託管資源進行釋放,由於若是退出程序之後資源就會釋放,不退出程序的話,SDK資源就不該該被釋放。所以再簡化一下流程每一個節點都有相應的代碼實現如以下所示:學習
1 //初始化SDK 2 CHCNetSDK.NET_DVR_Init(); 3 4 //用戶登陸 5 CHCNetSDK.NET_DVR_DEVICEINFO_V30 DeviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V30(); 6 CHCNetSDK.NET_DVR_Login_V30(設備IP地址, 設備端口, 用戶名, 密碼, ref DeviceInfo); 7 //說明:關於設備IP、端口、用戶名及密碼信息請根據本身要訪問設備的設置正確填寫 8 9 //預覽模塊 10 CHCNetSDK.NET_DVR_CLIENTINFO lpClientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO(); 11 lpClientInfo.lChannel = channel; 12 lpClientInfo.lLinkMode = 0x0000; 13 lpClientInfo.sMultiCastIP = ""; 14 m_fRealData = new CHCNetSDK.REALDATACALLBACK(RealDataCallBack); 15 IntPtr pUser = new IntPtr(); 16 CHCNetSDK.NET_DVR_RealPlay_V30(m_lUserID, ref lpClientInfo, m_fRealData, pUser, 1); 17 //說明:這裏的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 用戶數據的句柄。我作的這個視頻的中轉功能其實就是在這個回調函數中實現的。this
好了,核心的代碼都摘出來了,你們按照SDK提供的Demo照貓畫虎就能夠把預覽功能實現出來了。
服務端設計:
實現了預覽功能,下面看看中轉服務的實現。其中包含三個類:Server,Client以及ClientList類。
Server類主要負責從設備讀取數據並將數據緩存到服務器上,而且做爲Socket監聽服務端;ClientList維護一個客戶端列表,並在Server獲取到設備數據時便利客戶端列表發送數據到客戶端;Client類主要負責將服務端緩存的數據分發到各個終端請求上。
三個類的關係及主要成員請看下圖:
Server類:
1 class Server 2 { 3 int m_lUserID = -1; 4 //頭數據 5 byte[] headStream; 6 7 ClientList clientList = ClientList.GetClientList(); 8 CHCNetSDK.REALDATACALLBACK m_fRealData; 9 Socket listenSocket; 10 Semaphore m_maxNumberAcceptedClients; 11 /// <summary> 12 /// Server構造函數,啓動服務端Socket及海康SDK獲取設備數據 13 /// </summary> 14 /// <param name="ipPoint">服務端IP配置</param> 15 /// <param name="numConnections">最大客戶端鏈接數</param> 16 /// <param name="channel">設備監聽通道</param> 17 public Server(IPEndPoint ipPoint, int numConnections, int channel) 18 { 19 if (!InitHK()) 20 { 21 return; 22 } 23 RunGetStream(channel); 24 25 listenSocket = new Socket(ipPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 26 listenSocket.Bind(ipPoint); 27 m_maxNumberAcceptedClients = new Semaphore(numConnections, numConnections); 28 listenSocket.Listen(100); 29 Console.WriteLine("開始監聽客戶端鏈接......"); 30 StartAccept(null); 31 } 32 33 #region HKSDK 34 35 private void RunGetStream(int channel) 36 { 37 if (m_lUserID != -1)//初始化成功 38 { 39 CHCNetSDK.NET_DVR_CLIENTINFO lpClientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO(); 40 lpClientInfo.lChannel = channel; 41 lpClientInfo.lLinkMode = 0x0000; 42 lpClientInfo.sMultiCastIP = ""; 43 m_fRealData = new CHCNetSDK.REALDATACALLBACK(RealDataCallBack); 44 IntPtr pUser = new IntPtr(); 45 int m_lRealHandle = CHCNetSDK.NET_DVR_RealPlay_V30(m_lUserID, ref lpClientInfo, m_fRealData, pUser, 1); 46 Console.WriteLine("開始獲取視頻數據......"); 47 } 48 else//初始化 失敗,由於已經初始化了 49 { 50 Console.WriteLine("視頻數據獲取失敗......"); 51 } 52 } 53 54 private bool InitHK() 55 { 56 bool m_bInitSDK = CHCNetSDK.NET_DVR_Init(); 57 if (m_bInitSDK == false) 58 { 59 return false; 60 } 61 else 62 { 63 Console.WriteLine("設備SDK初始化成功......."); 64 CHCNetSDK.NET_DVR_DEVICEINFO_V30 DeviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V30(); 65 m_lUserID = CHCNetSDK.NET_DVR_Login_V30("設備IP", 鏈接端口, "鏈接用戶名", "鏈接密碼", ref DeviceInfo); 66 if (m_lUserID != -1) 67 { 68 Console.WriteLine("監控設備登陸成功......."); 69 return true; 70 } 71 else 72 { 73 Console.WriteLine("監控設備登陸失敗,稍後再試......."); 74 return false; 75 } 76 } 77 } 78 79 private void RealDataCallBack(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser) 80 { 81 byte[] data = new byte[dwBufSize]; 82 Marshal.Copy(pBuffer, data, 0, (int)dwBufSize); 83 Console.WriteLine("監控設備鏈接正常......"); 84 if (dwDataType == CHCNetSDK.NET_DVR_SYSHEAD) 85 { 86 headStream = data; 87 } 88 clientList.SetSendData(data); 89 return; 90 } 91 92 #endregion 93 94 #region Socket 95 /// <summary> 96 /// 監聽客戶端 97 /// </summary> 98 /// <param name="acceptEventArg"></param> 99 private void StartAccept(SocketAsyncEventArgs acceptEventArg) 100 { 101 if (acceptEventArg == null) 102 { 103 acceptEventArg = new SocketAsyncEventArgs(); 104 acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed); 105 } 106 else 107 { 108 acceptEventArg.AcceptSocket = null; 109 } 110 111 m_maxNumberAcceptedClients.WaitOne(); 112 bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArg); 113 if (!willRaiseEvent) 114 { 115 ProcessAccept(acceptEventArg); 116 } 117 } 118 /// <summary> 119 /// 增長客戶端列表 120 /// </summary> 121 /// <param name="e"></param> 122 private void ProcessAccept(SocketAsyncEventArgs e) 123 { 124 clientList.AddClient(new Client(e.AcceptSocket, headStream)); 125 StartAccept(e); 126 } 127 128 /// <summary> 129 /// Socket回調函數 130 /// </summary> 131 /// <param name="sender"></param> 132 /// <param name="e"></param> 133 private void IO_Completed(object sender, SocketAsyncEventArgs e) 134 { 135 switch (e.LastOperation) 136 { 137 case SocketAsyncOperation.Accept: 138 ProcessAccept(e); 139 break; 140 default: 141 throw new ArgumentException("The last operation completed on the socket was not a receive or send"); 142 } 143 } 144 145 #endregion 146 147 }
這裏有個細節問題要說明一下,當服務端每次註冊到設備時,設備第一次返回的數據裏面的前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); } } }
在SetSendData方法中遍歷客戶端列表發送數據時,用到了PerformanceCounter對象來控制服務器CPU的使用率,防止CPU資源過載。在實際運行過程當中須要對PerformanceCounter對象獲取的使用率的條件和線程等待時間作適當的微調來達到想要的效果。我這裏的參數是我在PC Server上部署的時候採用的,若是是高CPU配置的話,須要把CPU使用率的判斷條件改小一些,不然會出現服務端單次從設備讀取數據時間過長的問題,在客戶端顯示時出現延時。
最後看看Client類的實現:
1 class Client 2 { 3 /// <summary> 4 /// 客戶端鏈接Socket 5 /// </summary> 6 private Socket socket; 7 /// <summary> 8 /// 發送的數據類型 9 /// </summary> 10 private BufferType type = BufferType.Head; 11 /// <summary> 12 /// 頭數據 13 /// </summary> 14 private byte[] headStream; 15 private SocketError socketError = SocketError.Success; 16 /// <summary> 17 /// 控制數據發送順序信號量 18 /// </summary> 19 private ManualResetEvent sendManual = new ManualResetEvent(false); 20 private byte[] sendData; 21 /// <summary> 22 /// 發送數據線程 23 /// </summary> 24 private Thread sendThread; 25 /// <summary> 26 /// 客戶端構造函數 27 /// </summary> 28 /// <param name="socket"></param> 29 /// <param name="headStream"></param> 30 public Client(Socket socket, byte[] headStream) 31 { 32 this.headStream = headStream; 33 this.socket = socket; 34 sendThread = new Thread((object arg) => 35 { 36 37 while (true) 38 { 39 sendManual.WaitOne(); 40 if (socketError == SocketError.Success) 41 { 42 try 43 { 44 Console.WriteLine(sendData.Length); 45 socket.Send(sendData); 46 } 47 catch (Exception) 48 { 49 Distroy(); 50 break; 51 } 52 53 } 54 sendManual.Reset(); 55 } 56 }); 57 sendThread.IsBackground = true; 58 sendThread.Start(); 59 } 60 /// <summary> 61 /// 62 /// </summary> 63 public SocketError SocketError 64 { 65 get 66 { 67 return socketError; 68 } 69 } 70 /// <summary> 71 /// 72 /// </summary> 73 /// <param name="data"></param> 74 public void SetData(byte[] data) 75 { 76 if (this.socketError != SocketError.Success) 77 { 78 return; 79 } 80 if (type == BufferType.Head && headStream.Length == 40) 81 { 82 sendData = headStream; 83 type = BufferType.Body; 84 } 85 else 86 { 87 sendData = data; 88 } 89 sendManual.Set(); 90 } 91 /// <summary> 92 /// 銷燬Client對象,釋放資源 93 /// </summary> 94 private void Distroy() 95 { 96 this.sendThread.Abort(); 97 this.socket.Shutdown(SocketShutdown.Both); 98 this.socket.Dispose(); 99 this.socketError = SocketError.ConnectionRefused; 100 } 101 } 102 103 enum BufferType 104 { 105 Head, Body 106 }
簡要說明一下,由於中轉服務的一直處於大量鏈接數據的發送過程當中,因此在Client的構造函數中爲每個實例開了一個本地線程做爲數據發送的處理線程,而不是使用線程池來作處理。另外,使用ManualResetEvent實例做爲信號量來控制Client實例在發送數據時是按照Server實例從設備採集的數據的順序來一條一條發送的,這樣避免了因爲數據流混亂形成的客戶端解碼時出現解碼錯誤或者跳幀等現象。
好了,視頻中轉服務器端的程序已經開發出來了,接下來要作的就是作一個Web插件來接收服務端的數據並解碼播放,這些內容留做下一篇內容。敬請關注!