ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java对接海康人脸识别门禁:从JNA调用到事件上报的完整实践

Java对接海康人脸识别门禁:从JNA调用到事件上报的完整实践 1. 接设备前先想清楚三件容易被忽略的事1.1 确认设备型号和SDK版本别等连不上再翻手册先说一个我自己的教训。前年接一个园区门禁项目客户给了我一台海康的人脸识别终端我拿到手就开始照着网上的demo敲代码结果Login接口一直返回失败折腾了半天才发现设备型号属于新出厂的系列旧版SDK的动态库根本不支持它的登录协议。所以接设备的第一步永远是先确认两件事设备的具体型号包装盒或设备铭牌上都写着比如DS-K1T671系列和SDK版本。海康的设备网络SDK分为Windows版和Linux版分别对应HCNetSDK.dll和libhcnetsdk.so。Java对接没有官方SDK大家都是通过JNA或者JNative调用C接口这就意味着你必须在项目中引入对应平台、对应版本的动态库。同一个版本号下64位和32位的库文件也是分开的千万不要混用。我习惯的做法是把dll和so放到src/main/resources/native目录下启动时按操作系统类型解压到临时目录再加载这样打包成jar后丢到Windows服务器或者Linux服务器上都能自动适配。还有一个细节容易被忽略SDK版本不是越新越好。虽然海康官网能下到最新版但如果你合作的设备固件版本比较老新版SDK反而可能兼容出问题。最稳妥的办法是问设备供应商要他们验证过的SDK包同时把设备固件版本和SDK版本都记录下来写进项目文档里。否则换了台设备排查问题的时候连基线都没有。1.2 记录从哪条链路出来主动上报告诉你是事件不是记录很多刚接触这块的Java开发会有一个思维误区以为获取进出记录就像查数据库一样调一个接口把历史记录拉回来。实际上海康的人脸识别设备在大多数场景下用的是事件上报机制。也就是说当有人经过设备终端刷脸成功或识别失败时设备会把一次进出事件实时推送到你指定的服务端服务端通过回调函数接收这条事件再解析成业务记录落库。这里的核心概念是布防Alarm Setup。设备端支持多种报警事件类型门禁事件、人脸抓拍事件、闯入事件等你需要通过SDK向设备下发布防指令告诉设备我这边要接收哪些事件然后设备才会在事件发生时主动推送。这个推送过程不是靠Java轮询接口模拟的而是设备主动发数据包过来回调函数在其中扮演了服务端接收器的角色。理解了这条链路你就明白为什么项目里必须有一个常驻的服务进程而不是写个定时任务跑一下就结束。设备推数据是持续性的、实时的你必须保证接收服务始终在线。后面我会详细讲怎么处理断线重连这里先把这个概念立住。1.3 端口、网络隔离、时间同步这些准备工作比写代码更重要设备对接说到底是网络通信端口不通一切白搭。海康设备默认使用8000端口作为SDK通信端口部分新设备支持自定义登录前要在设备Web端确认一下实际端口。另外如果设备要上传抓拍图片通常还会用到HTTP端口默认80所以服务端防火墙不能只放行8000HTTP端口也要通。曾经有个现场SDK登录和设备布防都成功了但回调里拿到的图片URL怎么都下载不下来最后排查下来是安全组只开了8000端口HTTP端口没开图片请求全部超时。网络隔离是另一个经常掉链子的地方。很多企业园区把安防设备划分在独立的VLAN里服务端的IP段跟设备不在同一网络。这个时候你需要做路由策略确保服务端既能访问设备的8000端口也能被设备反向推送数据。海康设备的事件上报是设备主动发到你的服务端IP和端口所以你的服务端必须要有一个设备能访问到的内网IP不能躲在NAT后面还不做端口映射。时间同步我放到最后说但它其实是最容易埋雷的一点。设备的时间不准直接导致两个问题一是部分新固件的设备会拒绝跟时间偏差过大的客户端建立通信二是即使通信正常你落库的记录时间戳也是错的后期对账会非常痛苦。接设备第一件事就是去Web端或者通过ISAPI协议把设备时间和服务器时间对齐我后面排查问题的时候也经常会重新校一次时。2. Java侧的项目骨架JNA把C接口翻译成能用的样子2.1 依赖与动态库dll/so必须进对目录Java调用海康SDK没有官方jar包主流的做法是用JNAJava Native Access加载动态库。JNA相比JNI的好处是不用手写一行C代码在Java里声明一个接口继承Library把C函数的签名翻译过来就能直接调用。使用JNA唯一需要留意的就是版本匹配问题我用的是5.x版本功能稳定社区资料也多强烈建议不要用太老的JNA版本否则在Java 8以上环境偶尔会遇到奇怪的兼容问题。Maven坐标很简单dependency groupIdnet.java.dev.jna/groupId artifactIdjna/artifactId version5.13.0/version /dependency动态库文件放哪里是个讲究。如果你只是在本地IDE里调试把HCNetSDK.dll放在项目根目录或者系统的PATH路径下都能加载成功。但是一旦打成jar包发布到服务器这种松散的方式就失效了。我建议把windows-x64和linux-x64两个目录下的库文件作为资源文件打进jar然后在代码里通过一个工具类把它们解压出来再加载public class NativeLibLoader { public static String load() throws IOException { String osName System.getProperty(os.name).toLowerCase(); String libName; if (osName.contains(linux)) { libName libhcnetsdk.so; } else { libName HCNetSDK.dll; } String resourcePath /native/ libName; try (InputStream in NativeLibLoader.class.getResourceAsStream(resourcePath)) { if (in null) { throw new IOException(未找到动态库资源: resourcePath); } File tempDir new File(System.getProperty(java.io.tmpdir), hik-sdk); if (!tempDir.exists()) { tempDir.mkdirs(); } File tempLib new File(tempDir, libName); Files.copy(in, tempLib.toPath(), StandardCopyOption.REPLACE_EXISTING); return tempLib.getAbsolutePath(); } } }加载的时候用绝对路径指向解压出来的文件这样在Java代码里只要一行就能初始化SDK环境。另外提醒一点海康SDK的动态库依赖一批配套的so/dll文件比如crypto库、ssl库在Linux服务器上尤其明显缺一个库就会报UnsatisfiedLinkError所以建议把整个释放包里的库文件都带上不要只拷一个主库。2.2 初始化与连接参数一个demo级别的Init代码加载动态库之后的第一件事是初始化SDK环境。海康SDK的初始化函数是NET_DVR_Init它负责启动内部线程池、日志系统、网络通信等基础组件。我见过不少入门者在初始化前就急着调Login接口结果进程直接崩掉或返回错误码原因就是漏掉了这一步。初始化的同时建议把连接超时时间设置一下HCNetSDK INSTANCE Native.load(HCNetSDK, HCNetSDK.class); INSTANCE.NET_DVR_Init(); INSTANCE.NET_DVR_SetConnectTime(5000, 2);NET_DVR_SetConnectTime的第一个参数是超时时间毫秒第二个是重试次数。这个配置很关键设备偶尔假死或网络瞬断时默认的等待时间可能长达十几秒你的登录线程会被白白挂住。设置成5秒超时、2次重试约等于最多等待10秒就能感知到设备不可达再配合后面的自动重连机制整个服务的鲁棒性会明显好很多。还有一个大家容易忽略的点日志。SDK里NET_DVR_SetLogToFile(3, logDirPath, false)可以把SDK内部的调试日志写到文件里。真遇到疑难杂症比如设备连接不上、回调数据异常打开SDK日志往往能直接定位到原因比瞎猜高效得多。我在测试环境会开日志生产环境会关闭或只保留错误级别的日志避免日志文件膨胀过快。2.3 结构体必须手动声明JNA里最容易出错的字节对齐JNA对接C接口的痛点是结构体要自己在Java里重写。C语言的结构体有内存对齐规则Java侧必须用JNA的Structure类来模拟字段顺序不能乱类型要严格对应。举个例子登录用的NET_DVR_USER_LOGIN_INFO结构体在C语言里注释非常长Java侧声明就是public static class NET_DVR_USER_LOGIN_INFO extends Structure { public byte[] sDeviceAddress new byte[129]; public byte byUseTransport; public int wPort; public byte[] sUserName new byte[64]; public byte[] sPassword new byte[64]; public int cbLoginResult; public Pointer cbLoginResultPtr; public byte[] byRes2 new byte[60]; Override protected ListString getFieldOrder() { return Arrays.asList(sDeviceAddress, byUseTransport, wPort, sUserName, sPassword, cbLoginResult, cbLoginResultPtr, byRes2); } }字段顺序必须跟头文件保持一致getFieldOrder里写的顺序也必须和字段声明顺序完全一致否则JNA在读写内存时字段错位轻则取出来的值不对重则直接导致进程崩溃。很多老手都在这上面翻过车我自己就遇到过sPassword里的字节往后偏移了一位导致密码一直校验失败的情况。还有一点结构体里的byte[]数组长度只要跟C头文件对齐就行关于字符串字段是固定长度字节数组不要用Java的String因为C结构体在内存里就是定长的字节块。这为后面解析事件数据打下了基础。3. 登录、布防、回调整条事件链路的代码拆解3.1 登录接口从NET_DVR_Login_V40的入参说起初始化完成之后下一步就是登录设备。海康SDK提供过多个版本的登录接口老的NET_DVR_Login_V30参数较少新的NET_DVR_Login_V40把登录参数集中到了NET_DVR_USER_LOGIN_INFO结构体里同时返回更详细的设备能力信息NET_DVR_DEVICEINFO_V40我强烈建议直接用V40版本别再用老接口了。填参数的几个细节设备地址字段是字符串可以是IP或域名但建议直接用IP省得DNS解析出幺蛾子。用户名密码默认是admin但很多项目交付后客户会改密码所以接入前一定要跟现场确认最新的账号密码别拿默认密码上去试有些设备连续尝试错误密码会被锁定一段时间。端口字段填设备SDK通信端口默认8000如果客户改过就要以实际为准。登录返回的是用户IDint类型这个ID是无符号32位整数短期内重复使用的可能性极低。如果返回-1即0xFFFFFFFF说明登录失败可以用NET_DVR_GetLastError()获取错误码常见的有23用户名密码错误、7连接超时、17内存不足等。HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo new HCNetSDK.NET_DVR_USER_LOGIN_INFO(); loginInfo.sDeviceAddress toBytes(192.168.1.64, 129); loginInfo.wPort 8000; loginInfo.sUserName toBytes(admin, 64); loginInfo.sPassword toBytes(yourPassword, 64); HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo new HCNetSDK.NET_DVR_DEVICEINFO_V40(); int userId hcNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo); if (userId -1) { int err hcNetSDK.NET_DVR_GetLastError(); throw new RuntimeException(登录失败错误码: err); }登录成功之后设备信息里会携带一些基础能力参数比如设备通道数、是否支持报警等。这些信息在后续布防和通道配置时有用建议打日志保存下来。3.2 布防参数与回调注册时间、事件过滤、图片上传登录不是目的目的是拿到用户ID之后去布防。布防的海康接口是NET_DVR_SetupAlarmChan_V41这个函数会把你的用户ID和布防参数关联起来告诉设备端我要接收哪些报警事件。布防参数存在NET_DVR_SETUPALARM_PARAM结构体里其中有一个字段很关键dwAlarmType 0 表示私有报警类型基本涵盖了门禁事件、人脸抓拍事件等还有byLevel布防优先级等字段一般情况下用默认值就行。有一点需要特别留意如果设备抬头上报的是人脸抓拍事件而你在布防的时候没有把对应的报警类型打开回调函数照样收不到数据。回调注册的接口是NET_DVR_SetDVRMessageCallBack_V50它接收一个函数指针C语言里的回调函数在Java里就是定义一个接口public interface MSGCallBack extends StdCallLibrary.StdCallCallback { boolean invoke(int lCommand, HCNetSDK.NET_DVR_ALARMER pAlarmer, Pointer pAlarmInfo, int dwBufLen, Pointer pUser); }这个回调接口是整个对接流程的核心。一旦布防成功设备端有事件发生SDK就会在内部线程上调用这个回调函数。你可以在回调里把pAlarmInfo指针强制转换成对应的结构体从而解析数据。必须强调的是回调函数运行在SDK内部线程上你千万不要在回调里做耗时的业务逻辑比如写数据库、调远程接口否则轻则丢事件重则拖垮整个SDK的消息循环。正确的做法是回调里只做数据拷贝把指针里的数据转成Java对象后丢到阻塞队列或线程池里由业务线程异步处理。3.3 回调代码最小可运行版本一个完整的Java示例我这边给一个简化但能跑通的示例把登录、布防、回调串起来public class HikFaceGateway { private static final BlockingQueueFaceEvent EVENT_QUEUE new LinkedBlockingQueue(10000); private HCNetSDK hcNetSDK; private int userId; private int alarmHandle; public void start() { hcNetSDK Native.load(HCNetSDK, HCNetSDK.class); hcNetSDK.NET_DVR_Init(); hcNetSDK.NET_DVR_SetConnectTime(5000, 2); // 注册回调 HCNetSDK.MSGCallBack callback (lCommand, pAlarmer, pAlarmInfo, dwBufLen, pUser) - { if (lCommand HCNetSDK.COMM_ALARM_ACS) { // 门禁事件 HCNetSDK.NET_DVR_ACS_EVENT_INFO eventInfo new HCNetSDK.NET_DVR_ACS_EVENT_INFO(pAlarmInfo); eventInfo.read(); FaceEvent faceEvent new FaceEvent(); faceEvent.setEventType(eventInfo.dwMajor); faceEvent.setDirection(eventInfo.byIoIn); faceEvent.setPersonId(readString(eventInfo.struUserInfo.byCardNo)); // 拷贝图片URL等信息... EVENT_QUEUE.offer(faceEvent); } return true; }; hcNetSDK.NET_DVR_SetDVRMessageCallBack_V50(callback, new Pointer(0)); // 登录 login(); // 布防 HCNetSDK.NET_DVR_SETUPALARM_PARAM alarmParam new HCNetSDK.NET_DVR_SETUPALARM_PARAM(); alarmParam.dwSize alarmParam.size(); alarmHandle hcNetSDK.NET_DVR_SetupAlarmChan_V41(userId, alarmParam); if (alarmHandle -1) { throw new RuntimeException(布防失败错误码: hcNetSDK.NET_DVR_GetLastError()); } // 业务线程消费队列 ExecutorService executor Executors.newSingleThreadExecutor(); executor.submit(this::consumeEvents); } }这段代码的运行逻辑是设备在有人刷脸或刷卡时触发门禁事件SDK调用回调函数回调把数据转成业务对象放进队列业务线程从队列取数据落库。这套回调队列消费线程的模型几乎是我所有对接海康设备项目的标准模板既保证了实时性也避免了阻塞SDK内部线程。4. 进出记录解析拿到原始结构体之后的关键处理4.1 判断事件类型与进出方向人脸识别设备的进出记录在SDK回调里最常遇到的命令字是COMM_ALARM_ACS门禁事件不同类型的事件靠结构体里的dwMajor主类型和dwMinor次类型来细分。主类型是MAJOR_ALARM_ACS_EVENT门禁事件大类次类型下的MINOR_ACS_CARD_EVENT、MINOR_ACS_FACE_EVENT、MINOR_ACS_FINGERPRINT_EVENT分别表示刷卡、人脸、指纹。进出方向是业务上最关心的字段之一。在海康的门禁事件结构体NET_DVR_ACS_EVENT_INFO里有一个byIoIn字段0表示进门1表示出门。不同型号的设备可能字段含义有细微差异我遇到过一个设备厂商固件把进出门方向放在dwCurrentNo门号字段的高位里折腾了一个下午才从抓包数据里分析出来。所以拿到设备后建议先在Web端看一次真实事件核对一下SDK解析出来的字段值跟实际动作是否一致再开始写正式业务逻辑。人脸和卡事件还有一个需要注意的点比对结果。门禁事件不仅包含成功放行的记录也有识别失败的记录。在海康的结构体里byCompareStatus字段用来标记比对结果0表示成功非0表示失败。很多同学做数据统计时把失败事件也直接入库导致报表里进出人数虚高最后跟考勤系统对不上账非常尴尬。4.2 人员ID、卡号、比对分数怎么取记录里的人员信息在NET_DVR_ACS_EVENT_INFO的struUserInfo子结构里包含卡号、工号等字段。海康人脸设备的卡号默认是在用户信息里绑定的唯一编号通常对应到企业内部ID体系。解析的时候注意字节序和编码byCardNo一般是ASCII字符串直接用new String截掉结尾的\0即可。有些设备的工号存储是Unicode格式这时候直接用ASCII解析会得到乱码需要先判断结构体里的byCardType类型再决定用哪种编码。比对分数也是一个有价值的字段它可以帮助判断识别质量。如果某个闸机口的比对分数长期低于阈值说明这台设备可能需要重新调整摄像头角度或者补光。我在项目里会把这个字段存下来运维同学可以通过图表直观地看到设备健康度。图片信息同样重要。门禁事件结构体里通常带一个NET_DVR_JPEGPICTURE_INFO子结构里面是抓拍图片的URL。这里的URL是设备上的相对路径比如/ISAPI/Streaming/Media/...需要拼接设备的HTTP地址才能下载。很多新人不知道这一点把相对路径当成完整URL去请求肯定404。4.3 图片URL、中文乱码和字节数组拷贝解析结构体时最折磨人的是字符串编码问题。C结构体内部的字节数组长度固定比如卡号可能占32字节实际内容只有12字节剩下的全是\0。Java侧读取时如果直接对字节数组做trim遇到中文名比如工号里包含中文就会乱码。海康的设备中文编码通常是GBK不是UTF-8。所以从结构体里取出来的字节数组需要先去掉末尾的\0再用GBK转字符串private static String readChinese(byte[] raw) { int len 0; while (len raw.length raw[len] ! 0) { len; } return new String(raw, 0, len, Charset.forName(GBK)); }图片URL则直接按ASCII处理因为URL里不会有中文。但要注意海康上报的图片URL有一部分设备在上报时需要你额外控制一个开关在布防参数里把byUploadPicture或者byPicUploadType设置为对应模式否则事件结构体里的图片URL会是空字符串或者只给一个缩略图。具体哪个字段控制图片上传跟设备固件有关我在不同项目里见过两种完全不同的配置方式所以拿到新设备先试一遍布防参数用真实事件验证图片是否能正常获取。字节数组大块拷贝的场景也有讲究。你从pAlarmInfo指针构造结构体之后必须调用eventInfo.read()方法把数据从内存读取到Java对象里否则所有字段都是初始值这是JNA新手最常踩的坑。我做代码评审的时候几乎每次都能看到有人忘了这一行。5. 连上了设备却不推数据排查链路完整复盘5.1 先看设备时间再谈其它对齐问题这是我做对接排障的标准第一步。设备时间偏差过大会导致设备拒绝建立通信或者虽然通信正常但不主动上报事件。排查方法很简单设备Web端打开系统时间页面跟服务器时间对一下偏差超过1分钟就建议校时。海康设备支持通过ISAPI协议校时一条简单的PUT请求就能解决PUT /ISAPI/System/time HTTP/1.1 Host: deviceIpBody里带上?xml version1.0?TimetimeModemanual/timeModelocalTime2025-01-01T12:00:0008:00/localTime/Time设备就会把时间设置成指定值。我在项目启动时和每天凌晨各执行一次校时任务确保设备时间始终与服务器保持同步。5.2 布防句柄和回调注册的成功标志区分回调没触发这个问题的排查链路很多人一开始就会陷入代码逻辑里但实际上先确认两个返回值就可以排除大半问题。第一是布防接口的返回值NET_DVR_SetupAlarmChan_V41返回的句柄必须大于等于0如果返回-1说明布防指令被设备拒绝了去查设备日志或者抓包看看是不是用户权限不足或者协议版本不匹配。第二是回调注册的返回值NET_DVR_SetDVRMessageCallBack_V50返回booleanfalse说明注册失败常见原因是回调函数指针不合法。这里有一个JNA的坑回调接口必须定义为StdCallLibrary.StdCallCallback的子接口而且回调对象不能是局部变量必须在类里持有强引用否则可能会被GC回收导致调用时直接崩溃。我见过有人在方法里new了个回调传给SDK方法结束了回调对象被回收设备一推数据进程就core dump。如果这两个返回值都正常但收不到事件问题大概率出在事件类型上。5.3 事件类型没对上回调写了等于白写海康SDK命令字里门禁事件一般用COMM_ALARM_ACS0x5002人脸抓拍事件可能是COMM_ALARM_FACE_DETECT不同设备上报的事件命令字也不统一。如果你的回调里只处理了COMM_ALARM_ACS但这台设备实际推送的是COMM_ALARM_FACE_DETECT那回调入口进得去但走不到解析分支看起来就像设备没推送。最快的定位方法是回调入口直接打日志System.out.println(回调命令字: 0x Integer.toHexString(lCommand));如果日志里能看到命令字在刷说明设备推送是通的只是你没有处理对应类型。如果日志完全没有任何输出说明推送链路本身有问题应该回到5.1和5.2去查。另外提醒一下有些设备的事件上报是有开关的必须去设备Web端的事件管理或报警服务页面手动开启对应的事件类型。SDK布防只是让通道可用但通道里传什么内容设备端配置也在起作用。这个双端都要开的机制坑过不少人。5.4 实在收不到就兜底ISAPI主动查询进出记录前面说过事件上报是常态做法但有一部分老设备或特定场景下事件上报可能不稳定。这时候不要硬磕SDK回调换一个思路——用设备的ISAPI接口主动查询历史进出记录。海康门禁设备一般提供/ISAPI/AccessControl/AcsEvent接口POST一个JSON或XML查询条件就能返回一段时间内的进出事件。用Java原生的HttpClient就能直接调不需要经过SDK代码写起来反而更直接String url http:// deviceIp /ISAPI/AccessControl/AcsEvent?formatjson; String body {\AcsEvent\:{\major\:0,\minor\:0,\time\:{\startTime\:\2025-01-01T00:00:0008:00\,\endTime\:\2025-01-01T23:59:5908:00\},\maxResults\:30,\searchID\:\12345678\}}; HttpResponseString response client.send( HttpRequest.newBuilder(URI.create(url)) .header(Content-Type, application/json) .header(Authorization, basicAuth(user, password)) .POST(BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString() );ISAPI方式尤其适合做补数据场景某天事件推送链路异常导致丢了几小时数据用主动查询把缺失区间扫一遍就补回来了。我自己做过的项目里最终方案都是事件上报实时处理 ISAPI定时对账补漏双链路互为兜底数据完整率才算真正有保障。需要注意的是ISAPI接口的查询结果分页逻辑比较简单maxResults上限通常几十条需要翻页拉取全部数据同时该接口在设备并发压力大的时候会响应变慢对账任务建议避开白天高峰时段。6. 上线之前我总会再检查一遍的隐藏雷点6.1 网络闪断后的自动重连事件上报服务是7x24小时跑的网络不可能永远稳定。如果设备和服务端之间的网络闪断几分钟SDK内部消息连接会断开但进程不会崩溃只是回调静默失效。最坑的是你没感知等发现的时候已经丢了大半天数据。所以重连机制必须在一开始就写好。我的做法是单独起一个守护线程每隔30秒检查一次SDK的登录状态和设备句柄是否有效。海康SDK没有提供轻量的ping接口我用的是尝试调用某个底层函数看是否报错或者直接检查NET_DVR_GetLastError另一种更稳妥的方式是跟设备协商一个心跳机制每30秒调用一次NET_DVR_GetDVRWorkState获取设备工作状态返回正常说明链路健在如果调用失败就主动执行NET_DVR_LogoutNET_DVR_Login_V40NET_DVR_SetupAlarmChan_V41的重建流程。有一点必须提醒重连之后布防句柄会变原来的句柄已经无效了必须重新调用布防接口拿新的句柄。这个重建流程要保证线程安全避免重连期间回调还在旧句柄上触发的并发问题。6.2 回调线程里千万别做重活这句话我前面提过一次但因为它太重要值得专门再说透。SDK一旦启动回调回调函数的执行线程是SDK内部共享的。如果你在回调里执行数据库写入、HTTP调用、文件上传任何一个操作慢了几百毫秒SDK处理后续事件的线程就会被卡住产生两个后果一是事件堆积导致内存上涨二是设备端检测到接收方处理不过来会自动降低推送速度甚至断开发送链路。我这里遇到过一个真实案例项目初期回调里直接写了MySQL批量插入某天数据量突然增大数据库连接池被打满回调线程全部阻塞在拿连接上SDK整个消息循环停摆进程表现看起来就像死锁。后来把所有IO操作全部挪到队列异步处理问题立刻消失CPU占用还降下来了。所以回调函数里做的事越少越好把指针里的数据拷贝成Java对象、塞进内存队列、立即返回。业务处理全部交给其他线程池做。6.3 句柄、内存、进程退出这些资源不回收迟早出事JNA操作C库资源管理是容易被忽视的重灾区。登录返回的userId、布防返回的alarmHandle都是底层句柄程序退出前必须显式释放否则设备端的会话连接不会自动断开累积多了会超出设备的最大连接数导致新设备登录失败。释放顺序也有讲究先撤防NET_DVR_CloseAlarmChan_V30再退出登录NET_DVR_Logout最后调用NET_DVR_Cleanup结束SDK整体环境。顺序反了虽然大多数时候看不出问题但在某些固件版本上会导致SDK内部资源泄漏服务长时间运行后内存持续上涨。结构体本身的内存管理也需要留意。JNA的Structure对象在Java堆上管理JVM会自动回收但如果手动new Pointer出来的内存用完要记得Pointer.release()或者等GC处理。我在解析图片数据时用到了一个手动分配的Pointer缓冲区因为忘了释放跑了两周后内存涨了将近一个G排查半天才找到原因。另外进程退出时如果SDK线程还没结束有可能导致进程无法正常退出。我在服务的shutdownHook里做了完整的清理流程保证应用在滚动发布时能优雅退出不会出现端口未释放导致新进程起不来的情况。最后再分享一个个人习惯把设备相关的配置项IP、端口、用户名、密码、布防参数、校时开关全部外置到配置文件里而不是硬编码在代码中。一方面方便不同环境切换另一方面设备故障换新设备时只需要改配置重启服务不需要重新编译部署。这套对接方案我迭代了多个版本现在回头看最值钱的往往不是那些API调用的正确写法而是一开始就想清楚链路设计、异常场景和资源管理剩下的只是按部就班填代码罢了。
返回列表