海康威视频监控设备Web查看系统(二):服务器篇
聲明:本系列文章只提供交流與學習使用。文章中所有涉及到??低曉O備的SDK均可在??低暪俜骄W站下載得到。文章中所有除官方SDK意外的代碼均可隨意使用,任何涉及到??低暪纠娴姆钦J褂糜墒褂谜咦约贺撠煟c本人無關。
前言:
上一篇文章《??低曨l監控設備Web查看系統(一):概要篇》籠統的介紹了關于??狄曨l中轉方案的思路,本文將一步步實現方案中的視頻中轉服務端。文中會涉及到一些.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及??礢DK獲取設備數據
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插件來接收服務端的數據并解碼播放,這些內容留作下一篇內容。敬請關注!
總結
以上是生活随笔為你收集整理的海康威视频监控设备Web查看系统(二):服务器篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 哪个是最好的降压药(常用降压药列表)
- 下一篇: echarts地图