• SpringBoot集成海康网络设备SDK


    SDK介绍

    概述

    设备网络SDK是基于设备私有网络通信协议开发的,为嵌入式网络硬盘录像机、NVR、网络摄像机、网络球机、视频服务器、解码器、报警主机、网络存储等产品服务的配套模块,用于远程访问和控制设备软件的二次开发。

    功能

    图像预览, 文件回放和下载, 云台控制, 布防/撤防, 语音对讲, 日志管理, 解码卡, 远程升级, 远程重启/关闭, 格式化硬盘, 参数配置(系统配置, 通道配置, 串口配置, 报警配置, 用户配置), 多路解码器, 智能设备功能和获取设备能力集等。

    下载

    https://open.hikvision.com/download/5cda567cf47ae80dd41a54b3?type=10

    对接指南

    以java为例

    由于我司提供的设备网络SDK是封装的动态链接库(Windows的dll或者Linux的so),各种开发语言对接SDK,都是通过加载动态库链接,调用动态库中的接口实现功能模块对接,因此,设备网络SDK的对接不区分开发语言,而且对接的流程和对应的接口都是通用的,各种语言调用动态库的方式有所不同。本文重点介绍java开发语言如何对接设备网络SDK。目前我司提供的java语言开发的demo是通过JNA的方式调用动态链接库中的接口,JNA(Java Native Access)框架是SUN公司主导开发的开源java框架,是建立在JNI的基础上的一个框架,JNA框架提供了一组java工具类用于在运行期间动态访问动态链接库(native library:如Window的dll、Linux的so),实现在java语言中调用C/C++语言封装的接口,java开发人员只需要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射,而不需要编写任何Native/JNI代码,大大降低了Java调用动态链接库的开发难度。相比于JNI的方式,JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。JNA调用C/C++的过程大致如下:

    集成

    SpringBoot 项目为例,海康SDK版本为6.1.9.47,JNA版本为3.0.9,在windows环境使用Intellij IDEA 2022.2.3开发

    初始化项目

    • 新建 SpringBoot 项目,版本 2.5.3
    • 添加pom依赖:jna,fastjson2
    
    <dependency>
            <groupId>com.sun.jnagroupId>
            <artifactId>jnaartifactId>
            <version>3.0.9version>
    dependency>
    
    
    <dependency>
            <groupId>com.alibaba.fastjson2groupId>
            <artifactId>fastjson2artifactId>
            <version>2.0.20version>
    dependency>          
    
    
    
    • 将下载下来的海康sdk放到项目目录,并根据操作系统不同分别创建相应的文件夹
    • 若出现 HCNetSDK.NET_DVR_PREVIEWINFO.HWND找不到引用,可做如下处理:1:在ProjectStructure中引入官方示例代码中的 examples.jar ;2:将 HWND 类型修改为 int

    • 设置一个喜欢的端口 server.port

    初始化SDK

    初始化SDK概述

    一般的,我们希望在程序启动的时候就初始化sdk。

    • 这里使用了 ApplicationRunner 作为初始化入口,当程序启动成功后,将执行 Runner 做初始化
    • 为避免初始化sdk对主线程造成影响,所以 ApplicationRunner 需要放在线程池中 ThreadPoolExecutor,并添加try-catch处理
    • HCNetSDK是SDK示例代码中提供的一个对象,此对象继承Library,负责和更底层的C/C++库(更底层也许是C写的,这里不确定)交互,即执行 Native 调用。通过实例化此对象完成sdk依赖库的导入,并在后续业务开发中使用此对象向摄像机发布指令。
    • 涉及多操作系统平台的差异性,官方分别提供不同sdk依赖库,具体包含:win32,win64,linux32,linux64等,所以当初始化SDK的时候需要根据当前所处环境不同分别加载不同的依赖库文件
    • 上述提到的依赖库文件,在windows下就是 dll 后缀文件 , 在 linux 下就是 so 后缀文件
    • 真正执行初始化就是调用 hCNetSDK.NET_DVR_Init() 此方法,并可通过返回值为 truefalse 判断初始化是否成功。

    新建AppRunner

    • AppRunner 需要实现 ApplicationRunner 接口,并将
      AppRunner作为组件放到Spring 容器中管理
    • AppRunner 中注入SdkInitService ,并在run 方法中调用 SdkInitService 的initSdk 方法实现SDK的初始化
    
    package com.ramble.hikvisionsdkintegration;
    import com.ramble.hikvisionsdkintegration.service.SdkInitService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.stereotype.Component;
    
    @Component
    public class AppRunner  implements ApplicationRunner {
        @Autowired
        private SdkInitService hksdkInitService;
        @Override
        public void run(ApplicationArguments args) throws Exception {
            hksdkInitService.initSdk();
        }
    }
    
    
    

    新建SdkInitService

    • 定义一个公开的 hCNetSDK 属性,类型为 HCNetSDK ,并在构造函数中初始化 hCNetSDK 属性的值,此值需要全局唯一,这里参照官方代码做了单例处理。HCNetSDK 是官方提供的一个接口,一般的都是直接copy到项目源代码中,你会发现,所有和设备交互的地方都是通过这个接口来完成的
    • 内部定义一个异常回调类,用来处理和设备交互的时候全局异常的处理
    • 注入 ThreadPoolExecutor 执行器,真正的初始化将放到子线程中进行
    • 定义 initSdk 方法用来执行初始化
    • 需要注意的是,构造函数中为 hCNetSDK 属性初始化值,仅仅只是为了将 sdk 所需的依赖库文件 加载到运行时中,并没有真正的做初始化SDK的工作
    • 需要重点关注OSUtils中的代码,加载依赖库文件的前提是找到对应的库文件,的操作是在 getLoadLibrary 方法中管理的,这里编写的代码需要和部署时候选择的部署方式对应,否则可能会出现在windows中开发正常,部署到linux 中就报异常的问题

    SdkInitService:

    
    package com.ramble.hikvisionsdkintegration.service;
    import com.ramble.hikvisionsdkintegration.sdklib.HCNetSDK;
    import com.ramble.hikvisionsdkintegration.task.InitSdkTask;
    import com.ramble.hikvisionsdkintegration.util.OSUtils;
    import com.sun.jna.Native;
    import com.sun.jna.Pointer;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import java.util.concurrent.ThreadPoolExecutor;
    
    @Slf4j
    @Component
    public class SdkInitService {
        public static HCNetSDK hCNetSDK = null;
        static FExceptionCallBack_Imp fExceptionCallBack;
        static class FExceptionCallBack_Imp implements HCNetSDK.FExceptionCallBack {
            public void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {
                System.out.println("异常事件类型:" + dwType);
                return;
            }
        }
        public SdkInitService() {
            if (hCNetSDK == null) {
                synchronized (HCNetSDK.class) {
                    try {
                        hCNetSDK = (HCNetSDK) Native.loadLibrary(OSUtils.getLoadLibrary(), HCNetSDK.class);
                    } catch (Exception ex) {
                        log.error("SdkInitService-init-hCNetSDK-error");
                    }
                }
            }
        }
        @Autowired
        private ThreadPoolExecutor executor;
        public void initSdk() {
            log.info("HKSDKInitService-init-coming");
            executor.execute(new InitSdkTask());
        }
    }
    
    

    OSUtils:

    
    package com.ramble.hikvisionsdkintegration.util;
    import com.sun.jna.Platform;
    import lombok.extern.slf4j.Slf4j;
    import java.io.File;
    
    @Slf4j
    public class OSUtils {
        // 获取操作平台信息
        public static String getOsPrefix() {
            String arch = System.getProperty("os.arch").toLowerCase();
            final String name = System.getProperty("os.name");
            String osPrefix;
            if (Platform.isWindows()) {
                if ("i386".equals(arch)) {
                    arch = "x86";
                }
                osPrefix = "win32-" + arch;
            } else if (Platform.isLinux()) {
                if ("x86".equals(arch)) {
                    arch = "i386";
                } else if ("x86_64".equals(arch)) {
                    arch = "amd64";
                }
                osPrefix = "linux-" + arch;
            } else {
                osPrefix = name.toLowerCase();
                if ("x86".equals(arch)) {
                    arch = "i386";
                }
                if ("x86_64".equals(arch)) {
                    arch = "amd64";
                }
                int space = osPrefix.indexOf(" ");
                if (space != -1) {
                    osPrefix = osPrefix.substring(0, space);
                }
                osPrefix += "-" + arch;
            }
            return osPrefix;
        }
        public static String getOsName() {
            String osName = "";
            String osPrefix = getOsPrefix();
            if (osPrefix.toLowerCase().startsWith("win32-x86")
                    || osPrefix.toLowerCase().startsWith("win32-amd64")) {
                osName = "win";
            } else if (osPrefix.toLowerCase().startsWith("linux-i386")
                    || osPrefix.toLowerCase().startsWith("linux-amd64")) {
                osName = "linux";
            }
            return osName;
        }
        /**
         * 获取库文件
         * 区分win、linux
         *
         * @return
         */
        public static String getLoadLibrary() {
            if (isChecking()) {
                return null;
            }
            String userDir = System.getProperty("user.dir");
            log.info("getLoadLibrary-userDir={}", userDir);
            String loadLibrary = "";
            String library = "";
            String osPrefix = getOsPrefix();
            if (osPrefix.toLowerCase().startsWith("win32-x86")) {
                loadLibrary = System.getProperty("user.dir") + File.separator + "sdk" + File.separator + "hklibwin32" + File.separator;
                library = "HCNetSDK.dll";
            } else if (osPrefix.toLowerCase().startsWith("win32-amd64")) {
                loadLibrary = System.getProperty("user.dir") + File.separator + "sdk" + File.separator + "hklibwin64" + File.separator;
                library = "HCNetSDK.dll";
            } else if (osPrefix.toLowerCase().startsWith("linux-i386")) {
                //同 linux-amd64
                loadLibrary = "";
                library = "libhcnetsdk.so";
            } else if (osPrefix.toLowerCase().startsWith("linux-amd64")) {
                //方式一:使用系统默认的加载库路径,在系统的/usr/lib文件中加入你Java工程所需要使用的so文件,然后将HCNetSDKCom文件夹下的组件库也复制到/usr/lib目录,HCNetSDKCom文件夹中的组件库不要随意更换路径。CentOS 64位需拷贝到/usr/lib64下。
                //针对方式一,前缀就是绝对路径
                //loadLibrary = "/usr/lib64/lib/hkliblinux64/";
                //方式二:配置LD_LIBRARY_PATH环境变量加载库文件;配置/etc/ld.so.conf,加上你自己的Java工程所需要的so文件的路径
                //针对方式二,无需添加前缀,程序会从linux系统的so共享库中查找libhcnetsdk.so
                loadLibrary = "";
                library = "libhcnetsdk.so";
            }
            log.info("================= Load library Path :{} ==================", loadLibrary + library);
            return loadLibrary + library;
        }
        private static boolean checking = false;
        public static void setChecking() {
            checking = true;
        }
        public static void clearChecking() {
            checking = false;
        }
        public static boolean isChecking() {
            return checking;
        }
    }
    
    
    

    新建InitSdkTask

    • 此类实现 Runnable 接口,并重写run方法。
    • 新建一个私有属性 hCNetSDK 并赋值为 SdkInitService.hCNetSDK ,因为初始化需要用到 HCNetSDK 这个对象和设备交互,所以初始化前必须确保此对象已经创建,本例中,程序在执行 SdkInitService 构造函数的时候初始化了 HCNetSDK 对象,并放到一个全局静态变量中
    • 其实也可以不新建私有属性 hCNetSDK ,在需要用到此对象的地方 使用 SdkInitService.hCNetSDK 的方式获取也可以
    • 通过调用  hCNetSDK.NET_DVR_Init 方法执行初始化,并可以通过返回值确定是否初始化成功,初始化成功后,将可以调用业务接口向设备发送指令。
    • NET_DVR_SetConnectTime,NET_DVR_SetReconnect 是可选的,并不会对初始化SDK本身造成影响。
    • 为了避免对主程序造成影响,初始化代码将需要做 try - catch 处理

    InitSdkTask:

    
    package com.ramble.hikvisionsdkintegration.task;
    import com.ramble.hikvisionsdkintegration.sdklib.HCNetSDK;
    import com.ramble.hikvisionsdkintegration.service.SdkInitService;
    import com.ramble.hikvisionsdkintegration.util.OSUtils;
    import lombok.extern.slf4j.Slf4j;
    import java.util.Objects;
    
    @Slf4j
    public class InitSdkTask implements Runnable {
        /**
         * 装配 sdk 所需依赖
         */
        private static HCNetSDK hCNetSDK = SdkInitService.hCNetSDK;
        @Override
        public void run() {
            try {
                if (Objects.equals(OSUtils.getOsName(), "linux")) {
                    log.info("InitSdk-is-linux");
                    String userDir = System.getProperty("user.dir");
                    log.info("InitSdk-userDir={}", userDir);
                    String osPrefix = OSUtils.getOsPrefix();
                    if (osPrefix.toLowerCase().startsWith("linux-i386")) {
                        HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
                        HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
                        //这里是库的绝对路径,请根据实际情况修改,注意改路径必须有访问权限
                        //linux 下, 库加载参考:OSUtils.getLoadLibrary()
                        String strPath1 = System.getProperty("user.dir") + "/hkliblinux32/libcrypto.so.1.1";
                        String strPath2 = System.getProperty("user.dir") + "/hkliblinux32/libssl.so.1.1";
                        System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
                        ptrByteArray1.write();
                        hCNetSDK.NET_DVR_SetSDKInitCfg(3, ptrByteArray1.getPointer());
                        System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
                        ptrByteArray2.write();
                        hCNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());
                        //linux 下, 库加载参考:OSUtils.getLoadLibrary()
                        String strPathCom = System.getProperty("user.dir") + "/hkliblinux32/HCNetSDKCom/";
                        HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
                        System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());
                        struComPath.write();
                        hCNetSDK.NET_DVR_SetSDKInitCfg(2, struComPath.getPointer());
                    } else if (osPrefix.toLowerCase().startsWith("linux-amd64")) {
                        HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
                        HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
                        //这里是库的绝对路径,请根据实际情况修改,注意改路径必须有访问权限
                        //linux 下, 库加载参考:OSUtils.getLoadLibrary()
                        String strPath1 = System.getProperty("user.dir") + "/hkliblinux64/libcrypto.so.1.1";
                        String strPath2 = System.getProperty("user.dir") + "/hkliblinux64/libssl.so.1.1";
                        System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
                        ptrByteArray1.write();
                        hCNetSDK.NET_DVR_SetSDKInitCfg(3, ptrByteArray1.getPointer());
                        System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
                        ptrByteArray2.write();
                        hCNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());
                        String strPathCom = System.getProperty("user.dir") + "/hkliblinux64/HCNetSDKCom/";
                        //linux 下, 库加载参考:OSUtils.getLoadLibrary()
                        HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
                        System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());
                        struComPath.write();
                        hCNetSDK.NET_DVR_SetSDKInitCfg(2, struComPath.getPointer());
                    } else {
                        log.info("osPrefix={}", osPrefix);
                    }
                }
                //初始化sdk
                boolean isOk = hCNetSDK.NET_DVR_Init();
                hCNetSDK.NET_DVR_SetConnectTime(10, 1);
                hCNetSDK.NET_DVR_SetReconnect(100, true);
                if (!isOk) {
                    log.error("=================== InitSDK init fail ===================");
                } else {
                    log.info("============== InitSDK init success ====================");
                }
            } catch (Exception e) {
                log.error("InitSDK-error,e={}", e.getMessage());
                e.printStackTrace();
            }
        }
    }
    
    
    

    新建 HCNetSDK

    直接从官方示例代码中copy过来即可

    调用业务接口

    • 新建一个controller ,尝试调用 获取SDK状态 的接口。
    • 调用所有的业务接口之前都需要先登录
    
    package com.ramble.hikvisionsdkintegration.controller;
    import com.alibaba.fastjson2.JSON;
    import com.ramble.hikvisionsdkintegration.dto.GlobalResponseEntity;
    import com.ramble.hikvisionsdkintegration.sdklib.HCNetSDK;
    import com.ramble.hikvisionsdkintegration.service.SdkInitService;
    import com.sun.jna.Memory;
    import com.sun.jna.Pointer;
    import com.sun.jna.ptr.IntByReference;
    import lombok.AllArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @Slf4j
    @AllArgsConstructor
    @RestController
    @RequestMapping("/test")
    public class TestController {
        private static String m_sDeviceIP = "192.168.1.142";
        private static String m_sUsername = "xxx";
        private static String m_sPassword = "xxx";
        
        /**
         * 获取sdk状态
         *
         * @return {@link GlobalResponseEntity}<{@link String}>
         * 返回值举例:{"success":true,"code":"000000","message":"request successfully",
         * "data":"{\"dwRes\":[0,0,0,0,0,0,0,0,0,0],\"dwTotalAlarmChanNum\":0,\"dwTotalBroadCastNum\":0,\"dwTotalFileSearchNum\":0,\"dwTotalFormatNum\":0,
         * \"dwTotalLogSearchNum\":0,\"dwTotalLoginNum\":1,\"dwTotalPlayBackNum\":0,\"dwTotalRealPlayNum\":0,\"dwTotalSerialNum\":0,\"dwTotalUpgradeNum\":0,
         * \"dwTotalVoiceComNum\":0,\"autoRead\":true,\"autoWrite\":true,\"pointer\":{\"size\":84,\"valid\":true}}"}
         */
         
        @GetMapping("/state")
        public GlobalResponseEntity getSdkState() {
            //登录
            Integer userId = login();
            log.info("userId={}", userId);
            HCNetSDK.NET_DVR_SDKSTATE sdkState = new HCNetSDK.NET_DVR_SDKSTATE();
            //获取当前SDK状态信息
            boolean result = SdkInitService.hCNetSDK.NET_DVR_GetSDKState(sdkState);
            if (result) {
                sdkState.read();
                String s = JSON.toJSONString(sdkState);
                return GlobalResponseEntity.success(s);
            } else {
                int error = SdkInitService.hCNetSDK.NET_DVR_GetLastError();
                return GlobalResponseEntity.error("获取失败,错误码为:" + error);
            }
        }
        
        
        private Integer login() {
            HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();//设备登录信息
            m_strLoginInfo.sDeviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN];
            System.arraycopy(m_sDeviceIP.getBytes(), 0, m_strLoginInfo.sDeviceAddress, 0, m_sDeviceIP.length());
            m_strLoginInfo.sUserName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN];
            System.arraycopy(m_sUsername.getBytes(), 0, m_strLoginInfo.sUserName, 0, m_sUsername.length());
            m_strLoginInfo.sPassword = new byte[HCNetSDK.NET_DVR_LOGIN_PASSWD_MAX_LEN];
            System.arraycopy(m_sPassword.getBytes(), 0, m_strLoginInfo.sPassword, 0, m_sPassword.length());
            m_strLoginInfo.wPort = Short.valueOf("8000");
            m_strLoginInfo.bUseAsynLogin = false; //是否异步登录:0- 否,1- 是
            m_strLoginInfo.write();
            HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();//设备信息
            int loginHandler = SdkInitService.hCNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo);
            if (loginHandler == -1) {
                int errorCode = SdkInitService.hCNetSDK.NET_DVR_GetLastError();
                IntByReference errorInt = new IntByReference(errorCode);
                log.error("[HK] login fail errorCode:{}, errMsg:{}", errorCode, SdkInitService.hCNetSDK.NET_DVR_GetErrorMsg(errorInt));
                return null;
            } else {
                return loginHandler;
            }
        }
    }
    
    
    

    部署

    拷贝so库文件到部署目录

    所有厂家的所有版本sdk库文件均维护在项目源代码中,需要将linux库文件so文件拷贝到部署根目录,和jar文件同级

    追加环境变量

    通过配置 LD_LIBRARY_PATH 环境变量加载库文件,打开系统的 /etc/profile 配置文件,在最后追加so库文件所在目录:

    
    export  LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/app/jars/hkliblinux64:/home/app/jars/hkliblinux64/HCNetSDKCom
    
    

    如上所示:
    ● 32位就追加  hkliblinux32 目录,64位就追加 hkliblinux64 目录
    ● 不要忘记 HCNetSDKCom 目录也需要配置,因为里面也有so库文件。
    执行source 命令,让配置生效:

    
    source   /etc/profile
    
    

    追加so库加载路径

    打开 /etc/ld.so.conf 配置文件,追加so库文件所在目录

    
    /home/app/jars/hkliblinux64
    /home/app/jars/hkliblinux64/HCNetSDKCom
    
    

    如上所示:
    ● 32位就追加  hkliblinux32 目录,64位就追加 hkliblinux64 目录。
    ● 不要忘记 HCNetSDKCom 目录也需要配置,因为里面也有so库文件。

    执行 ldconfig 命令,让配置生效:

    
    ldconfig
    
    

    验证SDK初始化是否成功

    一般来说,可以在程序初始化SDK的时候添加日志,通过日志输出判断是否初始化成功。

    代码

    https://gitee.com/naylor_personal/ramble-spring-boot/tree/master/hikvision-sdk-integration


    __EOF__

  • 本文作者: 陈明亮
  • 本文链接: https://www.cnblogs.com/Naylor/p/17252436.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    最长回文子序列长度、最长回文子串,最长回文串、回文子串
    spring boot 集成 swagger3
    前有苹果、后有谷歌,微软的 2022 依旧“压力山大”
    Ubuntu安装MATLAB
    如何提升推荐系统的可解释性?京东智能推荐卖点技术全解析
    PMP证书在国内已经泛滥了,大家怎么看?
    以太网帧间隙IFG详解(Interframe Gap帧间距)
    使用VBA创建数字金字塔
    你真的面向对象了吗?
    Vue3+node.js网易云音乐实战项目(六)
  • 原文地址:https://www.cnblogs.com/Naylor/p/17252436.html