为了账号安全,请及时绑定邮箱和手机立即绑定

Web操作摄像头、高拍仪、指纹仪等设备的功能扩展方案

标签:
webpack

摘要:信息系统开发中难免会有要操作摄像头、高拍仪、指纹仪等硬件外设,或者诸如获取机器签名、硬件授权保护(加密锁)检测等情况。受限于Web本身运行机制,就不得不使用Active、浏览器插件进行能力扩展了。本文主要向大分享一种基于URL Scheme的Web功能扩展方法,供大家参考。

一、方案对比

1.1 ActiveX

早期的IE浏览器扩展方法,可以使用VB6、C++、.Net等编写,曾经使用VB6编写过指纹仪操控,IE6时代还好配置,后面IE7、IE8出来后兼容性就实在头大了,其它浏览器出来后就更难于解决了,便再没有使用过此方案了。缺点是对浏览器限制太多、兼容性太差,难于部署及调用,且只支持IE。

1.2 Chrome扩展插件

Chrome系浏览器的插件扩展方法,由于对此不熟,没有实际使用过,在此不作介绍。明显的缺点便是只支持Chrome系列。

1.3 自定义URL Scheme方案

此方案便是本文介绍的方案,方案大致过程是,Web页面使用自定义URL协议驱动起协议进程 , 再由协议进程拉起扩展功能WinForm应用进程,Web页通过HTTP与扩展应用通信,操控扩展功能,如下图所示:
图片描述

二、方案实现

2.1 协议进程

协议进程主要功能是:注册、反注册URL Scheme;协议调用时负责检查扩展功能WinForm应用是否已启动,如果没有启动则拉起扩展应用,否则直接退出无动作。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Web;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;
using Microsoft.Win32;


namespace UrlHook
{
    /// <summary>
    /// 协议入口程序
    /// </summary>
    class Program
    {
        #region 私有成员
        private const string PROTOCOL = "Hdc";
        private const string URL_FILE = "origin.set";
        #endregion

        #region 入口点
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main(params string[] args)
        {
            if (args.Length < 1)
                return;

            var first = args[0].Trim().ToLower();
            var second = false;
            if (args.Length >= 2)
                second = args[1].Trim().ToLower() == "-q";

            switch (first)
            {
                case "-i":
                    RegistrUrl(second);
                    return;
                case "-u":
                    UnregistrUrl(second);
                    return;
            }

            try
            {

                if (Process.GetProcessesByName("HdcClient").Any())
                {
                    return;
                }

                //启动进程
                Process p = new Process();
                p.StartInfo.FileName = Assembly.GetExecutingAssembly().Location;
                p.StartInfo.FileName = p.StartInfo.FileName.Substring(0, p.StartInfo.FileName.LastIndexOf("\\"));
                p.StartInfo.WorkingDirectory = p.StartInfo.FileName;
                p.StartInfo.FileName += "\\HdcClient.exe";
                p.Start();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
        #endregion

        #region 私有方法
        /// <summary>
        /// 向注册表注册URL地址
        /// </summary>
        private static void RegistrUrl(bool quiet = false)
        {
            var exePath = Assembly.GetExecutingAssembly().Location;

            RegistryKey root = Registry.ClassesRoot.CreateSubKey(PROTOCOL);
            root.SetValue(null, "Url:" + PROTOCOL);
            root.SetValue("URL Protocol", exePath);

            var deficon = root.CreateSubKey("DefaultIcon");
            deficon.SetValue(null, exePath + ",1", RegistryValueKind.String);

            var shell = root.CreateSubKey("shell")
                .CreateSubKey("open")
                .CreateSubKey("command");

            shell.SetValue(null, "\"" + exePath + "\" \"%1\"");

            shell.Close();
            deficon.Close();
            root.Close();

            if (!quiet)
            {
                MessageBox.Show("恭喜,协义注册成功;如果仍不生效,请偿试重启浏览器...", "提示"
                    , MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }

        /// <summary>
        /// 解除协议注册
        /// </summary>
        private static void UnregistrUrl(bool quiet = false)
        {
            RegistryKey root = Registry.ClassesRoot.OpenSubKey(PROTOCOL, true);
            if (root != null)
            {
                root.DeleteSubKeyTree("shell", false);
                Registry.ClassesRoot.DeleteSubKeyTree(PROTOCOL, false);
                root.Close();
            }

            Registry.ClassesRoot.Close();

            if (!quiet)
            {
                MessageBox.Show("协议解除成功,客户端已经失效。", "提示"
                        , MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }
        #endregion
    }
}

2.2 扩展应用集成Http Server

Web页是通过HTTP协议与扩展WinForm应用通信的,所以我们要在扩展应用中集成一个HTTP Server,这里我们采用的是.Net的OWIN库中的Kestrel,轻量、使用简单,虽然在WinForm中只支持Web API,但已经够用了。这里我们的监听的是http://localhost:27089,端口选择尽量选5位数的端口,以免与客户机的其它应用冲突。

//main.cs
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO;
using Microsoft.Owin.Hosting;
using System.Reflection;

namespace HdcClient
{
    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            //不能同时启动多个
            var query = from p in Process.GetProcesses()
                        where p.ProcessName == "HdcClient"
                        && p.Id != Process.GetCurrentProcess().Id
                        select p;

            if (query.Any())
            {
                IntPtr winHan = query.First().MainWindowHandle;
                if (winHan.ToInt32() == 0 && File.Exists("win.hwd"))
                {
                    winHan = (IntPtr)Convert.ToInt64(File.ReadAllText("win.hwd"), 16);
                }

                ShowWindow(winHan, 4);
                SetForegroundWindow(winHan);
                return;
            }

            //重定向路径
            var path = Assembly.GetExecutingAssembly().Location;
            path = Path.GetDirectoryName(path);
            Directory.SetCurrentDirectory(path);

            //启动服务通信
            WebApp.Start<Startup>("http://localhost:27089");

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new FrmMain());
        }


    }
}

//Startup.cs
using System.IO;
using System.Web.Http;

using Microsoft.Owin;
using Microsoft.Owin.FileSystems;
using Microsoft.Owin.StaticFiles;
using Owin;

namespace HdcClient
{
    /// <summary>
    ///Web启动程序
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// 配置各中间件
        /// </summary>
        /// <param name="appBuilder"></param>
        public void Configuration(IAppBuilder appBuilder)
        {
            //配置API路由
            HttpConfiguration config = new HttpConfiguration();

            config.Routes.MapHttpRoute(
                name: "MediaApi",
                routeTemplate: "api/media/{action}/{key}",
                defaults: new
                {
                    controller = "media"
                }
            );

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new
                {
                    id = RouteParameter.Optional
                }
            );

            appBuilder.Use<Api.CorsOptionsMiddleware>();
            appBuilder.UseWebApi(config);
        }
    }
}

2.3 关键问题跨域访问

助手虽然使用localhost本地地址与Web页通信,但是像chrome这样严格检测跨域访问的浏览器,仍然会有跨域无法访问扩展应用的问题。因此,HTTP Server要开启允许跨域访问,我们定义一个中间件来处理跨域,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Owin;


namespace HdcClient.Api
{
    /// <summary>
    /// CORS时OPTIONS提交响应
    /// </summary>
    public class CorsOptionsMiddleware : OwinMiddleware
    {
        #region 构造方法
        /// <summary>
        /// 初始化中间件
        /// </summary>
        /// <param name="next"></param>
        public CorsOptionsMiddleware(OwinMiddleware next)
            :base(next)
        {

        }
        #endregion

        #region 重写方法
        /// <summary>
        /// 响应跨域请求
        /// </summary>
        /// <param name="context">请求上下文</param>
        /// <returns></returns>
        public override Task Invoke(IOwinContext context)
        {
            if (context.Request.Method.ToUpper() != "OPTIONS")
                return this.Next.Invoke(context);

            var response = context.Response;
            response.Headers.Append("Access-Control-Allow-Origin", "*");
            response.Headers.Append("Access-Control-Allow-Methods", "*");
            response.Headers.Append("Access-Control-Allow-Headers", "x-requested-with");
            response.Headers.Append("Access-Control-Allow-Headers", "content-type");
            response.Headers.Append("Access-Control-Allow-Headers", "content-length");

            return Task.FromResult<string>("OK");
        }
        #endregion
    }
}

三、Web页如何调用?

3.1 调起助手


/*
 * 硬件设备控制访问控制
 *       
 * @Alphaair
 * 20151114 create.
 * 20190723 移值,更换AJAX库。
**/

import http from "axios";

const hdc = {
    VERSION: '2.0.0',
    CLIENT_URL: 'http://localhost:27089/',
    getRootUrl: function () {
        ///<summary>获取当前访问地址根URL</summary>

        var url = location.protocol + '//';
        url += location.host;

        return url;
    },
    openClient: function (callback, count) {
        ///<summary>开启客户端组件</summary>

        let url = `${this.CLIENT_URL}api/pipe/test`;
        http.get(url, {
            responseType: 'text'
        }).then(rsp => {
            //错误
            if (rsp.stack)
                throw rsp;

            try {
                if (callback)
                    callback();
            }
            catch (err) {
                alert(err.message);
                console.error(err);
            }
        }).catch(err => {
            console.error(err);
            if (count >= 10) {
                alert("客户端组件启动失败,请确认是否已经正常安装或者偿试手工启动!");
                return;
            }

            count = count || 1;
            if (count < 3) {
                let origin = this.getRootUrl();
                origin = encodeURIComponent(origin);
                window.open(`Hdc://startup/${origin}`);
            }

            //递归
            setTimeout(function () {
                count = count || 1;
                count++;
                hdc.openClient(callback, count);
            }, 5000);
        });
    },
    /**
     * 启动身份证读取
     * 
     * @param {Function} callback 读取回调
     * 
     */
    readIdCard: function (callback) {

        const self = this;
        let url = `${self.CLIENT_URL}/api/IdReader/Reading`;
        http.get(url, {
            params: {
                _ds: (new Date()).getTime()
            }
        }).then(rsp => {
            let fkb = rsp.data;
            if (fkb.Success) {
                callback(fkb);
            }
            else {
                alert(fkb.Message);
            }
        }).catch(err => {
            console.error('身份证阅读器启动失败,可能客户端未打开.', err);
            callback(false);
        });
    },
    /**
     * 获取身份证号码扫描结果
     * 
     * @param {Function} callback 读取回调
     * @param {Boolean} isLoop 是否循环读取
     */
    getIdCard: function (callback, isLoop) {
        //获取身份证扫描结果

        var self = this;
        if (!isLoop)
            self._cancelGetIdCard = false;
        else
            self._cancelGetIdCard = true;

        let url = `${self.CLIENT_URL}/api/IdReader/GetIdCard`;
        http.get(url, {
            params: {
                _ds: (new Date()).getTime()
            }
        }).then(rsp => {
            let fkb = rsp.data;
            if (fkb.Success) {
                callback(fkb);
                return;
            }

            //一秒后重新发起监听
            if (self._cancelGetIdCard) {
                setTimeout(function () {
                    self.getIdCard(callback, true);
                }, 1000);
            }
        }).catch(err => {
            console.error('获取身份证识别结果失败,请确认客户端正常.', err);
            callback(false);
        });
    },
    cancelIdCard: function () {
        this._cancelGetIdCard = false;
    }
};

export default hdc;

3.2 操作反馈结果获取

像高拍仪拍摄这样的操控需要一定时长,真实场景也无法确认用户什么时候操作完成,如果使用发起连接等待操作完成,势必有可能引起因为HTTP超时而失败。所以本方案采用操控发起与结果连接分离的方法,控制请求只负责调起相应的功能,不返回操控结果,结果采用轮询的方式获取,如下面代码:

http.get(url, {
            params: {
                _ds: (new Date()).getTime()
            }
        }).then(rsp => {
            ...
            //一秒后重新发起监听
            if (self._cancelGetIdCard) {
                setTimeout(function () {
                    self.getIdCard(callback, true);
                }, 1000);
            }
        }).catch(err => {
            ...
        });

四、实现效果

图片描述

作者:alphaair
原文出处:https://www.cnblogs.com/alphaair/p/14679536.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消