海康威視頻監控設備Web查看系統(二):服務器篇


聲明:本系列文章只提供交流與學習使用。文章中所有涉及到海康威視設備的SDK均可在海康威視官方網站下載得到。文章中所有除官方SDK意外的代碼均可隨意使用,任何涉及到海康威視公司利益的非正常使用由使用者自己負責,與本人無關。

前言:

      上一篇文章《海康威視頻監控設備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資源就不應該被釋放。因此再簡化一下流程每個節點都有相應的代碼實現如如下所示:

 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 用戶數據的句柄。我做的這個視頻的中轉功能其實就是在這個回調函數中實現的。

      好了,核心的代碼都摘出來了,大家按照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     }
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類的實現:

  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 }
ClientClass

 

      簡要說明一下,因為中轉服務的一直處於大量連接數據的發送過程中,所以在Client的構造函數中為每一個實例開了一個本地線程作為數據發送的處理線程,而不是使用線程池來做處理。另外,使用ManualResetEvent實例作為信號量來控制Client實例在發送數據時是按照Server實例從設備采集的數據的順序來一條一條發送的,這樣避免了由於數據流混亂造成的客戶端解碼時出現解碼錯誤或者跳幀等現象。

     好了,視頻中轉服務器端的程序已經開發出來了,接下來要做的就是做一個Web插件來接收服務端的數據並解碼播放,這些內容留作下一篇內容。敬請關注!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM