简介:面向需要集成海康威视报警能力的C#开发者,这份示例Demo演示了如何基于海康SDK完成设备登录、报警布防、事件订阅与消息处理,适合安防项目二次开发参考。压缩包共51个文件、约12.55MB,其中包含核心C#源码文件(如CHCNetSDK.cs、AlarmDemo.cs),以及运行所需的dll动态库和工程配置文件,并附有说明文档,便于快速理解项目结构并直接编译调试。包内示例工程已实现报警监听主流程,开发者可重点参考事件委托注册、报警回调解析以及异常处理逻辑,在此基础上扩展移动侦测、入侵报警等自定义业务。项目适配Visual Studio环境,代码注释与文件组织清晰,已有1326人学习下载,适合具备一定C#基础、希望缩短海康SDK对接周期的读者参考学习。
1. 报警布防监听demo到底在解决什么问题
海康威视NVR或DVR安装好后,报警事件通常只在设备本地屏幕上闪一下,或者要登录web管理页才能看到。业务系统要接住这些报警,最常见的手段就是让设备把事件主动上报。C#编写的海康威视设备报警布防监听demo,走的是“登录设备、下发布防、接收回调”这条链路:布防相当于打开设备的上报开关,监听则是守在回调线程里把报警数据接住并转交给业务队列。这个demo真正有价值的不只是“能收到报警”,还包括布防参数怎么传、回调线程怎么回UI、布防之后怎样验证它没有静默丢事件。接手过WinForms或WPF上位机的工程师,照着这套思路可以在一小时内把最小可跑版本拼出来,也能在后续排错时知道问题大概出在设备端还是SDK封装端。
2. 报警布防监听之前的SDK基座:C#侧DllImport与结构体
2.1 按运行位数固定海康威视SDK文件
写demo时遇到最多的启动错误不是编译错,而是“试图加载格式不正确的DLL”。海康威视网络SDK以C++头文件和动态库形式分发,C#通过P/Invoke按进程位数加载,动态库位数必须和主程序进程位数一致。项目里做的是纯报警布防监听场景,只依赖HCNetSDK.dll;预览、回放、语音对讲才会牵扯HCPreview.dll、AudioRender.dll这些播放相关依赖。我习惯在仓库根目录把SDK放成两份,便于后续扩展:
demo/ ├── lib/ │ ├── x64/ │ │ ├── HCNetSDK.dll │ │ ├── HCPreview.dll │ │ └── AudioRender.dll │ └── x86/ │ ├── HCNetSDK.dll │ ├── HCPreview.dll │ └── AudioRender.dll ├── HikAlarmDemo/ │ └── HikAlarmDemo.csproj └── README.md如果确认只做报警布防监听,可以只在x64目录放HCNetSDK.dll,大大降低环境噪音。CS文件里配合CopyToOutputDirectory把dll带到输出目录,同时把PlatformTarget指定为x64:
<PropertyGroup> <PlatformTarget>x64</PlatformTarget> </PropertyGroup> <ItemGroup> <None Include="..\lib\x64\HCNetSDK.dll" Link="HCNetSDK.dll" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>这段配置的作用是告诉.NET项目生成时使用x64平台,并把HCNetSDK.dll原样复制到运行目录。很多历史遗留项目还跑在x86上,那lib/x86这份不能省;但有一点要注意,报警模块在较新SDK版本里对x64支持更完整,x86下个别命令结构体大小校验会卡得比较死,能不切x86就不切。
2.2 报警布防监听要用的API入口
把整个监听流程拆开看,核心就八个函数,先列出分工,后续代码都基于这组入口。
| API | C#入口 | 在报警布防监听中的角色 |
|---|---|---|
| NET_DVR_Init | NET_DVR_Init() | 全局初始化,所有API调用前必须执行一次 |
| NET_DVR_SetConnectTime | NET_DVR_SetConnectTime(uint, uint) | 设置连接等待时间和重试次数 |
| NET_DVR_SetReconnect | NET_DVR_SetReconnect(int, int) | 设备断线后自动重连,长挂监听建议开启 |
| NET_DVR_Login_V40 | NET_DVR_Login_V40(ref, ref) | 登录并取得用户句柄 |
| NET_DVR_SetupAlarmChan_V41 | NET_DVR_SetupAlarmChan_V41(int, ref, delegate, IntPtr) | 布防,返回报警监听句柄 |
| NET_DVR_CloseAlarmChan_V41 | NET_DVR_CloseAlarmChan_V41(int) | 撤防并释放报警监听通道 |
| NET_DVR_Logout | NET_DVR_Logout(int) | 注销登录 |
| NET_DVR_Cleanup | NET_DVR_Cleanup() | 全局清理,进程退出前调用 |
布防返回的句柄和登录句柄不是同一个概念。登录句柄代表这个登录会话,撤防时传布防句柄,注销时传用户句柄。报警监听的生命周期以布防句柄为准,这句对应关系弄混,后面会出现撤防无效或者注销后回调还在泄的现象。
2.3 DllImport声明与三个关键结构体的C#写法
C#侧没有现成的SDK包,一切入口靠DllImport声明。委托回调参数我一般写成IntPtr而不是直接ref结构体,原因是海康报警信息结构体存在版本切换,IntPtr配合Marshal.PtrToStructure会更灵活,后面解析报警数据时可以直接躲开结构体版本错位的坑。
public delegate void MSGCallBack(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser); [DllImport("HCNetSDK.dll")] public static extern bool NET_DVR_Init(); [DllImport("HCNetSDK.dll")] public static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes); [DllImport("HCNetSDK.dll")] public static extern bool NET_DVR_SetReconnect(int dwInterval, int dwEnable); [DllImport("HCNetSDK.dll")] public static extern int NET_DVR_Login_V40(ref NET_DVR_LOGIN_INFO pLoginInfo, ref NET_DVR_DEVICEINFO_V40 lpDeviceInfo); [DllImport("HCNetSDK.dll")] public static extern int NET_DVR_SetupAlarmChan_V41(int lUserID, ref NET_DVR_SETUPALARM_PARAM lpSetupParam, MSGCallBack cbAlarm, IntPtr pUser); [DllImport("HCNetSDK.dll")] public static extern bool NET_DVR_CloseAlarmChan_V41(int lAlarmHandle); [DllImport("HCNetSDK.dll")] public static extern bool NET_DVR_Logout(int lUserID); [DllImport("HCNetSDK.dll")] public static extern void NET_DVR_Cleanup(); [DllImport("HCNetSDK.dll")] public static extern uint NET_DVR_GetLastError();这段声明的重点是函数返回类型。早期SDK有些接口返回BOOL,有些返回LONG句柄,C#里必须严格对应;登录和布防在头文件里返回句柄值,所以用int,布尔型返回值接口用bool即可。
接下来是登录信息结构体,这里只保留最常用的字段,完整定义建议以手里的SDK版本头文件为准:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct NET_DVR_LOGIN_INFO { public int dwSize; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] public string sDeviceAddress; public byte byUseTransport; public ushort wPort; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string sUserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string sPassword; public int cbLoginResult; public IntPtr pUser; public int bPasswordWrong; }sDeviceAddress按129字节定长声明,这和SDK头文件里char[129]保持一致。wPort是ushort,不能写成int,否则结构体字节偏移直接错位。另一个常用结构体是设备信息NET_DVR_DEVICEINFO_V40,它主要用来确认通道数和设备类型,demo里只需要在登录时传入,字段可以先声明到byStartChan,确认基本通道即可。
布防参数结构体NET_DVR_SETUPALARM_PARAM是监听配置的核心,它决定回调里拿到的是哪种报警数据类型。demo先按最简字段声明:
[StructLayout(LayoutKind.Sequential)] public struct NET_DVR_SETUPALARM_PARAM { public int dwSize; public byte byLevel; public byte byAlarmInfoType; public byte byRetAlarmTypeV40; public byte byRetDevInfoVersion; public byte byRetVQDAlarmType; public byte byFaceAlarmDetection; public byte bySupport; public byte byBrokenNetHttp; }2.4 布防参数里真正影响监听行为的三个字段
第一个是byLevel,表示布防级别,传1代表一级布防,日常实时监听用1就够。第二个是byAlarmInfoType,它决定报警信息结构体是哪一代:0表示NET_DVR_ALARMINFO经典结构体,1表示NET_DVR_ALARMINFO_V40扩展结构体。demo阶段为了少踩结构体长度坑,先设0,后面第4章解析数据时会说明差异。第三个是byRetAlarmTypeV40,置1后SDK按V40规则组织返回字段。很多网上的代码直接照抄这段参数,但没解释这三个字段,等到设备报警类型是智能分析事件时才发现回调信息取不全是正常的。
3. 报警布防监听主线:登录、布防、回调转发
3.1 登录设备并拿到布防需要的用户句柄
登录用NET_DVR_Login_V40,它的输入参数是登录信息和设备信息两个ref结构体,输出是用户ID。写一个最小登录方法:
public int Login(string ip, ushort port, string userName, string password) { var loginInfo = new NET_DVR_LOGIN_INFO { dwSize = Marshal.SizeOf<NET_DVR_LOGIN_INFO>(), sDeviceAddress = ip, wPort = port, sUserName = userName, sPassword = password }; var deviceInfo = new NET_DVR_DEVICEINFO_V40(); int userId = NET_DVR_Login_V40(ref loginInfo, ref deviceInfo); if (userId < 0) { uint error = NET_DVR_GetLastError(); throw new Exception($"登录失败,错误码: {error}"); } return userId; }登录前必须填dwSize,SDK通过字段长度判断传入的是哪一代结构体。port参数注意用ushort,网络端口本身65535封顶。登录失败时userId为负值,具体原因通过NET_DVR_GetLastError拿。这里是第一道常见坎:用户名带中文或者密码带特殊字符时,如果DllImport声明里没标CharSet.Ansi,字符串编码错位会直接报用户密码错误。
提示:SDK调用顺序很重要。NET_DVR_Init必须在登录前执行,程序退出前执行NET_DVR_Cleanup。顺序错乱时常见现象是登录偶发失败,错误码不固定。
3.2 布防并在C#侧保住回调委托
布防方法入参是登录返回的userId,真正的布防动作在NET_DVR_SetupAlarmChan_V41。它要求传入布防参数、回调委托、用户参数指针:
private MSGCallBack _alarmCallback; private int _alarmHandle; public void Arm(int userId) { var alarmParam = new NET_DVR_SETUPALARM_PARAM { dwSize = Marshal.SizeOf<NET_DVR_SETUPALARM_PARAM>(), byLevel = 1, byAlarmInfoType = 0, byRetAlarmTypeV40 = 1 }; _alarmCallback = OnAlarmMessage; _alarmHandle = NET_DVR_SetupAlarmChan_V41( userId, ref alarmParam, _alarmCallback, IntPtr.Zero); if (_alarmHandle < 0) { uint error = NET_DVR_GetLastError(); throw new Exception($"布防失败,错误码: {error}"); } }这里最关键的是把委托赋给类字段_alarmCallback。SDK拿到的是托管方法指针,如果委托是方法内的局部变量,方法退出后委托会被GC回收,结果是布防返回成功但回调一次都不进。这个问题在报警布防监听demo里出现频率非常高,表现形式就是“设备触发了,程序里完全没反应”。
布防参数中把byAlarmInfoType设0是为了让demo阶段只解析NET_DVR_ALARMINFO。布防返回的_alarmHandle是撤防入口,它和userId并存,撤防时要传它。
3.3 回调线程到UI线程的转发
海康SDK回调跑在自身消息线程里,回调方法里直接更新WPF控件会抛线程间操作异常,WinForms则表现为控件闪烁或无缘无故卡死。常见做法是用ConcurrentQueue先缓冲,再由UI定时器周期取数据。这也是C#上位机开发里经常遇到的“循环数据采集和UI刷新卡顿”问题的通用解法。
private ConcurrentQueue<string> _eventQueue = new ConcurrentQueue<string>();回调侧只入队不处理:
private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser) { string text = $"命令:0x{lCommand:X8}, 当前线程:{Environment.CurrentManagedThreadId}"; _eventQueue.Enqueue(text); }UI侧用DispatcherTimer每200毫秒批量消费:
_logTimer = new DispatcherTimer(); _logTimer.Interval = TimeSpan.FromMilliseconds(200); _logTimer.Tick += (s, e) => { while (_eventQueue.TryDequeue(out string message)) LogPanel.AppendText(message + Environment.NewLine); }; _logTimer.Start();这里把耗时操作都留在UI线程之外,回调线程每进一次消息只做一次Enqueue,即使报警事件密集,UI也不会被拖垮。200毫秒轮询间隔对报警展示足够,如果要做数据曲线,建议间隔缩短到100毫秒并配合BeginInvoke批量更新。
4. 报警数据解包与布防失败排错
4.1 用回调命令码区分报警类型
很多人第一次写回调时以为lCommand直接就是报警类型,实际它是命令码,叫法不同。lCommand决定报警数据结构体怎么解释,报警信息里还有byAlarmType这类细分字段。常见命令常量在SDK头文件里静态定义,demo中先集中处理三类:
| lCommand常量 | 值 | 报警数据支线 |
|---|---|---|
| COMM_ALARM_V30 | 0x1003 | 移动侦测、视频遮挡、硬盘异常等通用报警 |
| COMM_ALARM_RULE | 0x1102 | 智能分析规则类报警 |
| COMM_UPLOAD_ALARM | 0x8060 | 报警主机或扩展模块上传上来的报警 |
回调解析代码的骨架先搭起来:
private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser) { switch (lCommand) { case 0x1003: var alarmInfo = Marshal.PtrToStructure<NET_DVR_ALARMINFO>(pAlarmInfo); _eventQueue.Enqueue( $"移动侦测, 通道号:{alarmInfo.dwChannelNo}, " + $"报警类型:{alarmInfo.byAlarmType}"); break; case 0x1102: // 规则报警结构体字段更多,按SDK头文件完整声明后再解析 break; case 0x8060: // 报警主机上传数据,解析逻辑有时要按协议单独拆 break; } }NET_DVR_ALARMINFO里的dwChannelNo是报警触发通道号,byAlarmType是细分报警类型。把这两项取出来,再叠加设备的IP地址,就能知道是哪一路报警。移动侦测、视频遮挡和视频丢失都归在0x1003命令码下,靠byAlarmType区分。
4.2 用NET_DVR_GetLastError快速定位布防失败
布防失败不像网络连接失败那么容易被发现,很多情况是布防接口返回负值但界面看不出原因。排错统一走错误码,常见几个错误码在SDK各版本里基本保持一致:
| 错误码 | 错误宏 | 布防监听场景里的可能原因 |
|---|---|---|
| 7 | NET_DVR_NETWORK_FAIL_CONNECT | 设备IP不通或端口被防火墙拦截 |
| 9 | NET_DVR_NETWORK_SEND_ERROR | 认证请求发出失败,跨网段路由异常 |
| 11 | NET_DVR_USERERROR | 用户名或密码不对 |
| 17 | NET_DVR_PARAMETER_ERROR | 传入的结构体dwSize不正确 |
| 19 | NET_DVR_ALLOC_RESOURCE_ERROR | 设备端资源不足,常见于多客户端并发布防 |
| 23 | NET_DVR_ORDER_ERROR | 调用顺序错,NET_DVR_Init没执行或已Cleanup |
错误码转可读文本,可以封装一个通用方法,后续界面提示直接用:
public static string DescribeError(uint error) { switch (error) { case 7: return "网络连接失败,请确认IP和端口"; case 9: return "网络发送失败,检查路由或防火墙"; case 11: return "用户名或密码错误"; case 17: return "参数错误,结构体大小或字段不对"; case 19: return "资源分配失败,设备连接数可能已满"; case 23: return "调用顺序错误,初始化未完成"; default: return $"错误码:{error}"; } }第17号错误码在C#里出现最多。结构体声明多了或少了字段,会让dwSize和实际内存长度对不上;一旦deviceInfo结构体长度小于SDK预期,登录接口直接就拒绝。
4.3 排除“布防成功但收不到报警”的排查顺序
回调迟迟不触发时,先从设备端检查,再回到代码端。
第一步看设备web端的报警配置。部分设备默认只启用本机报警输出,没有勾选“联动报警上传”或“网络报警上报”。布防只是建立监听通道,设备侧不配置上传,事件不会发给SDK。第二步确认账号权限。海康设备的子用户权限列表里有一个“远程布防/撤防”,如果用户只勾了预览权限,布防返回成功后报警回调也进不来。第三步看同网段内是不是已经有一个客户端在撒防。某些老设备只允许单个会话布防,另一个客户端登录后把布防撤掉,本程序报警就断了。第四步检查程序里是否保存了委托实例,委托丢失一直是最隐蔽的静默故障。
提示:回调线程里不要再调用任何SDK接口,尤其不要调用NET_DVR_GetLastError。SDK内部是共享状态,回调线程读取会导致主线程登录或布防卡死。
5. 多设备同时布防的句柄管理与上线前验证
5.1 多设备布防怎么识别报警来自哪台设备
回调函数签名里没有userId,多设备同时布防时不能靠接口参数判断来源。常见做法是在SetupAlarmChan_V41的pUser参数里传入一个设备上下文指针,回调中从IntPtr还原出设备对象。C#里用GCHandle完成这个绑定:
public class DeviceContext { public string DeviceNo; public int UserId; public int AlarmHandle; public IntPtr UserParam; } private Dictionary<string, DeviceContext> _devices = new Dictionary<string, DeviceContext>(); public int ArmDevice(DeviceContext ctx, ref NET_DVR_SETUPALARM_PARAM param) { GCHandle gch = GCHandle.Alloc(ctx); ctx.UserParam = GCHandle.ToIntPtr(gch); ctx.AlarmHandle = NET_DVR_SetupAlarmChan_V41( ctx.UserId, ref param, _alarmCallback, ctx.UserParam); _devices[ctx.DeviceNo] = ctx; return ctx.AlarmHandle; }回调里用pUser取回原始对象:
private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser) { if (pUser == IntPtr.Zero) return; DeviceContext ctx = GCHandle.FromIntPtr(pUser).Target as DeviceContext; var alarmInfo = Marshal.PtrToStructure<NET_DVR_ALARMINFO>(pAlarmInfo); string line = $"{ctx?.DeviceNo} 通道:{alarmInfo.dwChannelNo} " + $"类型:{alarmInfo.byAlarmType}"; _eventQueue.Enqueue(line); }GCHandle保证了上下文对象在程序退出前不会被GC回收。撤防时再释放这个句柄,顺序是先撤防后释放GCHandle,不然回调里可能拿到已释放的Target再报空引用。
5.2 上线前验证监听链路是否真的在干活
布防返回成功只能证明命令被设备接受,不代表报警链路全通。连线调试时,先在设备侧手动触发一次报警输入,再看回调日志里是否出现对应事件。注意报警类型值与设备主机的报警输入端口对应关系,不同设备可能用不同byAlarmType值。
长时间挂机验证时给日志加时间戳,持续运行10分钟以上,对比人工触发次数和回调日志条数。一旦有丢事件,优先怀疑回调线程被耗时操作卡住。程序关闭时按“撤防、注销、清理”的顺序退出:
foreach (var dev in _devices.Values) { if (dev.AlarmHandle >= 0) NET_DVR_CloseAlarmChan_V41(dev.AlarmHandle); if (dev.UserId >= 0) NET_DVR_Logout(dev.UserId); if (dev.UserParam != IntPtr.Zero) GCHandle.FromIntPtr(dev.UserParam).Free(); } NET_DVR_Cleanup();如果在客户现场发现程序退出后设备端仍显示“在线”,多半是撤防或注销没有执行,导致会话留在设备端占着布防名额。按这段顺序关闭后,再打开设备web端确认在线列表为空,报警布防监听demo的链路才算真正收口。
本文还有配套的精品资源,点击获取