2024年4月

1. 引言

浏览器中如何创建文件夹、写入文件呢?

答曰:可以借助JSZip这个库来实现在浏览器内存中创建文件与文件夹,最后只需下载这个.zip文件,就是最终得结果

类似的使用场景如下:

  • 在线下载很多图片,希望这些图片能分类保存到各个文件夹并最终下载成一个zip文件
  • 在线下载很多文档,希望这些文档能分类保存到各个文件夹并最终下载成一个zip文件

本质上都是希望浏览器能创建文件夹和创建文件,最终保存成一个文件来提供下载

JSZip的GitHub站点:
Stuk/jszip: Create, read and edit .zip files with Javascript (github.com)

一个可用的中文站点:
JSZip参考手册 (asprain.cn)

下面主要记录一下基础使用,详细的API请参考上述文档

2. 使用

2.1 安装

使用NPM:

npm install jszip

使用在线CDN:

<script src="https://cdn.bootcdn.net/ajax/libs/jszip/3.10.1/jszip.js"></script>
  • 为了可以代码可以快速复现,笔者这里使用CDN的方式引入

2.2 创建zip实例

一个JSZip实例是读写.zip文件的基础

const zip = new JSZip();

2.3 读取zip文件

读取官方的示例文件
text.zip

const zip = new JSZip();

fetch("https://stuk.github.io/jszip/test/ref/text.zip")       // 1) fetch the url
    .then(function (response) {                       // 2) filter on 200 OK
        if (response.status === 200 || response.status === 0) {
            return Promise.resolve(response.blob());
        } else {
            return Promise.reject(new Error(response.statusText));
        }
    })
    .then(data => zip.loadAsync(data))                            // 3) 加载数据
    .then(function (zip) {
        zip.forEach(function (relativePath, file) {  	// 4) 遍历压缩包内的文件
           console.log(`path: ${relativePath}, file: ${file.name}`)
           // 输出:path: Hello.txt, file: Hello.txt
        });
})

因为
Hello.txt
是个文本文件,可以直接使用
string
的方式读取内部的数据

const zip = new JSZip();

fetch("https://stuk.github.io/jszip/test/ref/text.zip")       // 1) fetch the url
    .then(function (response) {                       // 2) filter on 200 OK
        if (response.status === 200 || response.status === 0) {
            return Promise.resolve(response.blob());
        } else {
            return Promise.reject(new Error(response.statusText));
        }
    })
    .then(data => zip.loadAsync(data))                            // 3) chain with the zip promise
    .then(function (zip) {
        return zip.file("Hello.txt").async("string"); // 4) 读取Hello.txt文件
    })
    .then(function success(text) {
        console.log(text); // 输出:Hello World
    }, function error(e) {
        console.error(e);
    });

2.4 创建zip文件

写入文件与数据

zip.file("file.txt", "content");
new Promise((resolve, reject) => {
    resolve(zip.file("file.txt").async("string"))
}).then(data => {
    console.log(data); // 输出:content
})

写入指定文件夹下的指定文件

zip.file("text/file.txt", "content");
zip.forEach(function (relativePath, file) {
    console.log(`path: ${relativePath}, file: ${file.name}`)
    // 输出:path: text/file.txt, file: text/file.txt
});

最后的目录结构可以参考下图

2.5 下载zip文件

这里将上面的file.txt下载为zip,使用a链接的方式

zip.generateAsync({ type: "blob" }).then(function (content) {
    document.body.appendChild(document.createElement("a"));
    document.querySelector("a").href = URL.createObjectURL(content);
    document.querySelector("a").download = "test.zip";
    document.querySelector("a").click();
});

image

完整的代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jszip/3.10.1/jszip.js"></script>
</head>

<body>

    <script>
        const zip = new JSZip();

        // fetch("https://stuk.github.io/jszip/test/ref/text.zip")       // 1) fetch the url
        //     .then(function (response) {                       // 2) filter on 200 OK
        //         if (response.status === 200 || response.status === 0) {
        //             return Promise.resolve(response.blob());
        //         } else {
        //             return Promise.reject(new Error(response.statusText));
        //         }
        //     })
        //     .then(data => zip.loadAsync(data))                            // 3) chain with the zip promise
        //     .then(function (zip) {
        //         return zip.file("Hello.txt").async("string"); // 4) chain with the text content
        //     })
        //     .then(function success(text) {
        //         console.log(text);
        //     }, function error(e) {
        //         console.error(e);
        //     });
        zip.file("text/file.txt", "content");
        zip.forEach(function (relativePath, file) { 
           console.log(`path: ${relativePath}, file: ${file.name}`)
        });

        zip.generateAsync({ type: "blob" }).then(function (content) {
            document.body.appendChild(document.createElement("a"));
            document.querySelector("a").href = URL.createObjectURL(content);
            document.querySelector("a").download = "test.zip";
            document.querySelector("a").click();
        });
    </script>

</body>

</html>

3. 参考资料

[1]
How to use JSZip (stuk.github.io)

[2]
JSZip参考手册 (asprain.cn)

背景介绍

hi 大家好,我是三合,作为一个非著名懒人,每天上完班回到家,瘫在沙发上一动都不想动,去开个灯我都嫌累,此时,智能家居拯救了我,只需要在手机点点点,开关灯,空调,窗帘就都搞定了,一开始我用的是开源的home assistan,俗称HA,搭配上hass-xiaomi-miot以及hap-python这几个插件,就可以将米家的智能家居设备接入苹果的homekit生态,整体而言在苹果手机上使用体验非常好,但是有一个致命的问题,每隔一段时间,米家的设备在homekit上就会失效,需要我重置,为此没少被老婆埋怨这智能家居怎么这么难用,然后ha又是python写的,说句实话,我不太喜欢这种动态语言,各种奇怪用法,看得我云里雾里,更谈不上调试了,此时我就在想,如果我用c#写homeKit和米家的sdk,自己搞一个智能家居,岂不美哉,毕竟有了原生api,那就有了无限可能。有了这个奇怪的想法后,我首先上github上搜索了c#的智能家居sdk,发现c#在智能家居这个领域,几乎一片空白,更谈不上原生的sdk了,基本都是python的项目,即使有一些c#的,也需要搭配ha或者HAP-NodeJS使用, 没办法,我只能参考hass-xiaomi-miot这个项目移植了米家的sdk并命名为【MiHome.Net】,以及参考hap-python这个项目移植了苹果homekit的sdk并命名为【Homekit.Net】,MiHome.Net还在为开源前作最后的代码修改,今天发布的,正是苹果homekit的sdk【Homekit.Net】,接下来,我将介绍他的用法。

HomeKit中的一些基本概念

HomeKit中每一个智能家居称为一个配件(Accessory),每个配件拥有多个服务(Services),每个服务又有多个特征(characteristics),所有配件都有配件信息服务,这个服务里包含了2个特性,1名字,2固件版本号,并且配件根据功能还有另外一些独有的服务,我们以一个开关为例,开关本身就是一个配件,配件种类是switch,他就拥有一个服务叫Switch,这个服务下面,有一个特性叫on,也就是开关,我们给这个特性赋值true,就代表开,赋值false,就代表关。

Homekit.Net存在的意义

通过本依赖包,用户可以通过代码模拟出各种各样的智能家居设备,并添加到苹果手机的家庭app中,这样我们就能在手机上控制这些模拟的智能家居设备执行一些我们在代码里配置好的操作,比如我们可以通过代码控制电脑打开或者关闭某个应用,然后利用本库封装为一个开关,那么我们就可以用家庭app中的这个模拟开关来控制应用了。有了原生api,大家就可以尽情的发挥想象力去搞事情了,比如DIY一个自动喂鱼机?

Getting Started

Nuget

接下来我将演示如何使用【Homekit.Net】,你可以运行以下命令在你的项目中安装 Homekit.Net 。

PM> Install-Package Homekit.Net

支持框架

net 6,net 8

示例

通过继承类Accessory,我们就可以自定义一个自己的配件,在下面的示例中,我们定义一个开关,在构造函数中,我们加载一个名为Switch的服务,并且定义配件类型为开关,从switch服务中获取on这个特性,通过操作on这个特性,我们就可以通过代码模拟开关状态变化了,并且可以在苹果手机的家庭app上看到开关状态的变化。

public class Switch : Accessory
{
    private bool IsOn;
    private Timer timer;
    public Characteristics CurrentOnCharacteristics { get; set; }

    public event Action<object> OnChange; 
    public Switch(AccessoryDriver accessoryDriver, string name, int? aid = null) : base(accessoryDriver, name, aid)
    {
        //加载switch开关服务
        var service = AddPreloadService("Switch");
        //定义配件种类为开关
        Category = Category.CATEGORY_SWITCH;
        //从switch服务中获取on这个特性
        CurrentOnCharacteristics = service.GetCharacteristics("On");
        //添加开关状态被家庭app改变后的回调函数
        CurrentOnCharacteristics.SetValueCallback = (o =>
        {
            OnChange(o);
            this.IsOn = (bool)o ;
        });
        //定义一个定时器,定时改变开关状态,用来模拟开关状态变化
        //timer = new Timer(Test, default, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
    }

    public void Test(object? state)
    {
        var random = new Random();
        var number = random.Next(1, 50);
        var isOn = number % 2 == 0;
        CurrentOnCharacteristics.SetValue(isOn);
        timer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
    }
}

接下来,我们再来一个示例,定义一个温度传感器,在构造函数中,我们加载一个名为TemperatureSensor的服务,并且定义配件类型为传感器,从TemperatureSensor服务中获取CurrentTemperature(当前温度)这个特性,通过代码操作CurrentTemperature这个特性,我们就可以模拟温度变化,并且在苹果手机的家庭app上看到温度变化了。

public class TemperatureSensor : Accessory
{

    public Characteristics CurrentTemperatureCharacteristics { get; set; }

    private Timer timer;
    public TemperatureSensor(AccessoryDriver accessoryDriver, string name, CancellationToken token = default) : base(accessoryDriver, name)
    {
        //加载TemperatureSensor温度服务
        var service = AddPreloadService("TemperatureSensor");
        //定义配件种类为传感器
        Category = Category.CATEGORY_SENSOR;
        //从TemperatureSensor服务中获取CurrentTemperature(当前温度)这个特性
        CurrentTemperatureCharacteristics = service.GetCharacteristics("CurrentTemperature");
        //设置温度为1
        CurrentTemperatureCharacteristics.SetValue(1);
        //定义一个定时器,定时改变温度,用来模拟温度变化
        timer = new Timer(Test, token, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));

    }

    public void Test(object? state)
    {
        if (state is CancellationToken token && token.IsCancellationRequested)
        {
            return;
        }
        // Console.WriteLine(DateTime.Now+"触发了定时任务");
        var random = new Random();
        var wd = random.Next(1, 50);
        // Console.WriteLine($"设置温度为{wd}度");
        CurrentTemperatureCharacteristics.SetValue(wd);
        timer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
    }
}

更多的配件类型,欢迎大家自行解锁,配件的所有服务和特征汇总,可以在程序运行起来后,查看Resources文件夹下的json文件
服务与特性集合

配件定义结束以后,我们就要让这个配件跑起来了,新建一个控制台程序,代码如下:

 internal class Program
 {
     private async static Task SingleAccessory()
     {

         var cts = new CancellationTokenSource();
         //先定义驱动
         var driver = new AccessoryDriver(port: 6666666);
         //定义配件
         var switchAccessory1 = new Switch(driver, "switch开关");
         //添加开关状态被苹果手机的家庭app改变后的回调
         switchAccessory1.OnChange += async (o) =>
         {
             Console.WriteLine("The switch state has changed.开关状态变化了");
         };

         driver.AddAccessory(switchAccessory1);
         await driver.StartAsync(cts.Token);
     }

     private async static Task MultipleAccessories()
     {

         var cts = new CancellationTokenSource();
         //先定义驱动
         var driver = new AccessoryDriver(port: 6554);
         //定义网关
         var bridge = new Bridge(driver, "网关");
         //定义配件1开关
         var switchAccessory1 = new Switch(driver, "开关switch");
         bridge.AddAccessory(switchAccessory1);
         //添加开关状态被苹果手机的家庭app改变后的回调
         switchAccessory1.OnChange += async (o) =>
         {
             Console.WriteLine("The switch state has changed.开关状态变化了");
         };
         //定义配件2传感器
         var temperatureSensor= new TemperatureSensor(driver, "传感器TemperatureSensor");
         bridge.AddAccessory(temperatureSensor);
         driver.AddAccessory(bridge);
         await driver.StartAsync(cts.Token);
     }

     async static Task Main(string[] args)
     {
         //Test Multiple Accessories 测试单配件
         await SingleAccessory();
         //Test Multiple Accessories 测试多配件
         //await MultipleAccessories();

     }
 }

以上这段代码分为2个部分,SingleAccessory单配件示例,和MultipleAccessories多配件示例,大体流程就是首先定义一个驱动,接着实例化之前定义的配件,并且把配件加入到驱动中,最后启动驱动即可。启动后效果如下图,他会在控制台上打印出一个二维码,

启动效果

接着我们使用苹果手机的家庭app扫描这个二维码(添加配件需要在同一个局域网中),即可添加我们代码中自定义的配件。

如果我们想在程序中定义多个配件,那么参考MultipleAccessories方法,首先得定义一个网关,接着把我们定义的多个配件添加到网关里,最后再启动驱动。

开源地址,欢迎star

本项目基于MIT协议开源,地址为
https://github.com/TripleView/HomeKit.Net

同时感谢以下项目

  1. HAP-Python

  2. ZeroConfig

写在最后

如果各位靓仔觉得这个项目不错,欢迎一键三连(推荐,star,关注)

晚上编码的时候,想到了以前编写的窗体关闭的事情,就是带托盘图标的应用,有一个主显示操作窗体,但是主窗体点击关闭按钮的时候,实际上是窗体隐藏而非真正关闭,这个在其它的一些应用程序里有这个效果。于是就想到了这个例子,记录下来,如果其他读者也有这个问题,那直接复用此例子的代码即可。

1、
项目目录;

2、
源码介绍;

1) 拦截关闭操作的代码;

2) 真正关闭窗体的代码;

3、
运行界面;

因为未对窗体界面进行处理,所以这里没界面效果截图。

4、
使用介绍;

5、
源码下载;

提供源码下载:
https://download.csdn.net/download/lzhdim/89097181

6、
其它建议;

这是个简单明了的例子,请读者自己扩展该应用。

上面介绍了C#的假关闭窗体的代码以及真正关闭窗体的代码,需要的读者请自己复用此例子的代码即可。

写在前面

在多线程的情况下对一个接口进行访问,如果访问次数过大,且没有缓存存在的情况下大量的请求打到数据库可能会存在数据库宕机,从而造成服务的不可用性。往往我们需要对其进行限流操作用来保证服务的高可用性,以下介绍下redis限流如何使用。

lua脚本

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 本身并没有提供对于原子性的直接支持
,它只是一种脚本语言,通常是嵌入到其他宿主程序中运行,比如Redis。 在Redis中,执行Lua脚本的原子性是指:整个Lua脚本在执行期间,不会被其他客户端的命令打断。

特性

  • 轻量级:它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
  • 可扩展:Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。

lua语法教程

redis使用Lua脚本

Redis 脚本使用 Lua 解释器来执行脚本。 Redis 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为
EVAL
。redis Eval 命令基本语法如下:

redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...] 

参数说明:

  • script
    : 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数。
  • numkeys
    : 用于指定键名参数的个数。
  • key [key ...]
    : 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
  • arg [arg ...]
    : 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

Jedis实现接口限流

首先,我们定义一个lua脚本:

local key = KEYS[1];

local times = ARGV[1];

local expire = ARGV[2];

local afterval = redis.call('incr',key);
if afterval ==1 then
    redis.call('expire',key,tonumber(expire) )
    return 1;
end;

if afterval > tonumber(times) then
    return 0;
end

return 1;

这个脚本首先定义了三个成员变量用来获取方法中传入的值,redis.call()方法是redis的命令脚本执行,即redis执行incr操作,对key中存储的key值加1操作,如果afterval值等于1时,执行redis的expire,设置key的过期时间,tonumber是将参数值转换为数值,返回,如果加一后的值大于我们传入的规定值时,返回0,进行限流。

java代码实现限流

    public boolean acquire(String limitKey, int limit, int expire) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
        Long result = (Long) jedis.eval(redisScript.getScriptAsString(), 1, limitKey, String.valueOf(limit), String.valueOf(expire));
        if (result == 0){
            return false;
        }else {
        return true;
        }
    }

controller层调用

     if (isAcquire.acquire("myKey",10,60)){
          // 接口放行
           return "success";
       }else {
         // 接口拒绝
           return "err";
       }

这个方法是传入一个你的key,这个key下每一分钟只能请求10次,如果超出10次,进行限流操作,等到上次请求接口的时间超出1分钟后才可以进行放行操作。

END

一、背景介绍

Windows家庭版提示不支持远程桌面,如下图Windows11家庭版:

本文将介绍一种Windows家庭版开启远程桌面的方法,分为开启远程桌面设置、配置问题排查及解决:

  • 安装远程桌面补丁文件;
  • 手动开启“远程协助”,防火墙开放端口;
  • RDP Wrapper自救指南;
  • 远程桌面设置常见问题排查和解决。

二、安装补丁

1、下载补丁并解压,下载链接:
https://github.com/stascorp/rdpwrap

注意:用谷歌浏览器会提示危险文件,选择
“保留危险文件”
即可。如果大家不放心,请用杀软进行查杀。


2、选择
“install.bat”
右键,选择
“以管理员身份运行”
进行安装,安装成功则如下提示。

3、选择
“RDPConf.exe”
右键,选择
“以管理员身份运行”
查看相关信息,如下图则表示安装成功。

此时,就能远程控制该机器了!

三、手动开启“远程协助”,防火墙开放端口

1、点击“
Windows图标”
打开开始菜单,然后找到点击“
设置”
进入设置界面。

2、在
“系统”
中选择
“关于”

“系统信息”
,然后点击
“高级系统设置”
进入系统属性。

然后选择
“远程
”选项卡并勾选
“允许远程协助连接这台计算机”
,最后点击
“应用”
保存配置。

3、按
“Win+S”
键和输入
“控制面板”
进入控制面板界面,然后选择
“系统和安全 > Windows Defender 防火墙 > 允许应用或功能通过Windows Defender防火墙”
,最后确保远程协助已允许。

四、RDP Wrapper自救指南

当RDP Wrapper初始状态Service state为Stopped时,应该如何排查和解决呢?

1、关闭防火墙/网络防火墙、退出杀毒软件。

2、重启电脑,手动复制rdpwrap.dll至C:\Program Files\RDP Wrapper,卸载后重新install

结果:Wrapper ver正常出现

3、检查防火墙配置是否允许3389端口

4、uninstall RDP Wrapper,重新安装,就可以了:

5、最后记得重新启用网络防火墙:

五、远程桌面设置常见问题排查和解决

1、
rdpwrap
下载安装之后,安装目录
缺失rdpwrap.dll或 rdpwrap.ini 文件

2、已经显示绿色
fully supported
,但是无论如何操作 Listener state都是红色
Not Listening

解决方案(两种解决方案都可以试试):

  • 检查下
    C:\Program Files\RDP Wrapper\rdpwarp.ini
    文件,看是不是刚才生成的ini配置文件粘贴错了位置,不能粘贴在文件最底端,要求粘贴到同类型配置文件的最顶端。
  • 重启服务,发现启动不了,被篡改了注册表解决方法可参考
    https://blog.csdn.net/qq_35903121/article/details/104342391
    ,按照上面操作后,命令行起终端管理员,然后执行net start TermService命令即可。

3、RDP Wrapper其他都绿了,但是termsrv ver无法显示,support level也不为绿

解决方案:

  • 这种情况可能是软件界面显示问题,理论上已经可以被远程,可以电脑远程试一下。

4、RDP Wrapper都配置OK,刚开始试过可以远程,但过段时间再远程不了电脑

解决方案:

  • 记得去设置里调一下电源选项,要不然息屏后睡眠会断网。系统--电源--“插入电源时,在闲置以下时间后将设备置于睡眠状态”:选择【
    从不

5、配置成功后未重启,开启防火墙后就无法远程

解决方案:

  • 配置后开启防火墙,记得需重启电脑哦!

六、参考文献

  • https://github.com/stascorp/rdpwrap
  • https://github.com/stascorp/rdpwrap/issues/1883
  • https://github.com/sebaxakerhtc/rdpwrap.ini/tree/master/autogenerated
  • https://blog.csdn.net/qq_35903121/article/details/104342391