声明:本系列文章只提供交流与学习使用。文章中所有涉及到海康威视设备的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 }
这里有个细节问题要说明一下,当服务端每次注册到设备时,设备第一次返回的数据里面的前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插件来接收服务端的数据并解码播放,这些内容留作下一篇内容。敬请关注!