Coder Social home page Coder Social logo

blog's Introduction

  • 👋 Hi, I’m @Ruikuan
  • 👀 I’m interested in ...
  • 🌱 I’m currently learning ...
  • 💞️ I’m looking to collaborate on ...
  • 📫 How to reach me ...

blog's People

Contributors

ruikuan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

Forkers

lerry hitomr

blog's Issues

`ConditionalWeakTable<TKey,TValue>` 概括

详情参见 ConditionalWeakTable<TKey,TValue> Class

一个不持有对应 Key 强引用的类 Dictionary 数据结构,如果 Table 外的 Key 已经被干掉了,那么这个 Table 里的这行 Key 连带 Value 记录都会被删掉。Table 外的意思包括,即使 Table 里面存在指向 Key 的引用,也不会阻止删除行为。所以,这个数据结构的行为更像一个 Table,而不是 Dictionary。

这种特性可以用于给运行中的对象附加额外的属性。需要使用对象额外属性的时候,可以很轻松查找,而原对象已经不需要的时候,也不用费心去处理 Table 里面的数据。

Vue 和 jQuery datepicker 结合使用

最好的结合方式是将 datepicker 封装成vue的自定义指令使用,封装如下:

Vue.directive('datepicker', {
    twoWay: true,
    bind: function () {
        $(this.el).datepicker({
            dateFormat: "yy-mm-dd" 
        });
        $(this.el).change(function(){  // change vm value when datepicker pick a date
            this.set($(this.el).val());
        }.bind(this));
    },
    update: function (val) {
        $(this.el).datepicker('setDate', val);
    },
    unbind: function () {
        $(this.el).datepicker('destroy');
        $(this.el).off('change');
    }
});

使用方式如下:

<input type="text" class="form-control" v-datepicker="dateValue" />

其中dateValue为 data中的值或者v-for里面的元素的值都行。
也可以尝试使用组件的方式实现。

再发一个int类型的指令,和input结合使用

Vue.directive('int', {
    twoWay: true,
    bind: function () {
        this.handler = function () {
            let val = this.el.value;
            if (val === '') {
                val = '0';
                this.el.value = val;
            }
            let intVal = parseInt(val, 10);
            if (!isNaN(intVal)) {
                this.set(intVal);
            }

            let currentValue = this._watcher.get();
            this.el.value = currentValue;

        }.bind(this)
        this.el.addEventListener('input', this.handler)
    },
    update: function (val) {
        this.el.value = val;
    },
    unbind: function () {
        this.el.removeEventListener('input', this.handler)
    }
});

使用方式

<input type="text" class="form-control" v-int="value" />

.Net Core 2.1 性能改进

根据 Performance Improvements in .NET Core 2.1 一文总结。比较的对象为 .Net Core 2.0。

JIT

  • EqualityComparer<T>.Default.Equals 能被 intrinsic 识别,devirtualize 并 inline,带来 ~2.5x 的性能提升。
  • 受上面的影响,Dictionary<Tkey,TValue>ContainsValue 也提升了 ~2.25x;上面基本上给所有的集合类搜索都带来了可观的提升。
  • 需在代码中直接使用 EqualityComparer<T>.Default.Equals,而不是先将 EqualityComparer<T>.Default 放到一个变量,然后用那个变量的 Equal 方法,这种微优化由于 JIT 识别不出来,无法生成高效的代码从而变成了负优化
  • Enum.HasFlag 也成了 intrinsic,从原来的臭名昭著的反射实现变成了 JIT 直接位操作,性能提升 ~50x,并且不会再有 allocated。
  • JIT 将循环中的局部 return/break 移到热路径外,让循环体更加紧凑连续,不再需要使用 goto 等 trick 来优化,从而改善了代码的 layout,还改善了代码编写的 shape,并且性能将近 ~2x。
  • 某些情况下将 TEST 和 LSH 指令改为生成 BT 指令,~1.4x。
  • 识别类似 ((IAnimal)thing).MakeSound() 这样通过接口来调用 struct 方法的代码(其中 thing 为 struct),避免装箱和接口方法调用,直接调用成员方法,并且尽可能 inline。~10x 的性能提升,并且不会 allocated。
  • 所有的优化组合起来使用,对上层代码有显著的性能提升。

Threading

  • 优化了 threadstatic 的访问性能。
  • 改善了 Monitor 在存在争用情况下的开销。
  • 改善了 ReaderWriterLockSlim 的 scalability。
  • 将 Timer 的所有操作(create, modify, fire, remove)从竞争一个全局锁的串行改成多个锁,提高并行度。
  • CancellationTokenSource. CancellationToken 的关注重点从 scalability 更改为 throughput 和 allocation,实现了 ~1.7x 的性能提升和 0 分配。
  • await 同步返回的 async 方法的开销削减,性能提升 ~1.5x。如果使用 ValueTask 的话不会在堆上分配。即使不是使用 ValueTask,对于已经完成的任务,框架在很多情况下会使用已经缓存的同一个 Task<int/bool/etc..> 来避免分配
  • await 异步的 async 方法的分配从 4 个对象缩减为 1 个,从而将分配空间减少一半。(将其中 3 个从 class 变成了 struct,并使用 in 传参。)

String

  • 利用 Span<T>.SequenceEqual 来向量化 String.Equal,性能提升 ~1.6x。
  • 同样向量化 String.IndexOf String.LastIndexOf, ~2.7x。
  • String.IndexOfAny ~2.5x。
  • String.ToLowerToUpper(包括 ToLower/UpperInvariant)在需要转换时 ~3x;在不需要转换(即字符已经符合目标)时 ~2x,而且不再有 allocated。
  • 对于 String.Split,视字符串的长度或使用栈上分配或使用 ArrayPool<int>.Shared 来消除原先用来记录字符串分段的 Int32[] 分配。再辅以 span 的优势来改善常见情形的 throughput,性能提升 ~1.5x,分配只为原来的 1/4。
  • 即使边边角角的字符串方法都有改善,如 String.Concat(IEnumerable<char>) ~1.5x,分配为原来的 1/9。

Formatting and Parsing

  • string.Format 性能 ~1.3x,分配为 2/3。
  • StringBuilder.Append 性能 ~2x,分配为 0。
  • 将 coreclr 和 corert 中跟 string 和 span 相关的大部分代码用 C# 重写,以可以应用更好的优化,比原先 c++ 的实现相比有很大的性能改善。譬如 Int32.ToString() ~2x;int.Parse ~1.3x。
  • 同理,Int32, UInt32, Int64, UInt64, Single, DoubleToString() 也一样改进。尤其在 unix 有将近 10 倍的提升。
  • BigInteger.ToString ~12x!并且 allocated 只为原先的 1/17。
  • DateTimeDateTimeOffsetROToString,分别提升 ~4x 和 ~2.6x。
  • Convert.FromBase64String ~1.5x;Convert.FromBase64CharArray ~1.6x。

Networking

  • Dns.GetHostAddressAsync 在 windows 上变成了真·异步操作。
  • IPAddress.HostToNetworkOrder/NetworkToHostOrder ~8x。
  • Uri 的分配降低到原来的一半,初始化 ~1.5x。
  • Socket 收发 ~2x。
  • 消除大量 SslStream 中的分配,修正它在 unix 中的瓶颈。
  • HttpClient 现在使用完全 C# 编写的 SocketsHttpHandler,一个用 Socket SslStreamNetworkStream 做服务端,用 HttpClient 做客户端通过 https 访问的示例,获得了 12.7 倍的性能飙升!同时分配也大大减少,也没了 Gen1 对象。

And More

  • Directory.EnumerateFiles ~3x,allocated 1/2。
  • System.Security.Cryptography.Rfc2898DeriveBytes.GetBytes 由于 Span 的使用完全消除了计算过程中的分配。总的分配:1120120 B -> 176 B,性能有所提高。
  • Guid.NewGuid() 在 unix 上有 4 倍的提升。
  • 数组处理LINQEnvironmentcollectionglobalizationpoolingSqlClientStreamWriter 和 StreamReader 等都有很大的改进。
  • Regex.Compiled 回来了而且生效,Match 性能提高约一倍。

Chorme 和 asp.net core ajax with Credentials 跨域访问实现

一般来说,asp.net core 只要这样简单配置代码,就能和Edge配合的很好了,Edge默认就会使用对应目标站点的 Credentials

app.UseCors(options =>{options.AllowAnyOrigin());

但Chorme要求比较严格,首先需要在客户端配置 withCredentials

//vue resource 的配置,其他的同理
Vue.http.options.xhr = {withCredentials: true}

而且对服务器端的返回也有严格限制,当要带上Credentials时,不能允许 Origins 设置为 *,只能指定特定的Origins,
另外,服务器端也必须指定允许Credentials,因此需要将服务器端代码改为:

app.UseCors(options =>
{
    options.WithOrigins("http://localhost:8080"); //特定的源
    options.AllowCredentials();  //显式指定
});

重读《深入理解计算机系统》

信息表示和处理

  • 加减法比乘法快
  • 除法最慢,慢很多
  • 位移实现乘法快

处理器体系结构

  • 基于条件数据传送比基于条件控制转移性能高。因为流水线的执行方式,使用基于条件控制可能导致预测错误,导致指令数目飙高。而基于条件数据传送的方式不依赖条件数据,让流水线一直可以满载正常执行。但要考虑计算不同条件结果的成本,如果成本高就不划算了。

  • 64 位 cpu 能将调动过程信息和参数记录在寄存器中(比 x86 多了 8 个寄存器),不需要对这些信息读写栈,大大提高了性能。

  • 流水线的并行模式增加了系统的吞吐量,但也带来了少量延迟。若流水线深度过深,延迟增大,导致吞吐量可能下降。

  • 流水线的示例:一堆车经过洗车机,不同的步骤同时执行。

  • 组合电路显示出一种 React 响应式的特性。

优化程序性能

  • 消除循环的低效率。即尽可能将成本高的东西在循环外做,循环内只使用结果。

  • 减少过程调用,内联。过程调用需要储存返回地址、传递参数等开销,需要很多指令。

  • 消除不必要的存储器引用。使用局部值变量,以便让它能够存储到寄存器中,比访问内存快得多。使用指针和数组都会导致读内存。

  • 循环展开。将本来一个个轮着来的改成每次多个,减少次数。利用了流水线来处理展开的操作,提高了并行度,并且某种方面减少了操作之间的依赖性。

  • 提高并行性。通过更改多个累积变量的结合方式,减少依赖性,能提高流水线的并行处理效率。

  • 使用 SIMD,直接提高并行度。

  • 同上,可以的话使用基于条件数据传送的方式,而不是用基于条件控制的方式。

//基于条件控制转移的示例
void minmax1(int a[], int b[], int n)
{
    int i;
    for(i = 0; i < n; i++)
    {
        if (a[i] > b[i])
        {
            int t = a[i];
            a[i] = b[i];
            b[i] = t;
        }
    }
}
// 基于条件数据传送的实例
void minmax2(int a[], int b[], int n)
{
    int i;
    for (i = 0; i < n; i++)
    {
        int min = a[i] < b[i] ? a[i] : b[i];
        int max = a[i] < b[i] ? b[i] : a[i];
        a[i] = min;
        b[i] = max;
    }
}
  • 打开 -o2 或更高级别的 -o3 等优化,能够使用更适合的指令和代码结构改变。

  • 高级设计。考虑适当的算法和数据结构。

  • 基本编码原则:(上面提到的)

    • 消除连续的函数调用。有可能时,将计算移到循环外。考虑有选择地妥协程序的模块性以获得更大的效率。
    • 消除不必要的存储器引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。
  • 低级优化

    • 展开循环,降低开销,并且使得进一步的优化成为可能。
    • 通过使用例如多个累积变量和重新结合等技术,找到方法提高指令级并行。
    • 用功能的风格重写条件操作,使得编译采用条件数据传送。
  • 不要因为提高效率损害正确性,引入足够的测试。

  • 使用均匀的哈希函数

  • Amdahl 定律:加快系统的一部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少。

存储器层次结构

  • 局部性原理:倾向于引用邻近于其他最近引用过的数据项的数据项(空间局部性),或者最近引用过的数据项本身(时间局部性)。

  • 具有良好局部性的程序比局部性差的程序性能高。因为缓存是基于局部性的。

  • 步长为 1 的引用模式(顺序引用模式)比步长为 k 的好。在存储器中以大步长跳来跳去的程序空间局部性很差。

    • 大对象的数组访问不如小对象的数组访问好
    • 对基本数据对象的访问比对类对象的访问好
    • 值类型比引用类型好
  • 取指令局部性:循环体代码会被执行很多次,因此它具有良好的时间局部性。循环体越小,循环迭代次数越多,局部性就越好。

  • 由于层次结构中较低层次离 cpu 较远,访问慢,为了补偿这些较长的时间,倾向于使用较大的缓存块。

  • 冲突不命中对性能影响很大,因为每次访问都不命中,每次访问都让之前的缓存失效。这跟放置策略有关系,只要使用非随机的放置策略都会有冲突不命中的可能。但使用随机放置会导致定位代价高昂。在实际使用中,可以组织程序访问数据的方式,以避免冲突不命中。

    • 抖动:高速缓存反复地加载和驱逐相同的高速缓存块的组。
    • 冲突不命中在真实的程序中很常见,当程序访问大小为 2 的幂的数组时,直接映射高速缓存缓存中通常会发生冲突不命中。
    • 应对抖动的方法:在访问数组(或其他)的尾部加入某长度字节的填充,将需要读取的两份或多份对象挤到不同的缓存组中,这样就消除了冲突不命中。
//假设一个块是 16 字节(容纳 4 个浮点数),高速缓存有两组,整个大小为 32 字节

//会抖动的版本,虽然具有良好的局部性,但性能不高
float dotprod(float x[8],float y[8])
{
    float aum = 0.0;
    int i;
    for( i = 0; i < 8; i++)
        sum += x[i] * y[i];
    return sum;
}

//只需要将 x 定义为 float x[12],给它后面增加填充即可以避免这个冲突不命中 (p415)
  • .NET 的线程池代码貌似用了尾部工作偷取的方法来避免缓存失效,具体如何需要再去看看,到时候更新上来。

  • 写缓存

    • 直写:写入的内容写到高速缓存,同时立即写到下一级存储中。
    • 写回:将内容写入高速缓存,等高速缓存块被驱逐时再写回下一级存储。需要给每个高速缓存行增加个 dirty 标志。
  • 建议使用写回和写分配模型。

  • 使用分块技术提高局部性(p433)。

  • 推荐使用如下技术提高局部性:

    • 注意力集中在内循环上,大部分计算和存储器访问都发生在这里。
    • 通过按照数据对象存储在存储器中的顺序,以步长为 1 来读数据,从而使得程序中的空间局部性最大。
    • 一旦从存储器中读入了一个数据对象,就尽可能多使用它,从而使得程序中的时间局部性最大。
    • 对多维数组操作中,空间局部性尤为重要。

ffmpeg 转换视频格式

将 mkv 转成 mp4

ffmpeg -i LostInTranslation.mkv -codec copy LostInTranslation.mp4

由于这两个只是封装格式不同,编码可以不变。转换基本不需要花费什么 cpu 工作来转换编码,非常快。

转换成适合串流观看的 mp4

优化 MP4 视频以便更快的网络串流 所述:

MP4 文件由名为 "atoms" 的数据块构成 。有存储字幕或章节的 atoms ,同样也有存储视频和音频数据的 atoms 。至于视频和音频 atoms 处于哪个位置,以及如何播放视频诸如分辨率和帧速等,这些元数据信息都存储于一个名为 moov 的特殊 atom 之中。当你播放视频时,程序搜寻整个 MP4 文件,定位 moov atom,然后使用它找到视频和音频数据的开头位置,并开始播放。然而, atoms 可能以任何顺序存储,所以程序无法提前得知 moov atom 在文件的哪个位置。如果你已经拥有整个视频文件,搜寻并找到 moov atom 问题并不大。然而,当你还没有拿到整个视频文件(比如说你串流播放 HTML5 视频时),恐怕就希望可以有另外一种方式。而这,就是串流播放视频的关键点!你无需事先下载整个视频文件,就可以立即开始观看。

当串流播放时,你的浏览器会请求视频并接受文件头部,它会检查 moov atom 是否在文件开头。如果 moov atom 没有在文件开头,则它要么得下载整个文件并试图找到 moov ,要么下载视频文件的不同小块并寻找 moov atom,反复搜寻直到遍历整个视频文件。

搜寻 moov atom 的整个过程需要耗费时间和带宽。很不幸的是,在 moov 被定位之前的这段时间里,视频都不能开始播放。

因此需要对 mp4 文件进行优化,重组 atoms,使 moov atom 位于文件开头。可以使用如下命令。

ffmpeg -i source_file -movflags faststart -codec copy output.mp4

-movflags faststart 参数告诉 ffmpeg 重组 MP4 文件 atoms,将 moov 放到文件开头。

asp.net core 1.1 Razor taghelper intellisense 启用办法

从 taghelper 加入到 asp.net mvc 中,在 visualstudio 中编辑 view 时就支持 intellisense。例如,当你在一个 <a> 中输入 asp- 时,它会自动列出 a 对应的 taghelper 供你自动补全,是一个挺方便的功能。
taghelper intellisense

问题是自从开始进入 asp.net core 升级到 1.0 之后,这个 intellisense 就没有了,在 visualstudio 2015 update3 中没有,为这个我还升级到了 visualstudio 2017 RC3,还是没有,等 2017 正式版出来,也还是没有。翻看一下 taghelper github 的 issue,开发团队说还没有搞好,正在努力开发中。看来靠不住了。虽然没有它也能照常开发,事实上我项目中的绝大部分都是在没有的情况下开发出来的,但有这东西能省不少力气,而且一眼看过去就知道是不是拼写错了,符合 taghelper 的属性是另一种颜色。现在只能靠肉眼看,写错了不管编译还是运行都不会有什么错误,只是生成的 html 不是预想中的,这样就给找问题带来了麻烦。

vs2017 发布的时候,微软给了一个 walkthrough,说可以通过一个 Razor Language Extension 的扩展解决这个问题。但安装完之后,我的项目仍然没有 intellisense,这就奇怪了。

我另外新建一个 web 项目,在上面测试了下, taghelper 的 intellisense 工作的很不错!

没办法,我首先对照下两者的 csproj 文件,看看是不是当前项目是旧版本 xproj + project.json 升级上来的有什么问题。根据新项目对现有项目删掉一些项增加一些项之后,情况没有改善。

为什么呢?我将 cshtml 文件关闭,再打开,不行;关闭,重新编译,再打开,还是不行。我不禁陷入了深思。

这时候,我想起之前对 _ViewImports.cshtml 做了些更改,给它加了一句引入我自己自定义的 taghelper(其实到现在我还没有实现一个自定义的 taghelper 呢,只是事先占个坑)。

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyProject

是不是那个添加自己的 taghelper 语句有问题呢?毕竟我还没有 taghelper。将它删掉看看。

删掉之后,果然 taghelper 立刻正常了!终于可以又用上它的 intellisense 了。以后真加入自己的 taghelper 再说了。

Hyper-V OpenWRT 折腾小记

现在的路由器用的是 RouterOS 的 RB450g ,虽然稳定可靠,但不开源不支持定制,有很多功能不能够很好支持,所以想虚拟出一个 OpenWRT 来,用它实现一些别的功能,取长补短。其实用普通的 linux 应该也可以,但既然都是折腾,OpenWrt 体积小,需求低,直接折腾 OpenWRT 也没什么不好。

OpenWRT 对 Hyper-V 的支持不好,都是万恶的反微软浪潮搞的。而我对 linux 阵型一向不怎么懂,就在 让OpenWRT完美适应Hyper-V 求了一个 vhd 来开启了折腾之旅。建一个虚拟机,将 vhd 附加进去,就顺利跑起来了。

网络配置

虽然虚拟机跑了起来,但它默认的网络配置不适合我现在的网络,需要将它的 dhcp 关掉,然后它自己也要从 lan 里面的 dhcp 获取 ip,修改如下:

/etc/config/network

config interface 'loopback'
	option ifname 'lo'
	option proto 'static'
	option ipaddr '127.0.0.1'
	option netmask '255.0.0.0'

config interface 'lan'
	option type 'bridge'
	option proto 'dhcp'
	option ifname 'eth0'

config globals 'globals'
	option ula_prefix 'fdfa:2d07:0cfd::/48'

删掉了 wan 端口的配置,因为用不上

/etc/config/dhcp

config dnsmasq
	option domainneeded '1'
	option boguspriv '1'
	option localise_queries '1'
	option rebind_protection '1'
	option rebind_localhost '1'
	#option local '/lan/'
	option domain 'lan'
	option expandhosts '1'
	option readethers '1'
	option leasefile '/tmp/dhcp.leases'
	option resolvfile '/tmp/resolv.conf.auto'
	option localservice '1'

config dhcp 'lan'
	option interface 'lan'
	option start '100'
	option limit '150'
	option leasetime '12h'
	option dhcpv6 'server'
	option ra 'server'
	option ignore '1' 

config dhcp 'wan'
	option interface 'wan'
	option ignore '1'

config odhcpd 'odhcpd'
	option maindhcp '0'
	option leasefile '/tmp/hosts/odhcpd'
	option leasetrigger '/usr/sbin/odhcpd-update'

'/usr/sbin/odhcpd-update'

主要是用 option ignore '1' 将 dhcp 禁用

修改完之后,重启 network 和 dnsmasq

/etc/init.d/network reload
/etc/init.d/dnsmasq restart

这样 OpenWRT 就能正确获取到 ip,能通过 ssh 访问了。不过访问前先要设置密码:

passwd

配置安装包的源

通常需要安装 luci web 管理界面,还有其他杂七杂八的东西,用 opkg 安装比较方便。但这个版本基本上什么东西都安装不了,因为没有配置源。配置源的方式如下:

/etc/opkg/customfeeds.conf

# add your custom package feeds here
#
# src/gz example_feed_name http://www.example.com/path/to/files
src/gz custom_feed http://downloads.openwrt.org/chaos_calmer/15.05/x86/generic/packages/luci
src/gz packag_feed http://downloads.openwrt.org/chaos_calmer/15.05/x86/generic/packages/packages
src/gz basepa_feed http://downloads.openwrt.org/chaos_calmer/15.05/x86/generic/packages/base

然后就可以执行命令安装东西了。

opkg update
opkg install xxx

由于编译的内核版本跟官方版本不是完全一致,有时候有些东西提示依赖不对,装不上,这个时候就可以强制忽略依赖问题安装

opkg install xxx --force-depends

我安装了vsftpd luci 等,安装 vsftpd 可以很方便地操作 OpenWRT 里的文件,配置文件也可以先拖出来,用 vscode 等编辑了,再拖回去,比 vim 等爽太多。

安装二进制文件

直接将文件用 ftp 传到上面,敲命令运行就行,要自动运行就看下面一节。

配置自动启动

貌似跟普通的 linux 不一样,自动启动的东西都放在 /etc/init.d/ 下面,里面的脚本拉 vsftpd 下来瞧瞧对照着写就行。类似如下格式:

#运行优先级
START=50 

start() {
	service_start /usr/sbin/vsftpd
}

stop() {
	service_stop /usr/sbin/vsftpd
}

写好之后,将脚本文件放到目录下,执行如下命令将它设置成可执行脚本并设置自动启动。

chmod +x xxxApp
xxxApp enable

然后开机之后,这个 xxxApp 就自动运行了。

配置 OpenVPN

很失败,没配置成功。安装是没问题的,但这个版本好像少了 TUN/TAP 模块,导致运行 tap 模式的 OpenVPN 没法使用。对我就没有什么意义了。可能需要重新编译内核,但我翻不了墙也没什么办法编译了。

配置 dns

本来设想它作为 dns 服务器,然后它上层是 RouterOS 上的 dns 服务器的,但配置几次都失败,总是解析不到 RouterOS 上面设定的 dns,先放弃。
现在的方案是打算用 OpenWRT 做一个 dns 服务器,然后 RouterOS 将 server 指定为 OpenWRT。基本上是可行的。对 OpenWRT 的配置如下:

/etc/config/network

config interface 'lan'
	option type 'bridge'
	option proto 'static'
	option ipaddr '192.168.0.14' # OpenWRT 的 ip
	option netmask '255.255.255.0'
	option gateway '192.168.0.1'
	option ifname 'eth0'

将网络配置设置成固定的 IP,避免通过 dhcp 获取到 dns 服务器。

/etc/resolv.conf

nameserver 127.0.0.1

让可以解析本机的 hosts,可以在 /etc/hosts 设置静态的域名解析。

/etc/dnsmasq.conf

listen-address=127.0.0.1,192.168.0.14
server=8.8.8.8
server=4.4.4.4

让 dns 服务器服务本机和内网,设置外部解析 dns 服务器。

/etc/init.d/dnsmasq restart

重新启动服务即可。

RouterOS 方面的配置是去掉勾选 pppoe 的 peer dns servers,在 dns 里面设置 server 为 192.168.0.14,然后 RouterOS 就到 OpenWRT 解析 dns 了, RouterOS 自己设定的 static dns entities 也能继续生效。不过还有个问题就是, OpenWRT 设定的 srv 记录,通过 RouterOS 转一道就没办法解析到了。

参考

让OpenWRT完美适应Hyper-V
dnsmasq (简体中文)

通过 asp.net core 内置的代码实行反射调用/异步调用方法

在 asp.net core 中有一个通过反射的方式来调用方法或者异步方法的帮助类 ObjectMethodExecutor,asp.net core 就是通过它来调用 controlleraction 方法的,SignalR 也是利用它来调用 hub 方法。

代码在这里。为了避免后续更新将代码弄没了,复制一份到这里。

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace Microsoft.Extensions.Internal
{
    internal class ObjectMethodExecutor
    {
        private readonly object[] _parameterDefaultValues;
        private readonly MethodExecutorAsync _executorAsync;
        private readonly MethodExecutor _executor;

        private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor =
            typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[] {
                typeof(object),                 // customAwaitable
                typeof(Func<object, object>),   // getAwaiterMethod
                typeof(Func<object, bool>),     // isCompletedMethod
                typeof(Func<object, object>),   // getResultMethod
                typeof(Action<object, Action>), // onCompletedMethod
                typeof(Action<object, Action>)  // unsafeOnCompletedMethod
            });

        private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues)
        {
            if (methodInfo == null)
            {
                throw new ArgumentNullException(nameof(methodInfo));
            }

            MethodInfo = methodInfo;
            MethodParameters = methodInfo.GetParameters();
            TargetTypeInfo = targetTypeInfo;
            MethodReturnType = methodInfo.ReturnType;

            var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo);

            IsMethodAsync = isAwaitable;
            AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null;

            // Upstream code may prefer to use the sync-executor even for async methods, because if it knows
            // that the result is a specific Task<T> where T is known, then it can directly cast to that type
            // and await it without the extra heap allocations involved in the _executorAsync code path.
            _executor = GetExecutor(methodInfo, targetTypeInfo);

            if (IsMethodAsync)
            {
                _executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo);
            }

            _parameterDefaultValues = parameterDefaultValues;
        }

        private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, object[] parameters);

        private delegate object MethodExecutor(object target, object[] parameters);

        private delegate void VoidMethodExecutor(object target, object[] parameters);

        public MethodInfo MethodInfo { get; }

        public ParameterInfo[] MethodParameters { get; }

        public TypeInfo TargetTypeInfo { get; }

        public Type AsyncResultType { get; }

        // This field is made internal set because it is set in unit tests.
        public Type MethodReturnType { get; internal set; }

        public bool IsMethodAsync { get; }

        public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo)
        {
            return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null);
        }

        public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues)
        {
            if (parameterDefaultValues == null)
            {
                throw new ArgumentNullException(nameof(parameterDefaultValues));
            }

            return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues);
        }

        /// <summary>
        /// Executes the configured method on <paramref name="target"/>. This can be used whether or not
        /// the configured method is asynchronous.
        /// </summary>
        /// <remarks>
        /// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than
        /// ExecuteAsync if you know at compile time what the return type is, because then you can directly
        /// "await" that value (via a cast), and then the generated code will be able to reference the
        /// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated
        /// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at
        /// compile time what type it would be.
        /// </remarks>
        /// <param name="target">The object whose method is to be executed.</param>
        /// <param name="parameters">Parameters to pass to the method.</param>
        /// <returns>The method return value.</returns>
        public object Execute(object target, object[] parameters)
        {
            return _executor(target, parameters);
        }

        /// <summary>
        /// Executes the configured method on <paramref name="target"/>. This can only be used if the configured
        /// method is asynchronous.
        /// </summary>
        /// <remarks>
        /// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync,
        /// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations
        /// as compared with using Execute and then using "await" on the result value typecasted to the known
        /// awaitable type. The possible extra heap allocations are for:
        /// 
        /// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally
        ///    it's a reference type, and you normally create a new instance per call).
        /// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance
        ///    of it, and if it is, it will have to be boxed so the calling code can reference it as an object).
        /// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling
        ///    code doesn't know what type it's going to be).
        /// </remarks>
        /// <param name="target">The object whose method is to be executed.</param>
        /// <param name="parameters">Parameters to pass to the method.</param>
        /// <returns>An object that you can "await" to get the method return value.</returns>
        public ObjectMethodExecutorAwaitable ExecuteAsync(object target, object[] parameters)
        {
            return _executorAsync(target, parameters);
        }

        public object GetDefaultValueForParameter(int index)
        {
            if (_parameterDefaultValues == null)
            {
                throw new InvalidOperationException($"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied.");
            }

            if (index < 0 || index > MethodParameters.Length - 1)
            {
                throw new ArgumentOutOfRangeException(nameof(index));
            }

            return _parameterDefaultValues[index];
        }

        private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo)
        {
            // Parameters to executor
            var targetParameter = Expression.Parameter(typeof(object), "target");
            var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

            // Build parameter list
            var parameters = new List<Expression>();
            var paramInfos = methodInfo.GetParameters();
            for (int i = 0; i < paramInfos.Length; i++)
            {
                var paramInfo = paramInfos[i];
                var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

                // valueCast is "(Ti) parameters[i]"
                parameters.Add(valueCast);
            }

            // Call method
            var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
            var methodCall = Expression.Call(instanceCast, methodInfo, parameters);

            // methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)"
            // Create function
            if (methodCall.Type == typeof(void))
            {
                var lambda = Expression.Lambda<VoidMethodExecutor>(methodCall, targetParameter, parametersParameter);
                var voidExecutor = lambda.Compile();
                return WrapVoidMethod(voidExecutor);
            }
            else
            {
                // must coerce methodCall to match ActionExecutor signature
                var castMethodCall = Expression.Convert(methodCall, typeof(object));
                var lambda = Expression.Lambda<MethodExecutor>(castMethodCall, targetParameter, parametersParameter);
                return lambda.Compile();
            }
        }

        private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor)
        {
            return delegate (object target, object[] parameters)
            {
                executor(target, parameters);
                return null;
            };
        }

        private static MethodExecutorAsync GetExecutorAsync(
            MethodInfo methodInfo,
            TypeInfo targetTypeInfo,
            CoercedAwaitableInfo coercedAwaitableInfo)
        {
            // Parameters to executor
            var targetParameter = Expression.Parameter(typeof(object), "target");
            var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

            // Build parameter list
            var parameters = new List<Expression>();
            var paramInfos = methodInfo.GetParameters();
            for (int i = 0; i < paramInfos.Length; i++)
            {
                var paramInfo = paramInfos[i];
                var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

                // valueCast is "(Ti) parameters[i]"
                parameters.Add(valueCast);
            }

            // Call method
            var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
            var methodCall = Expression.Call(instanceCast, methodInfo, parameters);

            // Using the method return value, construct an ObjectMethodExecutorAwaitable based on
            // the info we have about its implementation of the awaitable pattern. Note that all
            // the funcs/actions we construct here are precompiled, so that only one instance of
            // each is preserved throughout the lifetime of the ObjectMethodExecutor.

            // var getAwaiterFunc = (object awaitable) =>
            //     (object)((CustomAwaitableType)awaitable).GetAwaiter();
            var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable");
            var awaitableInfo = coercedAwaitableInfo.AwaitableInfo;
            var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType;
            var getAwaiterFunc = Expression.Lambda<Func<object, object>>(
                Expression.Convert(
                    Expression.Call(
                        Expression.Convert(customAwaitableParam, postCoercionMethodReturnType),
                        awaitableInfo.GetAwaiterMethod),
                    typeof(object)),
                customAwaitableParam).Compile();

            // var isCompletedFunc = (object awaiter) =>
            //     ((CustomAwaiterType)awaiter).IsCompleted;
            var isCompletedParam = Expression.Parameter(typeof(object), "awaiter");
            var isCompletedFunc = Expression.Lambda<Func<object, bool>>(
                Expression.MakeMemberAccess(
                    Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType),
                    awaitableInfo.AwaiterIsCompletedProperty),
                isCompletedParam).Compile();

            var getResultParam = Expression.Parameter(typeof(object), "awaiter");
            Func<object, object> getResultFunc;
            if (awaitableInfo.ResultType == typeof(void))
            {
                // var getResultFunc = (object awaiter) =>
                // {
                //     ((CustomAwaiterType)awaiter).GetResult(); // We need to invoke this to surface any exceptions
                //     return (object)null;
                // };
                getResultFunc = Expression.Lambda<Func<object, object>>(
                    Expression.Block(
                        Expression.Call(
                            Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
                            awaitableInfo.AwaiterGetResultMethod),
                        Expression.Constant(null)
                    ),
                    getResultParam).Compile();
            }
            else
            {
                // var getResultFunc = (object awaiter) =>
                //     (object)((CustomAwaiterType)awaiter).GetResult();
                getResultFunc = Expression.Lambda<Func<object, object>>(
                    Expression.Convert(
                        Expression.Call(
                            Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
                            awaitableInfo.AwaiterGetResultMethod),
                        typeof(object)),
                    getResultParam).Compile();
            }

            // var onCompletedFunc = (object awaiter, Action continuation) => {
            //     ((CustomAwaiterType)awaiter).OnCompleted(continuation);
            // };
            var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
            var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
            var onCompletedFunc = Expression.Lambda<Action<object, Action>>(
                Expression.Call(
                    Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType),
                    awaitableInfo.AwaiterOnCompletedMethod,
                    onCompletedParam2),
                onCompletedParam1,
                onCompletedParam2).Compile();

            Action<object, Action> unsafeOnCompletedFunc = null;
            if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null)
            {
                // var unsafeOnCompletedFunc = (object awaiter, Action continuation) => {
                //     ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation);
                // };
                var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
                var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
                unsafeOnCompletedFunc = Expression.Lambda<Action<object, Action>>(
                    Expression.Call(
                        Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType),
                        awaitableInfo.AwaiterUnsafeOnCompletedMethod,
                        unsafeOnCompletedParam2),
                    unsafeOnCompletedParam1,
                    unsafeOnCompletedParam2).Compile();
            }

            // If we need to pass the method call result through a coercer function to get an
            // awaitable, then do so.
            var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion
                ? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall)
                : (Expression)methodCall;

            // return new ObjectMethodExecutorAwaitable(
            //     (object)coercedMethodCall,
            //     getAwaiterFunc,
            //     isCompletedFunc,
            //     getResultFunc,
            //     onCompletedFunc,
            //     unsafeOnCompletedFunc);
            var returnValueExpression = Expression.New(
                _objectMethodExecutorAwaitableConstructor,
                Expression.Convert(coercedMethodCall, typeof(object)),
                Expression.Constant(getAwaiterFunc),
                Expression.Constant(isCompletedFunc),
                Expression.Constant(getResultFunc),
                Expression.Constant(onCompletedFunc),
                Expression.Constant(unsafeOnCompletedFunc, typeof(Action<object, Action>)));

            var lambda = Expression.Lambda<MethodExecutorAsync>(returnValueExpression, targetParameter, parametersParameter);
            return lambda.Compile();
        }
    }
}

使用方式很简单,利用 ObjectMethodExecutor.Create 方法根据传入的 TypeInfoMethodInfo 创建实例 executor ,然后视情况调用 executorExecuteExecuteAsync 方法即可。如果在编译时就知道要调用的 MethodInfo 返回值是不是 awaitable 的,可以直接调用 Execute 方法,如果是异步的,就 await 它的返回值,这样可能能节省点 alloced。如果编译时不知道是否 awaitable,那就调用 ExecuteAsync 即可。注释里面写的都挺详细了。

一个云盘批量文件下载辅助小脚本

一个云盘批量文件下载辅助小脚本

生成批量列表

// ==UserScript==
// @name         云盘文件列表
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  不可细说!
// @author       fdk
// @match        http://pan.baidu.com/disk/home*
// @grant        none
// ==/UserScript==

(function() {
    //{access_token} 替换成自己的 token
    let prefix = 'https://www.baidupcs.com/rest/2.0/pcs/stream?method=download&access_token={access_token}&path=';
    let batchText = '';
    let parentPath = $('li[node-type="historylistmanager-history-list"]').children().last().attr('title').replace('全部文件','');
    $('.filename').each(function() {
        let t = $(this);
        let c = t.parent().parent().prev().attr('class');
        if(c.indexOf('dir') < 0) {
            let fullPath = prefix + encodeURIComponent(parentPath + '/' + t.attr('title'));
            batchText = batchText + fullPath  + '\n';
        }
    });
    let textarea = document.createElement('textarea');
    textarea.style.width = window.innerWidth + 'px';
    textarea.style.height = window.innerHeight + 'px';
    textarea.className = 'copyhere';
    textarea.innerHTML = batchText;
    $('.module-list').children().first().before(textarea);
})();

设置为 context-menu 执行脚本,在云盘页面左边或上边右键点击可执行。

恢复脚本

// ==UserScript==
// @name         去掉复制遮罩
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  hoho!
// @author       fdk
// @match        http://pan.baidu.com/disk/home*
// @grant        none
// ==/UserScript==

(function() {
    $('.copyhere').remove();
})();

同样设置成 context-menu 脚本

在 asp.net core 中通过非注入的方式获取 service

通常,在开发 asp.net core 应用的时候,会将需要依赖的 service 通过构造方法注入到对象中,这样在对象中就可以使用对应的服务了,类似如下的方式

public class ApiController : Controller
{
    private CommitService _commitService = null;
    private WorkContext _workContext = null;

    public ApiController(CommitService commitService, WorkContext workContext)
    {
        _commitService = commitService;
        _workContext = workContext;
    }
}

但有时候我们并不是通过处理 http 请求的方式调用代码,比如我们需要在 asp.net core 中执行一个后台任务,这个后台任务是运行在后台线程中或者通过一个 timer 来触发,触发的代码需要访问系统里面的各个 service,但并没有一个方便的依赖注入点,这个时候我们应该怎么获得已经注册了的 service 呢?因为依赖注入实在是方便,我们是不会愿意自己手工用 new 构造 service的。

在 dotnet core 中内置了一个 IServiceProvider 接口,asp.net core 就是使用它来获得注册服务的。IServiceProvider 接口如下:

public interface IServiceProvider
{
    /// <summary>Gets the service object of the specified type.</summary>
    /// <returns>A service object of type <paramref name="serviceType" />.-or- null if there is no service object of type <paramref name="serviceType" />.</returns>
    /// <param name="serviceType">An object that specifies the type of service object to get. </param>
    /// <filterpriority>2</filterpriority>
    object GetService(Type serviceType);
}

引用了 Microsoft.Extensions.DependencyInjection.Abstractions 程序集之后,还可以使用泛型的 GetService<T>(this IServiceProvider provider) 方法。

CommitService commitService = ServiceProvider.GetService<CommitService>();

所以问题就变成了,我们如何获得 ServiceProvider,然后通过它在适当的时候用它获得我们需要的服务。

在 asp.net core 中获取 ServiceProvider 的方式大概有如下这些方式:

1. 通过 IServiceCollection.BuildServiceProvider 方法获得 ServiceProvider

这个扩展方法在 Microsoft.Extensions.DependencyInjection 中。

serviceCollection 可以在 Startup.ConfigureServices 组装 service 的时候获得。

需要注意的是,BuildServiceProvider 生成的 provider 只能获取 build 之前注册的 service, 之后再注册到 serviceCollection 中的 service 是不能获取的。

2. 通过 IAppBuilder.ApplicationServices 获得 ServiceProvider

Appbuilder 可以在 Startup.Configure 的时候获得。

3. 通过 HttpContext.RequestServices 获得 ServiceProvider

这个没什么好说的,如果能拿到 HttpContext 对象,通常也能通过 Controller 的构造方法实现依赖注入了。

4. 通过 IWebHost.Services 获得 ServiceProvider

Program 类里面初始化完毕 Webhost 之后,可以通过它获得。

获得想要的 ServiceProvider 之后,我们可以把这个 provider 放到随便某个后台任务能够访问的地方,需要的时候就能 happy 地用它获得各种 service 了。

加密 SQLServer 数据库备份及还原

从 SQLServer 2014 开始,就提供了原生的数据库备份加密功能。使用它不算很复杂,

加密备份数据库

使用下面几个步骤就可以得到一个加密的数据库备份文件。详细参考微软的创建加密的备份文档

1. 首先创建一个 master 数据库的主密钥,数据库需要用它来加密等会儿创建的证书。

-- Creates a database master key.   
-- The key is encrypted using the password "<master key password>"  
USE master;  
GO  
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'masterKey123@';  
GO  

2. 然后创建一个备份证书,到时候备份数据库时可以选它来加密备份集。

USE master;  
GO  
CREATE CERTIFICATE MyBackupEncryptCert  
   WITH SUBJECT = '备份数据库加密证书';  
GO

整个证书包含公私钥。而且使用该证书加密备份的数据库必须使用该证书才能解密还原。如果只是还原到本机还好,如果需要还原数据库备份到其他机器,就需要将证书备份出来。证书备份详细可以参见备份证书文档

我们将公私钥都备份出来,备份的时候需要提供一个密码来加密私钥。

USE master;  
GO  
BACKUP CERTIFICATE MyBackupEncryptCert TO FILE = 'c:\keys\cert.cer'  
    WITH PRIVATE KEY ( FILE = 'c:\keys\cert.pvk' ,   
    ENCRYPTION BY PASSWORD = 'backupCer@Key1' );  
GO  

请将这个密码记下来,还原证书的时候需要提供同一个密码,否则证书还原不了,备份恢复不了就死翘翘了。

3. 有了证书,就可以执行加密备份了。

参考创建加密的备份文档,使用下面语句就可以了。关键是使用 ENCRYPTION 选项,选择加密算法和刚才创建好的证书。

BACKUP DATABASE [MyTestDB]  
TO DISK = N'C:\DbBak\MyTestDB.bak'  
WITH  
  COMPRESSION,  
  ENCRYPTION   
   (  
    ALGORITHM = AES_256,  
    SERVER CERTIFICATE = MyBackupEncryptCert  
   ),  
  STATS = 10  
GO  

还原加密数据库备份

如果是还原到从中备份的数据库本机,是非常简单的,使用数据库还原向导,跟原来没有加密的操作一模一样。你感觉不到备份集是已经被加密过的。

但如果要还原到其他机器的数据库,在你选择备份集之后,数据库读不出来备份集里面的内容,没办法顺利还原。那么就需要下面那样做。

1. 首先要在目标机器数据库上还原备份证书。

将证书备份出来的文件 cert.cer 和 cert.pvk 复制到目标机器的 c:\keys\ 目录,然后执行 CREATE CERTIFICATE 语句从文件中还原证书。需要备份证书时的密码。

USE master;  
GO  
CREATE CERTIFICATE MyBackupEncryptCert   
    FROM FILE = 'c:\keys\cert.cer'   
    WITH PRIVATE KEY (FILE = 'c:\keys\cert.pvk',   
    DECRYPTION BY PASSWORD = 'backupCer@Key1');  
GO   

执行语句时,如果该 master 数据库先前还没有 master 数据库主密码,就需要像上面第一步那样给它创建一个。
然后我们在目标机器数据库上有了能解密的证书。

2. 还原数据库

然后就很简单,执行数据库还原向导,跟没有加密一样还原就行了。

要注意的点

  • 注意备份加密证书
  • 要记住备份加密证书的密码
  • 记住证书的名称

Midori 学习片段

Asynchronous Everything

但我想说最主要的指示是避免像瘟疫一样的大量分配对象,即使是短生命周期的那些。早期有一个关于 .NET 的箴言:Gen0 回收是免费的。不幸的是,用这搞出来了大量的库的这句,完全是废话。Gen0 回收导致暂停,
污染缓存( CPU Cache),还给高并行的系统带来频率节拍问题。

第一个关键的优化,当然是,如果一个 async 方法不进行 await ,就不应该为它分配任何对象。

(吐槽)我们给 .NET 分享了我们的经验,为实现 C# 的 await 时。可惜的是,那时候, .NET 的 Task 已经实现成类( class )了。因为 .NET 需要 async 方法返回 Task 类型,所以做不到零分配,除非你自己山寨一种模式,如缓存一个 singleton Task 对象。

多边形中心计算

有时候显示一个区域时,需要在多边形区域的中心显示一个标签或者说明,这个中心通常需要根据多边形的顶点来计算出来,而且需要比较符合中心的感觉。

最简单的方法是计算所有点的坐标的平均。

function getCenter(points) {
    let len = points.length
    let lat = 0.0;
    let lng = 0.0;
    for (let l of points) {
        lat += (l.latitude / len)
        lng += (l.longitude / len)
    }
    return { latitude: lat, longitude: lng }
}

存在的问题是如果多个点密集在某一个地方(比如那个地方的线条很复杂,所以需要很多点才能表达出来),那么导致这个平均点就格外偏向那个地方,一点也没有 core 的感觉。

还有也是比较粗略的方法是找出所有点中的 min(x),min(y),max(x),max(y),然后计算这些边缘的中点,大体可以得到一个很粗略的中心点。对于形状不规则比较厉害的形状,也难尽人意。

更靠谱的方法是计算多边形的重心,重心在均匀的密度情况下就是几何中心(我们显示的区域当然是均匀的),通常都符合人们对中心的期望。计算如下:

function getCenterOfGravityPoint(points) {
  let area = 0.0;//面积
  let lat = 0.0, lng = 0.0;// 重心的 x、y
  for (let i = 1; i <= points.length; i++) {
    let iLat = points[i % points.length].latitude;
    let iLng = points[i % points.length].longitude;
    let nextLat = points[i - 1].latitude;
    let nextLng = points[i - 1].longitude;
    let temp = (iLat * nextLng - iLng * nextLat) / 2.0;
    area += temp;
    lat += temp * (iLat + nextLat) / 3.0;
    lng += temp * (iLng + nextLng) / 3.0;
  }
  lat = lat / area;
  lng = lng / area;
  return { latitude: lat, longitude: lng };
}

这样得出的中心即使在很不规则的多边形情况下,看起来也自然多了。

参考
几何中心 - 维基百科,自由的百科全书
[几何]计算不规则多边形的面积、中心、重心

使用 MagickImage.NET 的一些坑

MagickImage.NET 是一个图片处理库,可以用于转换图片格式、改变图片大小等。在 .net framework 和 dotnet core 上都可以跑。我主要用它来生成缩略图。生成缩略图的代码很简单,如下:

using (MagickImage image = new MagickImage(sourceFile))
{
    image.Thumbnail(800, 800); // 按照比例生成宽高都不会超过 800 的缩略图
    image.Write(targetFile);
}

不过还是遇到了一些坑。

光跑 CPU 不干活

第一次运行代码 demo 时,cpu 飙到 100%,风扇狂转,然而代码就是跑不过 image.Thumbnail(800, 800) 那行,感觉是挂在了那里,但它又没有抛异常。我担心哪里出了问题,赶紧把进程 kill 掉了。重复几次,都是同样的表现。心里想,这玩意真不靠谱!

后来又硬着头皮跑了一两分钟没 kill 进程,看它会玩出什么幺蛾子。嘿!结果烤完两分钟之后,它就正常了,缩略图也顺利生成了。之后再跑,也不会再烧 cpu 了,每次都是立刻就生成缩略图。

最后在它的 github 里得到了回应,说它是能用 OpenGL 来处理图片的,每台机器第一次跑的时候,会先进行基准测试,看到底是 GPU 快还是 CPU 快,然后选择 GPU 还是 CPU 来处理图片。如果嫌这样烦,直接通过 OpenCL.IsEnabled = false; 将 OpenGL 处理禁用就可以了。

奇怪的全黑缩略图

然后在使用过程中,又出现了诡异的问题。

生成一批图片的缩略图时,第一张生成的缩略图永远都是全黑的,而且只有 4KB 多点的大小,而之外的其他图片都正常。而且如果将缩略图的大小改成 900,它又没有问题了,但 700/750 也会随机出问题。真是百思不得其解。

而且这个问题只会出现在上面这种代码里,如果使用下面的代码,也不会有问题。但这样的代码跑起来慢得离谱。

using (MagickImage image = new MagickImage(sourceFile))
{
    MagickGeometry size = new MagickGeometry(800, 800);
    size.IgnoreAspectRatio = false;
    image.Resize(size);
    image.Write(targetFile);
}

在这个诡异的问题上消磨了半天,忽然灵机一动,会不会是因为使用 GPU 来处理图片的问题?在我印象中,之前 GPU 某些时候并不是太稳定,偶尔出现花屏,黑屏之类的问题。嗯,Microsoft Edge 就经常崩……

然后按照上面的方法禁用了 OpenGL 之后,问题就不再出现了,代码跑得很欢快,腰不酸了,腿不疼了,一口气搞了好多张。正好这些代码是要部署到 linux server 上面的,上面根本没有什么鬼的 GPU。

结论

结论就是要想用得宽心点,还是不要用它的 OpenGL 了……

RouterOS 中的 NAT

前几天琢磨 如何将外网请求重定向到内网 时碰到了不少困难,还是自己对 NAT 了解得太少导致的。这两天认真学习了 RouterOS 里面的 NAT 知识,将我对它的理解写在这里。

Destination NAT | DST-NAT

dst-nat 是重写 ip packet 的 dst-address 和 dst-port 的操作。
进入路由器的包,使用这个操作,可以改写包的目标地址和端口,改变包的路由方向。

假如有一个 ip 包如下:

Address Port
Source 192.168.0.23 6789
Destination 8.8.8.8 80

对这个包应用 dst-nat,将它的目标地址改为 192.168.0.14,目标端口改为 8080

add chain=dstnat dst-address=8.8.8.8 protocol=tcp dst-port=80 \
  action=dst-nat to-address=192.168.0.14 to-port=8080

经过 dst-nat 之后,这个包就变成了

Address Port
Source 192.168.0.23 6789
Destination 192.168.0.14 8080

拥有 IP 192.168.0.14 的设备将会收到这个包。

为了能够应对将来到来的回复包,RouterOS 会记录这次 dst-nat 对应的转换映射如下:

Original Map
Address 8.8.8.8 192.168.0.14
Port 80 8080

这样,当回复包经过路由器的时候,路由器会检查回复包的 Source Address 和 Port,有对应映射记录的话,会将 Source Address 和 Port 改变为映射前的地址和端口。
(注意:这里暂不考虑回复包不经过路由器的情况,我们假设所有的包都经过路由器。)

回复包

Address Port
Source 192.168.0.14 8080
Destination 192.168.0.23 6789
被根据映射端口记录修改源地址和端口,变为:
Address Port
Source 8.8.8.8 80
Destination 192.168.0.23 6789

这样,发送端就能顺利收到回复包,两端能够进行顺利的信息交换了。

这个过程如图所示:
dst-nat

Source NAT | SRC-NAT

src-nat 是对即将离开路由器的包,重写包的源地址和源端口的操作。

通常在家庭网络中,路由器对外只有一个 IP,假设这个 IP 为 7.7.7.7,而路由器内网的设备有很多,有多个内网 IP,有个类似 192.168.0.0/24 这样的网段。内网的设备 A 要和外网的服务器进行信息交换,只能够借助路由器的外网 IP 来进行,因为外部网络并不知道如何将目标地址为 192.168.0.1 的包发送到何处,这个包自然没办法回到目标设备 A 了。
因此,需要使用 src-nat 操作对内网准备出外网的包进行源地址重写,将源地址重写为 7.7.7.7,这样外部网络回复包的时候,会顺利将包发送到拥有公网 IP 7.7.7.7 的路由器上,路由器再根据之前记录的映射,将回复包的目标地址设置为内网的某 192.168.0.x IP,将它传递到目标设备上。

假如路由器收到一个包如下:

Address Port
Source 192.168.0.23 6789
Destination 8.8.8.8 80

为了能顺利收到服务器 8.8.8.8 的回复包,路由器(公网 IP 为 7.7.7.7)对它应用 src-nat:

add chain=srcnat src-address=192.168.0.0/24 \
  out-interface=wan action=src-nat to-address=7.7.7.7

即对所有经过 wan 端口出去互联网的包,都将它的源地址改为 7.7.7.7,端口不设置,让路由器随机选择一个端口,假设随机选择了 9876。经过转换,包变成了:

Address Port
Source 7.7.7.7 9876
Destination 8.8.8.8 80

路由器记录这次转换映射如下:

Original Map
Address 192.168.0.23 7.7.7.7
Port 6789 9876

服务器 8.8.8.8 对该包进行回复,回复包的目标地址是 7.7.7.7,这样路由器就能顺利收到这个回复包了。路由器再根据原先的映射记录将包的目标地址转化为 192.168.0.23,目标端口转化为 6789,就不再赘述了。

除了从 WAN 出去的包,从 Lan 口出去的包也可以改写,其实从任何一个口出去的包都可以做 src-nat,不过为了保证包能够回到路由器,从 lan 口出去的包需要设置 to-address 为路由器的内网 IP,如 192.168.0.1 这样。

整个过程如图所示:
src-nat

Masquerade

masquerade 属于 src-nat 的一种,是 src-nat 的特殊转化,它的不同在于,它会根据包出去的 interface,自动将包源地址转化为对应 interface 所属网络的路由器 IP,端口随机,也即 masquerade 的转换是智能化设置的,起到一个简化设置的作用。其他功能和处理方式跟 src-nat 是一样的。

参考

Manual:Packet Flow
Manual:IP/Firewall/NAT
Hairpin NAT
Linux NAT基本流程与实现技巧 写文章的时候并没有参考这篇文章,不过后来看了觉得内容很有帮助,加到这里方便以后查阅。

不要使用 struct 默认的 GetHashCode

经常有人在 Dictionary<TKey,TValue>HashSet<T> 中误用没有自定义 GetHashCodestruct 做 key,出现性能问题。这是默认的 ValueTypeGetHashCode 实现存在的问题造成的。

GetHashCode 有个原则是:对于所有 Equals 的对象,它们的 GetHashCode 应该返回相等的值。但反之则没有要求,也就是并没有要求不一样的对象一定要返回不同的 code,这在物理上是不可能做到的,因为 int 的范围相当有限。

CLR 中对于 structGetHashCode 的默认实现为,先检查 struct 的所有字段,分两种情况处理:

  • 对于所有字段都是值类型,而且中间没有间隙的(由于 struct 的对齐布局,字段中间可能产生间隙,像 {bool,int} 这样的结构就会在 boolint 之间产生 3 个字节的间隙),则 CLR 会对其中所有的值的每 32 位做 XOR,从而生成 hashcode。这种生成的方式会利用到所有的字段内容,因为属于比较“好”的 hashcode。不过这里也存在一个鲜为人知的 bug:如果 struct 包含 System.Decimal,由于 System.Decimal 的字节并不代表它的值,所以对于相同值的 decimal,可能生成的 hashcode 并不一样。只是 struct 包含 'decimal' 的情况,而不是 decimal 自身的情况
struct Test { public decimal value; }

static void Main() {
    var t1 = new Test() { value = 1.0m };
    var t2 = new Test() { value = 1.00m };
    if (t1.GetHashCode() != t2.GetHashCode())
        Console.WriteLine("gack!");
}
  • 对于字段中有引用类型,或者字段中间有间隙的,CLR 会枚举 struct 的所有字段,选择 第一个 可用于生成 hashcode 的字段(值类型字段或者非空的引用类型),使用它的 GetHashCode 方法获得它的 hashcode,再跟 structMethodTable 指针做 XOR,最终的结果就作为了整个 structhashcode。对于这种情况,因为它只选取了第一个字段来生成 hashcode,所以其他字段的内容跟生成的 hashcode 没有关系,即使这些内容完全不一样,生成的 hashcode 都是一样的,这样就造成了大量的 hashcode 冲突,从而严重影响性能。
struct Test
{
    public int i;
    public string s; //不管 s 的内容是什么都影响不了 hashcode
}

结论

由于 struct 的 layout 经常不可控(调换下字段的顺序就可能造成影响),以及情况 2 的存在,使用默认的 GetHashCode 实现是很不明智的。对于自定义的 struct,绝不要使用默认实现,应该自定义靠谱的 GetHashCode

番外

对于 Enum,调用它的 Equals 会导致装箱(之前版本的 clr 调用 GetHashCode 也会,目前版本的改过来了)。为了避免这种情况,请调用 EqualityComparer<Enum>.Default.Equals(enum1, enum2)

关于 ValueTask

C# 7 中引入了 ValueTask。(这句话不够严谨)目的是为了解决一些情形下 Task 会造成过多内存分配的问题。

众所周知,Task 是个 class,每次返回一个 Task 都会分配一个新的对象,在内存中积累起来,造成 gc 压力。如果它对应的是 IO 任务那还好,IO 导致的延迟和开销可能远超这个小小的内存分配,负面影响不显。但如果只是由于 API 设计的限制,需要使用类似 Task.FromResult 直接返回一个值,那就很浪费了。而且在某些类似缓存的场合,第一次操作是大开销的 IO 操作,之后的所有访问都是直接从内存中返回,这样也很浪费。特别是在循环里面 await 这个 Task。

为了某种程度上弥补这种负面影响,引入了 ValueTask。ValueTask 是一个 struct,所以通常它自己是不会在堆里面分配东西的。它很符合上述的缓存模型的使用方式,有两个构造函数,一个是 ValueTask(TResult result),直接通过传入结果构造;另一个是 ValueTask(Task task),指向了另一个 Task,如果调用了这个构造函数,无疑还是会在堆里分配这个 Task 的。

微软文档里面推荐的使用方式如下。感觉也是最适合它的使用方式:

public ValueTask<int> CachedFunc()
{
    return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(LoadCache());
}
private bool cache = false;
private int cacheResult;
private async Task<int> LoadCache()
{
    // simulate async work:
    await Task.Delay(100);
    cacheResult = 100;
    cache = true;
    return cacheResult;
}

主要优点

对于能够直接同步返回值的操作,直接返回 new ValueTask(result) 是非常好的,它将 result 直接返回,并不会导致在堆里分配内存。频繁调用也不会有什么副作用。

保持兼容性,如果是执行异步操作,那么就直接返回底下的 Task,操作方式跟原来的一样。当然,分配就无可避免了。

主要缺点

如果操作都是异步操作,那么就相当于给 Task 多加了一些开销,也避免不了分配。

ValueTask 不如 Task 那么直接。

由于 ValueTask 包含了 ResultValue 字段以及 Task 字段,所以 await 生成的状态机会更加复杂,导致性能可能下降。

所以 ValueTask 也不是万能药,需要综合多种因素(同步异步操作的比例,操作耗时等)来进行取舍,通常需要进行测试来验证是否值得将 Task 替换为 ValueTask。

使用 Proxy Object 的灵异现象和弱智原因

在使用 Vue 开发项目的过程中,一直伴随着一个诡异的问题。应用能在 Edge 上顺利运行,一直没有什么问题,但当跑在 Chorme 上时,开始一段时间可以正常运行,但操作两三次之后就会出错,界面所有操作都失去了程序响应。
说是失去程序响应,是因为界面对各种输入还是响应的,只是输入完全不能改变程序的状态,比如 input 里面输入什么内容,是没办法更改 Vue 里面的 data 的,正常情况下它们互相绑定,数据会随着输入更改。

查看 console,Chorme 出来个奇怪的提示:

Error:'set' on proxy: trap returned falsish for property.

查了一下 google,不能其解。

为了跟踪对象的更改,以设置某个状态位供自己使用,我的确对 object 进行了一层 proxy 包装,更改了它的 set 行为。一直以来在 Edge 下测试,调试,都完全没有问题,所以我一直没觉得自己的 proxy 有什么问题。
而 Vue 貌似也是用 proxy 来跟踪数据更改的,会不会是它 proxy 了我 proxy 的 object, 所以出了问题? 跟踪的数据不能进行自己的 proxy? 用 Vue + 上面那句 Error 搜索,也搜不出任何有用的信息。

是不是只有在 Chorme 上才那么脆弱?怀着这样的念头,我打开了长久闲置的 Firefox。
Firefox的表现跟 Chorme 一致,前几次操作都没有问题,后面就变僵尸了。不过它的 console 输出比较靠谱:

Error:'set' on proxy: trap returned false for property.

不像 Chorme 那样搞个错别字。

根据多数干掉少数原则,看来有问题的是 Edge,还有我的程序。这次我细细跟搜索结果看了下 proxy 相关的内容,关键是 MDN 的 handler.set() 说明 这个页面,里面有明确说明:

The set method should return a boolean value. Return true to indicate that assignment succeeded. If the set method returns false, and the assignment happened in strict-mode code, a TypeError will be thrown.

原来 set 无论如何都是要返回一个 boolean 的,而我的 proxy 知识是在 Learn ES2015 · Babel 里面快餐式速成的,里面根本没提到返回值这单事,所以我直接就在 set 里面搞完自己的事情就 OK 了,什么返回值都没有给。所以 proxy 的行为算是未定义行为,极可能出问题。诡异的是浏览器并不会一开始就报错,前几次操作成功,后面就出错。

找到原因,问题就容易解决了,我直接在自己的 set 方法里面最后一句加上 return true, 然后什么问题都解决了,三个浏览器都笑了。

综合这段时间的调试,可以看出, Edge 是限制最宽松的,无论是 cors 的凭据控制,还是对 proxy 行为的默许,它都是很随便的就让人通过了。
Chorme 就严格得多,不过它这种随机性出错也让人抓狂,还有那个错别字是什么鬼?

以后快餐式看完之后,还得找靠谱的地方好好补补课啊。

使用 ffmpeg 将 rtmp 流保存为 mp4 文件

在 Windows 下使用 ffmpeg, 将录制的视频按照日期命名,命令如下

set dt=%date:~0,4%%date:~5,2%%date:~8,2%
ffmpeg.exe -i "rtmp://live" -b:v 900k -vcodec libx264 -acodec aac -b:a 256k -strict -2 -t 2700 %dt%.mp4

ffmpeg 的参数太复杂,我还没有完全弄清,这里 -b:v 是视频的码率,-b:a 是音频的码率,-t 设定的是录制时间。上面命令使用的是软件编码 (libx264) 的方式,cpu 利用率比较高。

机器上使用的是 Nvidia 的显卡,ffmpeg 可以使用 Nvidia 显卡硬编码 (h264_nvenc),几乎不会占用 cpu

ffmpeg.exe -i "rtmp://live" -b:v 900k -vcodec h264_nvenc -acodec aac -b:a 256k -strict -2 output.mp4

如果想要编码的视频质量更高,可以使用另外的编码参数,如下:

ffmpeg.exe -i "rtmp://live" -b:v 900k -vcodec h264_nvenc -profile:v high -acodec aac -b:a 256k -strict -2 output.mp4

也可以加大码率,或者干脆去掉 -b:v 码率,让它按照源的码率来工作。这主要是文件体积和视频质量的权衡。

ffmpeg.exe -i "rtmp://live" -c:v h264_nvenc -profile:v high -acodec aac -b:a 256k -strict -2 output.mp4

如果不需要重新编码,只需要将流的内容记录成文件,即可以使使用如下脚本。这个脚本录出来的文件体积相当大

ffmpeg.exe -i "rtmp://live" -acodec copy -vcodec copy -f flv -y output.flv

将这个写成 bat,放计划任务跑,就可以每天定时录制电视剧节目了。

用 RouterOS 将访问外网某端口/地址的请求重定向到内网机器

为了将访问外网端口(假设是 1688)的所有请求都重定向到内网的某台机器,今天足足折腾了一整天。所幸终于搞定了。

目标

假设外网有多个服务,都运行在端口 1688 上,而我内网能够模拟这个服务,运行在 192.168.0.14 上,端口也是 1688,那么我希望所有访问外网服务的请求都重定向回内网,提高访问速度,应该怎么做呢?

目标

步骤

  1. 要重定向访问,首先得将请求包的 dst-address 重写为 192.168.0.14。使用 dst-nat 就可以重写包的目标地址
/ip firewall nat
add chain=dstnat dst-address=!192.168.0.0/24 protocol=tcp dst-port=1688 \
  action=dst-nat to-address=192.168.0.14

包改变情况如下:

192.168.0.2 > 8.8.8.8   --->  192.168.0.2 > 192.168.0.14

8.8.8.8 的目标地址信息丢失了。 (并没有丢失,信息还保存在路由器中,这是之前的误解。)

  1. 重写目标地址之后,笔记本发的包能顺利到达 0.14 了,但这里有个问题,就是 0.14 回应的包直接发给 0.2 了,因为它们在同一个内网,包不需要经过路由器。但 0.2 并不知道它的包已经给了 0.14,它还在等待 8.8.8.8 的包。当 0.14 的包到了之后,不是它等待的包,直接就被它抛弃了。
192.168.0.14 > 192.168.0.2 ---> 废弃   #此路不通
  1. 怎么办呢?为了能够让回复的包的源地址能顺利写回 8.8.8.8,就要将出口到 lan 的包也做一次 masquerade/srcnat ,这样它在离开路由器去 0.14 的时候,源地址会变成 0.1,回来的包也会经过路由,再由之前保存的连接信息将源地址变回 8.8.8.8
/ip firewall nat
add chain=srcnat src-address=192.168.0.0/24 \
  dst-address=192.168.0.14 protocol=tcp dst-port=1688 \
  out-interface=lan action=masquerade

包改变情况如下:

192.168.0.2 > 8.8.8.8   --->  192.168.0.1 > 192.168.0.14
192.168.0.14 > 192.168.0.1   --->  8.8.8.8 > 192.168.0.2

这样包就能够顺利来回了。

参考

Hairpin NAT

in 修饰符和 readonly struct 以及伴随的性能影响

值类型的 readonly 字段和性能影响

readonly 修饰符在值类型和引用类型之间的表现有点不同。对于引用类型的 readonly 字段,编译器只保证它在构造方法之外不能重新指定,即不能再通过 a = xxx 来重新设定引用,不会管它内部进行了什么改变。而对于值类型的 readonly 字段,则意味着在实例的整个生命周期中,所有它的内部值都不会变化。为了避免潜在的变化,对于 readonly 的值类型字段,编译器每次调用方法或属性之前都会进行防御性复制。防御性复制带来可观的性能开销。

private FairlyLargeStruct _nonReadOnlyStruct = new FairlyLargeStruct(42);
private readonly FairlyLargeStruct _readOnlyStruct = new FairlyLargeStruct(42);
private readonly int[] _data = Enumerable.Range(1, 100_000).ToArray();
        
[Benchmark]
public int AggregateForNonReadOnlyField()
{
    int result = 0;
    foreach (int n in _data)
        result += n + _nonReadOnlyStruct.N;
    return result;
}
 
[Benchmark]
public int AggregateForReadOnlyField()
{
    int result = 0;
    foreach (int n in _data)
        result += n + _readOnlyStruct.N;
    return result;
}
                       Method |      Mean |    Error |    StdDev |
----------------------------- |----------:|---------:|----------:|
 AggregateForNonReadOnlyField |  87.92 us | 1.800 us |  3.677 us |
    AggregateForReadOnlyField | 148.29 us | 4.226 us | 12.460 us |

仅仅多了一个 readonly 修饰符就造成了大量的性能损失。

解决这个问题至少有三个办法:

  1. 使用字段而不是使用属性。编译器看到只是读取 struct 字段的操作时,知道不会有副作用,因此不会进行防御性复制。但这个对封装不好。
  2. 不要使用 readonly 修饰值类型。
  3. 使用 readonly struct
public readonly struct FairlyLargeStruct
{
    private readonly long l1, l2, l3, l4;
    public int N { get; }
    public FairlyLargeStruct(int n) : this() => N = n;
}

readonly struct

C# 7.2 允许通过 readonly struct 表明值类型的 immutable,不但对性能有好处,而且能够更明确表示一种不可变的观点:值是 immutable 的。(不过可以通过某些肮脏的反射操作来破坏。)

readonly struct 强制如下行为:

  1. 编译器检查 struct 是真的不可变并且只由 readonly 的字段和/或只读的属性。(像 public int Foo {get; private set;} 这种就不是只读的)
  2. 允许编译器在某些上下文省略防御性复制,像上面提到的 readonly 值类型字段之类的情况。

对于 readonly struct FairlyLargeStruct 的基准结果如下:

                       Method |     Mean |    Error |   StdDev |
----------------------------- |---------:|---------:|---------:|
 AggregateForNonReadOnlyField | 91.19 us | 1.811 us | 2.597 us |
    AggregateForReadOnlyField | 89.25 us | 1.775 us | 3.705 us |

in 修饰符

之前 C# 有三种传参方式:值传递,传引用(ref),输出参数(out),实际上在内部 ref 和 out 是一样的。

C# 7.2 带来了新的传参方式:in 修饰符。in 的语义是只读的引用,在底下,参数被当作用System.Runtime.CompilerServices.IsReadOnlyAttribute修饰的引用传递。编译器确保在方法中不会修改这个参数,而且对于 in 修饰的 struct,编译器还保证不能向它的字段赋值,也就是说 in 的只读修饰对于 struct 的影响是深度的。

public void Foo(in string s)
{
    // Cannot assign to variable 'in string' because it is a readonly variable
    s = string.Empty;
}
  1. 不能用重载区分 inrefout,它们本质上是一样的

  2. 不能将这三个用于迭代器和 async 方法

  3. 可以将 using 块的变量通过 in 传递,即使不能通过 refout 传递。因为通过 in 传递是安全的,编译器去除了这个限制。

struct Disposable : IDisposable
{
    public void Dispose() { }
}
 
public void DisposableSample()
{
using (var d = new Disposable())
{
    // Ok
    ByIn(d);
    // Cannot use 'd' as a ref or out value because it is a 'using variable'
    //ByRef(ref d);
}
 
void ByRef(ref Disposable disposable) { }
void ByIn(in Disposable disposable) { }
  1. in 参数可以有默认值,refout 不行。
public int ByIn(in string s = "") => s.Length;
  1. 可以进行只有 in 修饰符的不同的重载。
public int Foo(in string s) => s.Length;
public int Foo(string s) => s.Length;

在有这样重载实现的时候,在 C# 7.2 中 Foo(s) 调用的是 Foo(in string s),而在 C# 7.3 之后,调用的是 Foo(string s),看起来是 C# 7.2 的实现存在语义上的问题。

但对于不是重载的情况,只有 in 实现的情况下,对于调用方,in 参数的 in 修饰符是可选的,因为它对于调用方的语义保证是不变的。这样对于 API 的提供方很方便,可以将自己的实现改成通过 in 参数传入大结构,而无需自己的所有调用方都修改自己的调用代码,就可以获得性能的提高。不过不能通过 in 字面量的方式进行调用。

public int ByIn(in string s) => s.Length;

string s = string.Empty;
ByIn(in s); // Works fine
ByIn(s); // Works fine as well!
// Fail?!?! An expression cannot be used in this context because it may not be passed or returned by reference
ByIn(in "some string");
ByIn("some string"); // Works fine!

in 修饰符的性能特性

in 参数跟 readonly 字段相当类似,为了避免破坏 struct 的 readonly/in 语义,在调用 struct 的属性和方法之前,编译器会做一次防御性复制,从而导致性能降低。因此,绝不应该通过 in 来传递非 readonly struct 结构体!非 readonly struct 通过 in 传递常常导致频繁的防御性复制,让性能变得更糟。

public struct FairlyLargeStruct
{
    private readonly long l1, l2, l3, l4;
    public int N { get; }
    public FairlyLargeStruct(int n) : this() => N = n;
}
 

private readonly int[] _data = Enumerable.Range(1, 100_000).ToArray();
 
[Benchmark]
public int AggregatePassedByValue()
{
    return DoAggregate(new FairlyLargeStruct(42));
 
    int DoAggregate(FairlyLargeStruct largeStruct)
    {
        int result = 0;
        foreach (int n in _data)
            result += n + largeStruct.N;
        return result;
    }
}
 
[Benchmark]
public int AggregatePassedByIn()
{
    return DoAggregate(new FairlyLargeStruct(42));
 
    int DoAggregate(in FairlyLargeStruct largeStruct)
    {
        int result = 0;
        foreach (int n in _data)
            result += n + largeStruct.N;
        return result;
    }
}

结果

                 Method |      Mean |     Error |    StdDev |
----------------------- |----------:|----------:|----------:|
 AggregatePassedByValue |  71.24 us | 0.3150 us | 0.2278 us |
    AggregatePassedByIn | 124.02 us | 3.2885 us | 9.6963 us |

结论

  • readonly struct 对于设计和性能角度都很有用。
  • 如果 struct 的大小比 IntPtr.Size 大,应该通过 in 来传递获得性能提高。
  • 可以通过使用 in 来传递引用类型,让自己的设计意图更清晰。(其实也无所谓)
  • 绝不使用 in 来传递非 readonly struct,因为对性能会造成负面的影响,而且常常是不容易发觉的。
  • in 对引用类型和基本的数字类型也可以应用,但基本上没什么特别的作用。
  • 对于通过 ref readonly 方法返回的 struct 引用,上面会导致防御性复制的内容仍然成立。因此最好也结合 readonly struct 使用。

番外

可以使用 ErrorProne.NET 来检查和避免 structreadonly/in 相关的性能问题。

参考:
The ‘in’-modifier and the readonly structs in C#
Reference semantics with value types
Performance traps of ref locals and ref returns in C#

使用 ffmpeg 切割和合并视频文件

试用了不少软件来进行视频文件的简单操作,例如切割和合并,效果都难以让人满意。今天试下直接使用 ffmpeg 来操作一下,效果相当不错,比各种自由软件好多了。

切割

切割命令如下:

ffmpeg -i input.mp4 -ss 00:21:25.0 -to 00:33:05.0 -c copy output.mp4

从视频从21分25秒 到 33分05秒的片段切割出来。其中 -to 参数可以换为 -t, -t表示的是切割的时间段,例如切割10秒钟的片段。

合并

只是简单的将多个参数完全一致的文件合并的话,可以采用 concat demuxer 的方式。
首先生成一个待合并视频列表文件 list.txt,内容如下

file 'input1.mp4'
file 'input2.mp4'

然后执行命令

ffmpeg -f concat -i list.txt -c copy output.mp4

轻松搞定。

处理音频

将 MP4 中的音频转录到 MP3

ffmpeg -i filename.mp4 filename.mp3

ffmpeg -i video.mp4 -b:a 192K -vn music.mp3

截取 MP3 中的一段

ffmpeg -i file.mp3 -ss 00:00:20 -to 00:00:40 -c copy file-2.mp3

合并 MP3

ffmpeg -i "concat:1.mp3|2.mp3" -acodec copy output.mp3

参考

Using ffmpeg to cut up video
Concatenate two mp4 files using ffmpeg

使用 PowerShell 创建自签名 https 证书

假如要创建针对域名 api.mydomain.com 的 https 证书,可以 PS 中执行如下命令即可:

New-SelfSignedCertificate -DnsName "api.mydomain.com" -CertStoreLocation "cert:\LocalMachine\My"

创建的证书被存储在[证书(本地计算机)-个人]中,可以根据需要导出到其他电脑导入受信任的根证书区域,避免访问这个证书的网站不受信任。若使用 iis,则这个证书在绑定 https 协议时已经可以提供选择了。证书有效期一年。

上面命令中的 api.mydomain.com 可以更改为 *.mydomain.com 来实现通配符证书。

其他的需求如需要更改证书有效期、直接将证书创建到根证书区域等,可以参阅 New-SelfSignedCertificate 文档

How to create a self-signed certificate for a domain name for development?

使用 App_offline.htm 将 asp.net 网站转入维护状态

对使用中的网站进行维护时,通常需要将网站临时关闭,进行文件更新等操作,然后再开启,以避免在更新文件过程中,客户端读到不一致的内容。(如果是在负载均衡场景下就不需要这么麻烦,可以与负载均衡配合,进行客户端无感知更新了。)

但直接关闭网站或者杀掉进程,可能会导致正在进行的 request 处理意外中断,没有全面使用事务的话有可能导致数据不一致,系统数据被破坏。而且客户端/浏览器访问直接就服务端 503 出错,给人的观感相当不好。

是使用 iis 部署的 asp.net (core) 程序的话,有个更优雅点的方式,就是直接将一个名为 App_offline.htm 的文件放置到 web application 的根目录下。iis 检测到该文件之后,对所有之后新到的 request 都返回该文件的内容,同时等到一段时间(默认 10 秒)后关闭 web application,有 10 秒,基本上可以保证正在处理的 request 的应用逻辑都跑完。此时你就可以从容更改网站内容了。

更改完之后,将该文件删掉。下一个请求又重新访问到 web application 的内容了。

几个注意的点:

  • 文件名必须精确的为 App_offline.htm,区分大小写。
  • 某些 iis 版本下,该文件大小要求 > 512 字节,如果页面不够大,可以用一些 html 注释填充。

Razor 预编译与 Partial View 搜索

今天在访问发布系统的时候,打开某个页面,系统报异常:

The partial view '_OnetimeServiceEditInfo' was not found

奇怪了,之前都是用得好端端的。在本地开发环境一试,却又一切正常,没有任何问题。

系统是用 asp.net core 1.1 开发的,本机环境跟发布环境不同之处在于发布环境的 view 都是通过预编译生成了一个 dll 文件直接部署的。想到这点,我猜想是不是 dll 里面没有包含 _OnetimeServiceEditInfo 这个 view?用 ILSpy 看了下,并不是这个原因,这个 view 的确已经编译进去了。我又检查了下本地的 view 文件,view 也是存在的。

莫非 asp.net core 的预编译有 bug?一般情况下我是不会有这种想法的,因为这些框架的代码质量比我们个人的代码质量靠谱得多。不过之前在用 asp.net mvc 的时候,我的确遇到过不能通过 VirtualPathProvider 使用 precompiled view 的问题,所以产生了一丝怀疑。google 了下,没发现什么有价值的信息。

那肯定是自己的问题了!

再回头仔细看了下使用这个 partial view 的页面代码,双击 _OneTimeServiceEditInfo.cshtml 文件名,复制左边,粘贴到 @Html.Partial 里面,一看,没有任何区别啊。不管,先保存,咦,文件内容居然有改动!compare 一下,才发现,有个大小写的问题。本来应该是 T 的,写成了 t。

@Html.Partial("_OneTimeServiceEditInfo", Model) // T
@Html.Partial("_OnetimeServiceEditInfo", Model) // t
@* T 和 t 混淆了。 *@

原因就很清晰了,在没有预编译的情况下,ViewEngine 寻找对应 view 的方式,是区分大小写的。而使用预编译之后,ViewEngine 寻找 view 时,就区分大小写了。所以造成了行为不一致。开发的时候发现不了。而且之前发布的环境也没有问题,要不早暴露出来了。搞不清楚是不是因为升级某个版本之后,行为才改变了。虽然的确是自己代码的 bug,但升级真的还是有风险哪。

看来的确应该用 container 来开发、测试和部署的环境差异问题,确保环境一致,以免出现这种环境不一致排查问题困难的情况。

小程序的兼容性坑:iOS vs 安卓、真机调试 vs 真机运行

这段时间也搞了一下小程序。小程序声称自己只需要针对自己的平台开发一次,然后各个平台都可以跑,跟之前 java 之类的宣称是一样的。java 结果我们都知道,一次编写,到处调试。小程序也差不多。

问题

今天遇到一个兼容性问题症状如下:

小程序在开发环境编译运行没有问题,使用 iPhone 预览没有问题,使用 iPhone 真机调试没有问题,最后发布了体验版、正式版,iPhone 上都完全没有问题。然后接到反馈,说安卓下面这个小程序没办法运行。然后掏出弟弟淘汰了万年的 Meizu M9 看看,果然出了问题,页面只有一个按钮,而且所有功能都没有。

一开始觉得是小事一桩,用真机调试一下,看看什么情况不就完了。结果真机调试二维码扫描连上去,小程序居然就正常了。这就坑了。再尝试了几次,结果是手机直接访问预览版本、体验版本、正式版本都不行,但真机调试就没问题。这个“真机”调试根本就跑不出什么真机的情况,调试环境不一致。提交了下腾讯的自动测试,它跑了几个安卓机型,却又都正常。这就让人想不通了。

由于没办法使用调试工具,而看样子小程序里面的 javascript 代码从一开始就没办法正常运行,有点一筹莫展。然后针对各种猜想做了下尝试。

  1. 由于使用了 typescript 的模板,是不是这个模板生成的 javascript 小程序不懂?

直接下手修改了生成后的 javascript 代码,删掉了一些 typescript 生成的内容而觉得不是很需要或者可能出错的。结果没变化。

  1. 是不是由于使用了 import 来引入 util 方法使用,而小程序不支持?

util 方法都移入到 page 的 js 里,运行,结果依旧。

  1. 无奈,最终只好用上了删除一半代码 debug 大法

一路删除定位下来,最终确定是因为安卓系统不支持 Intl.NumberFormat 对象。我用这个来格式化显示小程序中的金额数值:

let moneyFormatter = new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY',
    minimumFractionDigits: 2
})
return moneyFormatter.format(amount)

这个对象现在属于标准的一部分,所有稍微新点的浏览器都支持,NodeJs 也支持,iPhone 上也支持,估计使用的是 javaScriptCore。而安卓上面不知道为什么就不支持,也不知道微信用的是什么样的核心。总之这里存在一个不兼容。

而为什么 “真机” 调试不会出问题呢?猜想是因为真机调试运行的还是在编辑器提供的 chromium 环境上,而 chromium 环境是支持这个 api 的。所以这个“真机”可能也就调试一下触摸操作以及屏幕分辨率方面的东西了。

解决方法

用如下几种方法可以解决:

  1. 使用 string.toLocaleString 方法来代替上面的写法:
return amount.toLocaleString('zh-CN', {
    style: 'currency',
    currency: 'CNY',
    minimumFractionDigits: 2
})

这样写,在安卓上不会因为没有 Intl.NumberFormat 而出错,虽然它也还是不能正确格式化显示,只会按照原来的数值显示。比如金额是 9998,它就显示成 9998,而不是我们希望的 ¥9,998.00,在 iPhone 上则是没问题的。

  1. 使用 Intl 的 polyfill

参见这里 https://github.com/andyearnshaw/Intl.js/

我没有用这个,因为没有类型信息,不是很喜欢。

  1. 自己写了一个 format 方法。

由于我需要的场景很简单,就是一个金钱的简单格式化,几行代码就搞定了,引入 polyfill 文件尺寸太大,干脆就自己轮一个了(不作太多全面考虑)。

export function format(amount: number): string {
  let fixedString = amount.toFixed(2)
  let len = fixedString.length
  let result = ''
  for (let i = len - 1; i >= 0; i--) {
    let stepFromTail = len - 1 - i

    let char = fixedString.charAt(i)
    if (stepFromTail <= 5) {
      result = char + result
    }
    else {
      if (stepFromTail % 3 === 0) {
        if (char !== '-') {
          result = ',' + result
        }
      }
      result = char + result
    }
  }
  return '¥' + result
}

总结

编写小程序一旦用到各种稍微不是非常常用的 api,一定要留意兼容性问题,而且尽量两个平台都要做一下测试,免得被埋了。

在 linux 上用 nginx 代理 asp.net core 应用并使用 let's encrypt 证书开启 https

下面操作针对的是 Debian 9,基本上也可以照搬到 Ubuntu 16.04 或更新版本。我们使用的域名为 mydomain.com,用来接收 let's encrypt 证书更新提醒的邮箱是 [email protected],此邮箱不需要跟使用域名一致。我们要部署的 web 应用为 demoWeb,为 asp.net core 2.0 应用。

在 linux 上安装 .net core

首先参考 https://www.microsoft.com/net/learn/get-started/linuxdebian 来将 .net core 安装到 linux 上。

安装系统组件:

sudo apt-get update
sudo apt-get install curl libunwind8 gettext apt-transport-https

注册可信微软产品 key:

curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg

注册微软产品 feed:

sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-debian-stretch-prod stretch main" > /etc/apt/sources.list.d/dotnetdev.list'

安装 .NET Core SDK:

sudo apt-get update
sudo apt-get install dotnet-sdk-2.1.4

将 dotnet 加到环境 PATH:

export PATH=$PATH:$HOME/dotnet

创建一个 console app 并运行,验证下安装有没有问题,输出 Hello world 则表示安装成功:

dotnet new console -o myApp
cd myApp
dotnet run

将 asp.net core 应用部署到 linux

参考文档 https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?tabs=aspnetcore2x 来将应用部署到 linux。

首先 publish 应用 demoWeb,可以通过 dotnet publish -c 命令,或者通过 visual studio 的 publish 菜单项,将应用 publish 到一个目录中。如果是用 vs 的话,会 publish 到 .\bin\Release\PublishOutput 中。使用压缩软件 winrar 或其他将此目录中的所有文件压缩到压缩包 demoWeb.zip。下面我们假设这个 demoWeb.zip 位于 E:\project\demoWeb\bin\Release\PublishOutput 中。我们使用 win 10 上的 wsl 来将这个压缩文件复制到服务器。

进入 bash,在 cmd 或者 powershell 中运行:

bash

然后跳转到压缩包所在的目录。注意 linux 环境下,路径是区分大小写的。

cd /mnt/e/project/demoWeb/bin/Release/PublishOutput

上面这个步骤可以省略,可以在压缩包所在目录按住 shift 点击右键,选择在此打开 powershell,然后在 powershell 中输入 bash,即可在 bash 中切换到当前目录。

将压缩包复制到服务器端:

scp demoWeb.zip [email protected]:/var/demoWeb

在服务器端解压缩:

cd /var/demoWeb
unzip demoWeb.zip

现在在服务器端(ssh)中输入 dotnet /var/demoWeb/demoWeb.dll,一切顺利的话,它会显示正在 http://localhost:5000 listen,这个端口是在你 web 应用的 Program.cs 中配置的。

配置 nginx 反向代理

由于需要通过 nginx 进行反向代理,先修改 demoWeb 的代码,在 StartupConfigure 方法的 UseAuthentication 或类似的验证中间件前加入:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// before it
app.UseAuthentication();

将它重新发布。

安装 nginx:

sudo apt-get install nginx

启动它:

sudo service nginx start

可以通过 http://mydomain.com 看看能否看到 nginx 的默认页面。

配置反向代理。修改 /etc/nginx/nginx.conf 文件,可以用 vim 或者其他编辑器

sudo vim /etc/nginx/nginx.conf

将文件修改为:

http {
	upstream demoWeb {
		# Replace the port with your app's localhost port.
		server localhost:5000;
	}

	server {
		listen 80;
		server_name mydomain.com;
		#client_max_body_size 2M;
		location / {
			proxy_pass http://demoWeb;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection keep-alive;
			proxy_set_header Host $http_host;
			proxy_cache_bypass $http_upgrade;
		}
	}
}

如果需要允许请求的大小增加,可以在 http/server/location 里面增加 client_max_body_size 项来配置最大的请求大小,默认为 1MB。

保存文件,验证并重新加载 nginx 的配置:

sudo nginx -t && sudo nginx -s reload

如果没有问题的话,现在可以通过 http://mydomain.com 访问到你的 demoWeb 应用的内容了。

使用 systemd 监控 Kestrel 进程

由于 nginx 不像 iis 的 ASP.NET Core Module 那样能够管理 asp.net core 应用宿主 Kestrel 进程,万一这进程挂掉了,我们的网站就会 down 掉。所以我们需要使用 systemd 来监控它,万一它挂掉了,自动将它重新拉起来。旧版本的 linux 可以使用 supervisor 来做到这个。

创建服务定义文件:

sudo vim /etc/systemd/system/kestrel-demoWeb.service

文件内容为:

[Unit]
Description=Example .NET Web API App running on Linux

[Service]
WorkingDirectory=/var/demoWeb
ExecStart=/usr/bin/dotnet /var/demoWeb/demoWeb.dll
Restart=always
RestartSec=10  # Restart service after 10 seconds if dotnet service crashes
SyslogIdentifier=dotnet-demoWeb
User=www-data # 可以替换为其他用户名
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

保存文件,并启用服务:

systemctl enable kestrel-demoWeb.service

启动服务,并验证是不是正常跑起来了:

systemctl start kestrel-demoWeb.service
systemctl status kestrel-demoWeb.service

顺利的话,就能跑起来了。

使用 let's encrypt 开启 https

此处主要参考了 https://nozzlegear.com/blog/lets-encrypt-and-nginx 来配置 https 证书。

首先安装 Certbot/opt/letsencrypt 目录:

sudo apt update
sudo git clone https://github.com/certbot/certbot /opt/letsencrypt

创建 /var/www/letsencrypt 目录,并给 nginx 访问它的权限:

mkdir -p /var/www/letsencrypt
# www-data is the Nginx username
sudo chgrp www-data /var/www/letsencrypt

创建域名配置文件,并编辑它:

sudo mkdir -p /etc/letsencrypt/configs
sudo touch /etc/letsencrypt/configs/mydomain.com.conf
sudo vim /etc/letsencrypt/configs/mydomain.com.conf

内容编辑为:

# Just one single domain.
domains = mydomain.com

# Key size.
rsa-key-size = 4096 # Or 2048

# The current version of Let's Encrypt (as of May 3, 2017) is using this server.
server = https://acme-v01.api.letsencrypt.org/directory

# This email address will receive renewal reminders.
email = [email protected]

# This will run as a cronjob, so turn off the ncurses UI.
text = True

# Place the certs in the /var/www/letsencrypt folder (under .well-known/acme-challenge/).
authenticator = webroot
webroot-path = /var/www/letsencrypt/

修改 nginx 配置文件,让它支持 let's encrypt 的获取证书验证:

sudo vim /etc/nginx/nginx.conf

文件更改为:

http {
	upstream demoWeb {
		server localhost:5000;
	}

	server {
		listen 80;
		server_name mydomain.com;

		location / {
			proxy_pass http://demoWeb;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection keep-alive;
			proxy_set_header Host $http_host;
			proxy_cache_bypass $http_upgrade;
		}
		# 让 let's encrypt 的访问穿透 demoWeb 应用,让它可以获取它需要的内容
		location /.well-known/acme-challenge {
			root /var/www/letsencrypt;
		}
	}
}

保存文件并重新加载配置文件:

sudo nginx -t && sudo nginx -s reload

运行 certbot 来创建你的域名的证书:

cd /opt/letsencrypt
./certbot-auto --config /etc/letsencrypt/configs/mydomain.com.conf certonly

在处理过程中,需要你同意用户协议以及决定是否将 Email 分享,视情况自己处理。一切顺利的话,会显示一行“Congratulations! Your certificate and chain have been saved at $path”。这表明你可以在后面的 https 配置中使用这些证书文件了。

再次修改 nginx 的配置来允许 https 访问:

sudo vim /etc/nginx/nginx.conf

修改之后文件内容如下:

http {
	upstream demoWeb {
		server localhost:5000;
	}

	server {
		listen 80;
		server_name mydomain.com;

		location / {
			proxy_pass http://demoWeb;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection keep-alive;
			proxy_set_header Host $http_host;
			proxy_cache_bypass $http_upgrade;
		}
		# 让 let's encrypt 的访问穿透 demoWeb 应用,让它可以获取它需要的内容
		location /.well-known/acme-challenge {
			root /var/www/letsencrypt;
		}
	}
	# 加入 https 节点
	server {
		listen 443 ssl;
		server_name mydomain.com;
		ssl on;
		gzip on;
		ssl_stapling on;
		ssl_stapling_verify on;
		ssl_session_timeout 5m;
		# Note: Some tutorials may tell you to use cert.pem, which will work in most browsers but fails on e.g. Amazon Alexa server requests.
		# Alexa requests need the intermediary certs too, which cert.pem does not have. fullchain.pem does have them.
		ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
		ssl_trusted_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
		ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;

		location / {
			proxy_pass http://demoWeb;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection keep-alive;
			proxy_set_header Host $http_host;
			proxy_cache_bypass $http_upgrade;
		}
	}
}

保存文件并重新加载配置(再一次):

sudo nginx -t && sudo nginx -s reload

顺利的话,你已经可以通过 https://mydomain.com 访问你的 demoWeb 应用了。

强制 https 访问

经过上面的配置,你现在既可以通过 http 也可以通过 https 来访问自己的 web 应用。通常为了安全的考虑,会强制只通过 https 来访问,将 http 的请求跳转到 https 中。下面我们通过再一次修改 nginx 的配置来达成这一点。

sudo vim /etc/nginx/nginx.conf

将配置更改为:

http {
	upstream demoWeb {
		server localhost:5000;
	}

	server {
		listen 80;
		server_name mydomain.com;

		location / {
			# 将请求重定向到 https
			add_header Strict-Transport-Security max-age=15768000;
			return 301 https://$host$request_uri;
		}
		# 让 let's encrypt 的访问穿透 demoWeb 应用,让它可以获取它需要的内容
		location /.well-known/acme-challenge {
			root /var/www/letsencrypt;
		}
	}
	# 加入 https 节点
	server {
		listen 443 ssl;
		server_name mydomain.com;
		ssl on;
		gzip on;
		ssl_stapling on;
		ssl_stapling_verify on;
		ssl_session_timeout 5m;
		# Note: Some tutorials may tell you to use cert.pem, which will work in most browsers but fails on e.g. Amazon Alexa server requests.
		# Alexa requests need the intermediary certs too, which cert.pem does not have. fullchain.pem does have them.
		ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
		ssl_trusted_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
		ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;

		location / {
			proxy_pass http://demoWeb;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection keep-alive;
			proxy_set_header Host $http_host;
			proxy_cache_bypass $http_upgrade;
		}
	}
}

保存并重新加载配置:

sudo nginx -t && sudo nginx -s reload

现在访问 http://mydomain.com 的请求都会被自动重定向到 https://mydomain.com。

定期自动更新 let's encrypt 的证书

let's encrypt 的证书 90天 过期,因此要每隔一段时间更新一下。我们可以使用 linux 的 cron job 来自动完成这件事。

创建一个自动执行任务帐号有权限访问并执行的文件:

vim ~/renew-letsencrypt.sh

文件内容:

#!/bin/sh

mkdir -p /var/log/letsencrypt;
cd /opt/letsencrypt/;
./certbot-auto --non-interactive --keep-until-expiring --agree-tos --qui
et --config /etc/letsencrypt/configs/mydomain.com.conf certonly;

if [ $? -ne 0 ]
 then
        ERRORLOG=`tail /var/log/letsencrypt/letsencrypt.log`
        echo -e "The Let's Encrypt cert has not been renewed! \n \n" \
                 $ERRORLOG
 else
        nginx -s reload
fi

exit 0;

给它执行权限(不确定是不是需要):

chmod +x ~/renew-letsencrypt.sh

最后,打开 crontab 来添加 cron job

crontab -e -u yourUserName
# In crontab
0 0 1 JAN,MAR,MAY,JUL,SEP,NOV * ~/renew-letsencrypt.sh

保存即可。

至此,所有的工作都已经完成。

一种批量更新数据的 SQL Server 写法

2个表,
表 A 有字段: A1和A2
表 B 有字段: B1和B2

欲更新 A1 字段,条件是 A2 = B2,更新内容为 B1。

可以采用如下写法:

UPDATE A
SET A1 = B1
FROM A, B
WHERE A.A2 = B.B2

All about Span

什么是 Span<T>

System.Span<T> 是表示任意的连续内存的一个 value type,无论是托管内存、栈分配的内存还是 interop 的 native code 内存。提供高性能且安全的 access。

通过 Span<T> 访问数组

var arr = new byte[10];
Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>

访问其中的一部分

Span<byte> slicedBytes = bytes.Slice(start: 5, length: 2);
slicedBytes[0] = 42;
slicedBytes[1] = 43;
Assert.Equal(42, slicedBytes[0]);
Assert.Equal(43, slicedBytes[1]);
Assert.Equal(arr[5], slicedBytes[0]);
Assert.Equal(arr[6], slicedBytes[1]);
slicedBytes[2] = 44; // Throws IndexOutOfRangeException
bytes[2] = 45; // OK
Assert.Equal(arr[2], bytes[2]);
Assert.Equal(45, arr[2]);

访问栈上分配的内存

Span<byte> bytes = stackalloc byte[2]; // Using C# 7.2 stackalloc support for spans
bytes[0] = 42;
bytes[1] = 43;
Assert.Equal(42, bytes[0]);
Assert.Equal(43, bytes[1]);
bytes[2] = 44; // throws IndexOutOfRangeException

访问 native heap 内存

IntPtr ptr = Marshal.AllocHGlobal(1);
try
{
  Span<byte> bytes;
  unsafe { bytes = new Span<byte>((byte*)ptr, 1); }
  bytes[0] = 42;
  Assert.Equal(42, bytes[0]);
  Assert.Equal(Marshal.ReadByte(ptr), bytes[0]);
  bytes[1] = 43; // Throws IndexOutOfRangeException
}
finally { Marshal.FreeHGlobal(ptr); }

Span<T> 索引器利用了 C# 7.0 的新特性 ref return,用 ref T 来定义返回值,因此返回的是实际存储位置的引用,而不是返回一个存储值的 copy。

public ref T this[int index] { get { ... } }

Span<T> 的另一个变体是 System.ReadOnlySpan<T>,用来进行只读访问。返回值为 C# 7.2 新特性提供的 ref readonly T,用来操作类似 string 等不可变序列。

string str = "hello, world";
string worldString = str.Substring(startIndex: 7, length: 5); // Allocates 
ReadOnlySpan<char> worldSpan = str.AsReadOnlySpan().Slice(start: 7, length: 5); // No allocation
Assert.Equal('w', worldSpan[0]);
worldSpan[0] = 'a'; // Error CS0200: indexer cannot be assigned to

Span<T> 支持重新解释(reinterpret)的转换,也即可以将 Span<byte> 转换到 Span<int>,其中 Span<int>[0] 对应 Span<byte>[0][1][2][3],这样可以读取一个 byte buffer,然后将它传到方法里分组为 int 进行处理,安全且高效。

Span<T> 的实现

Span<T> 是一个包含一个 ref 和 length 的值类型,接近如下声明:

public readonly ref struct Span<T>
{
  private readonly ref T _pointer; // There is not ref in C# or MSIL
  private readonly int _length;
  ...
}

不过在 C# 和 MSIL 中都无法定义一个 ref 字段,实际上是 JIT 对 Span<T> 进行了特别对待,为它生成对应 ref 字段的 JIT 指令。这里的 ref 使用跟方法的 ref 参数类似,就是直接指向 location 的引用。这种直接或间接包含有这种引用的类型被称之为 ref-like type,C# 7.2 允许通过 ref struct 来定义这种类型。

  1. Span<T> 被定义为一种跟数组一样高效的方式:对 Span<T> 进行索引操作是直接的,无需根据开始指针和偏移来计算。( ArraySegment<T> 就需要进行这样的换算。)
  2. 由于 Span<T>ref-like type,所以它会收到限制。

Memory<T> 是什么以及我们为什么需要它

Span<T> 不仅能指向对象或数组的开头,也能指向它们中间的一段,这种引用称之为内部指针,而 GC 处理这种情况代价不轻,所以只允许这种 ref 在栈中出现。另外,Span<T> 大小超过了一个字长,这意味着读和写 Span<T> 不是一个原子操作。如果允许多个线程同时操作它的字段,可能引起撕裂。因此,Span<T> 的实例被限制为只能在栈上生存,而不能在堆中。这意味着不能装箱Span<T>,也不能将它用于反射 API(需装箱),不能在类中包含 Span<T> 的字段,甚至不能在非 ref sturct 中包含 Span<T> 字段。而且即使是隐式会将 Span<T> 转化为此类字段的使用场景都不行,例如通过捕获它们到 lambda 中 / async 方法中 / 迭代器中。也不能将 Span<T> 作为泛型参数使用。

这种限制在很多场景下都无所谓,特别是对于只搞计算的同步方法。但对异步方法就很麻烦了,所以引入了 Memory<T>。还有它对应的 ReadOnlyMemory<T>

// Memory<T> looks very much like an ArraySegment<T>:
public readonly struct Memory<T>
{
  private readonly object _object;
  private readonly int _index;
  private readonly int _length;
  ...
}

可以在它们之间互相转换使用。

Span<T>Memory<T> 如何跟类库进行整合

无数新的使用 {ReadOnly}Span<T>{ReadOnly}Memory<T> 的 API 被加入到类库中。例如 int 之类的基元类型,都加入了接受 ReadOnlySpan<char>Parse 方法,以进行 0 分配的操作。这些包含了新 API 重载的类型包括 System.RandomSystem.Text.StringBuilderSystem.Net.Sockets。而且很多 API 不但支持新的 Span<T>,而且将返回值变成了 ValueTask<T>,以在同步操作或者已经有缓存数据的时候直接返回时不需要分配新的对象。例如 Stream.ReadAsync

另外,Span<T> 允许框架包含一些以往会引起内存安全忧虑的方法,比如以往生成一个随机字符串,如下处理:

int length = ...;
Random rand = ...;
var chars = new char[length];
for (int i = 0; i < chars.Length; i++)
{
  chars[i] = (char)(rand.Next(0, 10) + '0');
}
string id = new string(chars);

Span<char> 可以在栈上分配,并避免 unsafe 的使用,结合新的接受 ReadOnlySpan<char> 的字符串构造方法,可以将生成算法修改为:

int length = ...;
Random rand = ...;
Span<char> chars = stackalloc char[length];
for (int i = 0; i < chars.Length; i++)
{
  chars[i] = (char)(rand.Next(0, 10) + '0');
}
string id = new string(chars);

这样就避免了在堆上做临时的字符串数组分配,但还是需要一次额外的复制操作将生成数据从栈上复制到字符串中。而且这种使用方式只适用于字符串小的情况,如果长的话就会导致栈溢出。那么能否直接写道字符串所在的内存呢?Span<T> 让你可以这么做。在 string 新的构造函数之外,它还多了个 Create 方法:

public static string Create<TState>(
  int length, TState state, SpanAction<char, TState> action);
...
public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);

这个方法允许你填充可写的 Span<T> 来生成 string。由于 Span<T> 只能在栈上生存的特质,这里可以保证在字符串构造完之前,对应的 Span<T> 就已经终结了,无法再在其他地方使用,无法在字符串构造完成之后再来更改字符串。

int length = ...;
Random rand = ...;
string id = string.Create(length, rand, (Span<char> chars, Random r) =>
{
  for (int i = 0; chars.Length; i++)
  {
    chars[i] = (char)(r.Next(0, 10) + '0');
  }
});

这样就可以直接在字符串空间中任意填充了,不仅避免了复制,而且没有了大小的限制。

除了核心的框架类型有了新的成员之外,还有很多的新的 .NET 类型被开发出来跟 Span<T> 一起对特定的场景进行高效处理。例如,开发人员发现如果他们在 UTF-8 上处理字符串不需要 encode 和 decode 的话会获得显著的性能提升。新的类型如 System.Buffers.Text.Base64System.Buffers.Text.Utf8ParserSystem.Buffers.Text.Utf8Formatter 被加了进来。它们对字节进行处理,不单避免了 Unicode 的编解码,还允许它们直接在各种网络栈很底层中很常见的 native buffer 上进行操作:

ReadOnlySpan<byte> utf8Text = ...;
if (!Utf8Parser.TryParse(utf8Text, out Guid value,
  out int bytesConsumed, standardFormat = 'P'))
  throw new InvalidDataException();

除了公开出来的这些方法,框架内部也开始利用这些新的基于 Span<T>Memory<T> 的方法来获得更好的性能。.NET Core 内的调用已经切换到使用新的 ReadAsync 重载以避免不必要的分配。

ASP.NET Core 现在也重度依赖 Span<T>,例如 Kestrel Server 的 HTTP parser 现在就是在它之上构建的。将来,很可能 span 会在更底层的公开 API 中暴露出来,例如在中间件管道中。

.NET Runtime

.net runtime 致力于消除不必要的边界检测,以提高 Span<T> 的使用性能。

C# 和 编译器

C# 7.2 中引入了跟 span 相关的若干特性(事实上 C# 7.2 的编译器就必须使用 Span<T>)。

Ref struct。只能生存在栈上的会传染的值类型。所有想要在字段中包含 ref-like type 的类型都必需定义为 ref struct。例如你想要定义一个用于 Span<T> 的 Enumerator:

public ref struct Enumerator
{
  private readonly Span<char> _span;
  private int _index;
  ...
}

Span<T> 的 stackalloc。在以往的 C# 版本中,stackalloc 结果保存在本地的一个指针变量中。在 C# 7.2 中,stackalloc 能作为表达式的一部分使用并且能指向一个 span,而且不需要使用 unsafe 关键字。所以,以往的写法:

Span<byte> bytes;
unsafe
{
  byte* tmp = stackalloc byte[length];
  bytes = new Span<byte>(tmp, length);
}

变成了更简单的:

Span<byte> bytes = stackalloc byte[length];

对需要一些小空间来进行操作但又想避免在堆中分配的情形特别有用。

Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length];
... // Code that operates on the Span<byte>

Span 使用验证。避免 Span 从使用栈帧逃离出去引起内存安全问题。

.net 中的大对象

在 .net 的内存分配中,有四种逻辑区域,Gen 0/1/2 三代是针对小对象的,还专门有个 LOH 大对象堆,里面放的是“大”对象。因为大对象在内存中复制来复制去成本很高,因此特别给它安排了 LOH,LOH 回收的时候只标记和清除废弃的对象,不会进行压缩,也就是说,不会在 LOH 中挪动大对象。因此如果中间部分大对象被回收了,这个段中间就有孔洞,这跟小对象堆是不同的,小对象堆每次回收都会压缩,全都缩在一起(pin住了的除外)。

.net 团队经过大量测试,找到了大小对象的分界点,为 85000 字节,>= 85000 bytes 的就是大对象。大对象一般是各种数组,除了机器生成的代码,正常的对象也到不了那么大。但 double 数组是个特例,如果 double 数组超过 1000 个元素,就认为它是大对象了,据说是因为 double 是 8 bytes 的,要 8 字节对齐,所以特殊对待。但没找到资料说 long 是怎么处理的。现在 clr 开源了,有空可以去看看具体是怎么处理。我在知乎看到个帖子说 primitive type 的数组,无论多大都不会进 LOH,但我没查到对应的资料,而且对这个说法我是很怀疑的,毕竟大对象提出的目的就是解决复制成本问题。还是得等看了 clr 代码才知道是不是真如所说,在此之前,先保留怀疑态度。

大对象 gc 有两个特点:

  • 回收时不压缩,因此可能会留洞。留洞多了,内存利用率就不高,很可能总体内存满足需求的,但因为不连续的洞比较小,就塞不进去,然后 oom。而且留洞也会导致查找空闲空间塞对象的过程比较复杂耗时,降低性能。
  • LOH 的回收是跟 Gen 2 回收一起的。因此每次对 LOH 回收都会触发 full gc,成本很高,造成吞吐量急剧下降。

如何避免频繁 gc LOH 呢?我想到的有下面几个办法:

  1. 将一块大对象 pin 起来,尽可能复用,而不是生成新的大对象。这种模式有对象池、memoryPool 等。分配越少,就越不会触发 gc。
  2. 可以将一个逻辑大对象实现成多个小对象,这样它就不会跑进 LOH 了,就可以早期在 Gen 0 中被回收,成本很低。做法就是假如需要一个 > 85000 bytes 的 List,可以实现一个 IList 对象,内部是将 List 切分为多个小 List 的链表,每个小 List 不会超过 85000 bytes。这样就可以避免对象过大了。需要注意的是,在 x64 中,每个引用需要占用 8 bytes 而不是 x86 中的 4 bytes。应该现在已经有这样的技术了。
  3. 在栈上分配数组,完全避免回收。可以用 unsafe 的 stackalloc 将数组分配在栈上,如下面代码。但需要注意不要同时分配过多过大的数组,造成 stackoverflow,而且由于对象太大,要注意隐式的赋值复制。尽量用 ref 操作。
//在栈上直接分配数组
unsafe
{
    Char* pc = stackalloc Char[20000];
}
//或构造一个 struct,将数组嵌入到 struct,由于 struct 是分配在栈上的,也实现了栈上分配数组
unsafe struct CharArray
{
    public fixed Char Characters[20];
}
  1. 使用非托管的内存,使用 Marshal.AllocHGlobal 或 Marshal.AllocCoTaskMem 分配内存,然后对应地手动使用 Marshal.FreeHGlobal 和 Marshal.FreeCoTaskMem 来释放内存。这样不会给 GC 增加压力。现实的使用方式可以见 Kestrel 的一些使用

在实践中,大对象经常是比较长的字符串,例如 xml、生成的 html、以及传来传去的 json 等,这都是需要注意的地方。譬如小心选择靠谱的 json 序列化方案,限制用户上传的内容长度或将长度分块,流式处理数据以避免分配过多内存等。

new() 约束的陷阱

发现一篇很好的文章 Dissecting the new() constraint in C#: a perfect example of a leaky abstraction,在这里写一下要点。

问题

之前(.net framework 2.0 前时代)在项目中动态创建某个不定类型实例时,使用的是 Activator.CreateInstance(),虽然知道这种创建方式是通过反射实现的,性能不太理想,当时也没有太好的其他选择。后来泛型出来了,有了个 new() 约束,可以轻松地在代码里面 new T() 这样写了,心里美滋滋的,想微软还真是贴心哪,这下子不单代码更加简练了,还直接能把对象 new 出来,性能完美了。

public class NodeFactory
{
    public static TNode CreateNode<TNode>() 
        where TNode : Node, new()
    {
        return new TNode();
    }
}

没想到看了这篇文章之后才知道,原来看起来眉清目秀的 new TNode(),不是直接执行 new 指令创建出来的,而是一转身就跑去调用了 Activator.CreateInstance<T>(),看着靠谱的样子,实际上暗度陈仓的干活,真是让人大跌眼镜。性能比直接调用 Activator.CreateInstance<T> 还差。

而且这样的实现,除了性能问题,还有正确性上也存在问题:

  • 万一类型的构造方法抛出异常,new T() 返回的异常不是构造方法抛出的那个,而是被封装到了反射 API 调用的 TargetInvocationException 中。虽然可以通过如下方式使用 ExceptionDispatchInfo 来重新抛出纠正,但代码调用方不是相当了解细节的话就容易出错。
public static T Create<T>() where T : new()
{
    try
    {
        return new T();
    }
    catch (TargetInvocationException e)
    {
        var edi = ExceptionDispatchInfo.Capture(e.InnerException);
        edi.Throw();
        // Required to avoid compiler error regarding unreachable code
        throw;
    }
}
  • 虽然 C# 是不能创建不带参数的 struct 自定义构造方法的,但 clr 支持这样做,可能通过构造 il 或者使用其他 clr 语言构造出这样的 struct,由于 Activator.CreateInstance<T> 内部使用了 cache 来进行一定的性能优化,而这优化对于这种有不带参数的构造方法的 struct 存在 bug,对于这种 structActivator.CreateInstance<T> 只会对创建的第一个实例调用这构造方法,后面的实例都不会调用,从而造成行为方面的问题。

解决

那么应该如何应对这种情况?如何创建高性能而且正确的实例初始方法?

可以使用后面出来的表达式树来进行处理。表达式树是一种轻量级的代码生成方案,可以编译成 delegate,部分也可以编译成 Expression<DelegateType> 表达式。在这里我们可以使用它编译成 delegate

版本 1

public static class FastActivator
{
    public static T CreateInstance<T>() where T : new()
    {
        return FastActivatorImpl<T>.NewFunction();
    }
 
    private static class FastActivatorImpl<T> where T : new()
    {
        // Compiler translates 'new T()' into Expression.New()
        private static readonly Expression<Func<T>> NewExpression = () => new T();
 
        // Compiling expression into the delegate
        public static readonly Func<T> NewFunction = NewExpression.Compile();
    }
}

FastActivator.CreateInstance 概念上跟 Activator.CreateInstance 类似,但有两点不一样:

  • 它不会包装异常。
  • 运行时不会依赖反射。虽然在表达式构造时有依赖,但只会发生一次。

经过基准测试,FastActivator.CreateInstanceActivator.CreateInstance 快 5 倍,但比通过 Func<Node> 直接通过方法创建实例的方式还是慢 3.5 倍。为什么慢那么多呢?因为 Expression.Compile 创建一个 DynamicMethod 并把它关联到一个匿名程序集,为了让它在一个安全的沙箱环境跑,这主要是为了跑部分可信代码的安全性考虑,但带来了运行时的开销。

可以通过将 DynamicMethod 的一个 constructor 关联到特定的模块来解决。由于使用 Expression.Compile 实现这个存在困难,我们可以手动 “compile” 我们的 factory 方法:

版本 2

public static class DynamicModuleLambdaCompiler
{
    public static Func<T> GenerateFactory<T>() where T:new()
    {
        Expression<Func<T>> expr = () => new T();
        NewExpression newExpr = (NewExpression)expr.Body;
 
        var method = new DynamicMethod(
            name: "lambda", 
            returnType: newExpr.Type,
            parameterTypes: new Type[0],
            m: typeof(DynamicModuleLambdaCompiler).Module,
            skipVisibility: true);
 
        ILGenerator ilGen = method.GetILGenerator();
        // Constructor for value types could be null
        if (newExpr.Constructor != null)
        {
            ilGen.Emit(OpCodes.Newobj, newExpr.Constructor);
        }
        else
        {
            LocalBuilder temp = ilGen.DeclareLocal(newExpr.Type);
            ilGen.Emit(OpCodes.Ldloca, temp);
            ilGen.Emit(OpCodes.Initobj, newExpr.Type);
            ilGen.Emit(OpCodes.Ldloc, temp);
        }
            
        ilGen.Emit(OpCodes.Ret);
 
        return (Func<T>)method.CreateDelegate(typeof(Func<T>));
    }
}

有了这个新的 helper 方法,上面的 FastActivator 可以修改为:

public static class FastActivator
{
    public static T CreateInstance<T>() where T : new()
    {
        return FastActivatorImpl<T>.Create();
    }
 
    private static class FastActivatorImpl<T> where T : new()
    {
        public static readonly Func<T> Create =
            DynamicModuleLambdaCompiler.GenerateFactory<T>();
    }
}

这个版本比上个版本快两倍,但还是比 Func<Node> 慢两倍。原因如下:

  • 这个方法是基于泛型实现的,而调用泛型方法不会被 inline,所以多出了函数调用的开销。
  • 对于引用类型的泛型,clr 在执行时需要对类型进行判断,确定类型正确,多出了检查类型的开销。而对于值类型,这个版本其实并不会慢。

版本 3

为了解决这个引用类型的问题,避免间接性层次的增加,我们可以将内嵌的 FastActivatorImpl<T> 类搬到 FastActivator 外面,并直接调用它:

public static class FastActivator<T> where T : new()
{
    /// <summary>
    /// Extremely fast generic factory method that returns an instance
    /// of the type <typeparam name="T"/>.
    /// </summary>
    public static readonly Func<T> Create =
        DynamicModuleLambdaCompiler.GenerateFactory<T>();
}

这个版本表现就相当好了,可以跟直接调用 Func<Node> 相比较。

番外

如果 JIT 支持对 new T() 直接生成 new 指令就好了。

部署 asp.net core 应用到 windows server 2008 r2

我正在用的 dotnet core 版本为 1.0。
预先在 windows 2008 r2 上安装 iis 之类的就先跳过了。一般的部署步骤可以参考微软的官方教程 部署到 asp.net core 到 iis
但依照教程做之后,还有问题,访问网站还是会出错。其实还是有些依赖组件要安装的,但教程里面没有提。

1. 安装 .NET Core Windows Server Hosting

这个教程里面有说明

2. 安装 KB2533623 补丁

这是 dotnet core 的一个前置依赖,到 https://support.microsoft.com/en-us/kb/2533623 下载对应版本安装

3. 安装 KB2999226 补丁

另一个依赖,到 https://support.microsoft.com/en-us/kb/2999226 下载安装

都安装完之后,如果正确按照教程的指引来正确配置了 iis 和 publish 的话,网站就能顺利跑起来了。

关于 Data Protection Key

按照上面步骤操作下来,网站是能跑起来了,但还有个问题,就是 Data Protection 的 key 会漂移。

在没有使用持久化 key 的时候,每次 dotnet 跑起来,都会生成并使用新的 key,这样就导致每次进程起来 key 都不一样,而这个 key 不单影响显式用 data protect api 的操作,还影响用户验证等,造成的现象就是即使使用持久化 cookie,每次服务程序重启之后,验证信息还是会丢失,又需要用户重新登录。

教程 部署到 asp.net core 到 iis 里有提到使用 powershell 脚本在注册表生成 key 容器的方法,但这个脚本在 windows 2008 r2 下直接运行是有问题的,会提示 [Microsoft.Win32.RegistryView] 类型找不到。解决方法是安装个新的 powershell,下载这个补丁 Windows Management Framework 5.0 安装之后,再运行脚本即可。

关于应用的回收以及进程闲置

还是在对应的 App pool 高级设置中配置,默认还是按照20分钟闲置就释放进程。

号外:部署到 windows 2012

就按照官方教程即可,没有什么前置项需要像 windows 2008 那样额外安装的。所有操作都很顺利。

一个提高 dotnet 数组访问性能的小技巧

在 dotnet 中,array 是区分类型的,生成一个 T[] 数组,就不能在其中存入不是 T(及其子类)的元素。对于引用类型,CLR 会对存入的元素做类型检查。不过由于值类型是不能继承的,所以值类型的数组就不存在这样的检查。所以利用这一点来避免对引用类型数组进行类型检查的开销,从而提高存入性能。虽然数组性能已经很不错了,但有时候性能的这一点提高也有帮助。

我们可以创建一个对引用类型的值类型包装,如下:

public struct ObjectWrapper
{
    public readonly object Instance;
    public ObjectWrapper(object instance)
    {
        Instance = instance;
    }
}

然后使用这个值类型来创建数组,这样在设置数组值 ObjectWrapperArray[i] = objectWrapperInstance 时就不会有检查了。性能会有一定的提升。

Roslyn 代码库里面的 ObjectPool<T> 也利用了这样的技巧:

internal class ObjectPool<T> where T : class
{
    [DebuggerDisplay("{Value,nq}")]
    private struct Element
    {
        internal T Value;
    }
 
    // Storage for the pool objects. The first item is stored in a dedicated field because we
    // expect to be able to satisfy most requests from it.
    private T _firstItem;
    private readonly Element[] _items;
 
    // other members ommitted for brievity
}

RouterOS 使用 3322 动态域名

为了将内网的服务提供出去,需要申请一个动态域名。我是在 pubyun(即以前的 3322.org) 申请的免费动态域名。

申请之后,可以通过 pubyun 提供的 api ,在 pppoe 获得的 ip 改变之后,动态更新域名的 ip。

更新 IP 的脚本如下:

:local ednsuser "{username}"
:local ednspass "{password}"
:local ednshost "{example.f3322.net}"
:local ednsinterface "{pppoe-out1}"
:local members "http://members.3322.org/dyndns/update?system=dyndns"
:local status
:local status [/interface get [/interface find name=$ednsinterface] running]
:if ($status!=false) do={
:local ednslastip [:resolve $ednshost]
:if ([ :typeof $ednslastip ] = nil ) do={ :local ednslastip "0" }
:local ednsiph [ /ip address get [/ip address find interface=$ednsinterface ] address ]
:local ednsip [:pick $ednsiph 0 [:find $ednsiph "/"]]
:local ednsstr "&hostname=$ednshost&myip=$ednsip"
:if ($ednslastip != $ednsip) do={/tool fetch url=($members . $ednsstr) mode=http user=$ednsuser password=$ednspass dst-path=$ednshost
:delay 2
:local result [/file get $ednshost contents]
:log info ($ednshost . " " .$result)
/file remove $ednshost ;
}
}

主要的逻辑就是检查 pppoe-out1 的 ip 跟当前域名 example.f3322.net 解析出的ip是不是一致,不一致的话,就用 api 更新域名的 ip。

脚本放在 system - scripts 里面,命名为 DDNS ,这个脚背通过计划任务隔一段时间执行一次,一般还是不少于10分钟吧。计划任务在 system - scheduler 里面增加, OnEvent 里面填

DDNS

即可,DDNS 是上面定义的脚本的名称。

经过测试,能够顺利更新动态域名的 IP。

备注

命令

/tool fetch url=$url mode=http dst-path=$ednshost

只有在服务器返回有内容的响应才会执行成功,后面的代码才能继续运行。如果服务器返回一个内容为空的 200 响应(不是出错),这句也是执行不成功。

参考

DDNS动态域名脚本(花生壳+3322公云)for ROS 6.x
如何在ROS中设置花生壳服务

Asp.net core 的一个 model binding 问题

在做一些简单的基础数据 CRUD 时,出了个怪问题:有个 post 的 action 的参数无论如何都没法绑定上。

方法的签名大概如下:

public async Task<IActionResult> EditProductCatalog(ProductCatalogModel model)
{
    if (ModelState.IsValid)
    {
        // ……
    }
}

ModelState 没有问题,但 model 虽然不是为 null,但里面的所有属性都是默认值。也就是简单地 new 了一个对象出来而已。而其他类似的方法都能绑定成功。

找了一下原因,发现是是因为 ProductCatalogModel 有个属性叫 Model,估计是因为这个原因,Binder 没有能够正确的区分 action 方法参数的 model 和对象属性里面的 Model,导致绑定出错。

public class ProductCatalogModel
{
    // ……
    public string Model { get; set; }
    // ……
}

解决方法:将方法前面里面的参数名从 model 改成 m 即可。

解决因为卸载 vs2015 导致 LocalDB 没法使用的问题

这些天,看着 mSATA 128G 的 c 盘剩余空间所剩无几,想着已经安装了 vs2017,不如将 vs2015 卸载了,看能不能给 c 盘腾出多点空间。于是手贱将 vs2015 卸载掉了。控制面板卸载之后,还有很多相关的组件不会被卸载,看着碍眼,便更作死地找了微软的这个全面清除 vs 的工具,一下子将 vs 相关的东西全部干掉了。这下好了,不单 vs2015、还有早期残留的2013、2010、2008 都被干得干干净净。

但运行 vs2017 编译就报错了,找不到对应的组件。这个好办,运行下安装程序,修复一下,搞定了。ctrl + F5 编译通过,浏览器打开,跑起来了。

还是有问题,程序报异常:无法连接 sqlserver,找不到对应实例名称啥的。想用 SQL Server Management Studio 连一下,SSMS 也被 TotalUninstaller 一并干掉了,只好当场下载,安装。结果同样还是连不上,类似的错误。

估摸着应该是 TotalUninstaller 将 LocalDB 也干掉了,但看了下 visualstudio 2017 的安装程序,修复的时候应该也将 LocalDB 重新装上去了啊。不知道是不是装上去了,反正再下载一个 LocalDB 的安装程序,自己装下吧。于是下载了个 SQL Server 2016 LocalDB 的安装程序,装上去。测试下,仍然是有问题,未解决。

在 vs2017 的 server explorer 里面尝试添加一个 connection,说找不到实例。执行命令重新生成实例:

sqllocaldb delete MSSQLLocalDB
sqllocaldb create MSSQLLocalDB

重建了一个当前版本的实例,版本号为 13.*。直接将数据库文件 attach 上去,说不兼容的数据库。运行 cmd 命令

sqllocaldb v

结果在机器上报错,说访问注册表项返回 0 错误。

再想回来,我之前的 localdb 是基于 vs2015 自带的 SQL Server 2014 的 localdb,现在是 vs2017 的 SQL Server 2016 的 localdb,不兼容也正常。遂找到一个 sqlserver 2014 的安装程序,装了这个版本的 localdb。这下 sqllocadb v 列出了两列数据,一个是 12.* 的正常版本号,另一个本来应该是 13.* 版本号的,却显示同样的注册表项访问错误。既然显示错误,那么直接将 sqlserver 2016 localdb 卸载了。吸取经验,从控制面板卸载都是不干净的,直接运行它的安装程序,在安装程序里面选择卸载。再用 v 看了下,现在只剩下一个版本 12.* 了。

现在应该走上正道了。再用上面的指令删除 MSSQLLocalDB 再重建,得到了一个版本号为 12.* 的默认的 MSSQLLocalDB 实例。这下用 vs2017 的 server explorer 能顺利将数据库文件 attach 上去了,而且也能通过它的工具执行 sql 操作了。修改一下程序的 connection string,程序也顺利跑起来了。

但 SSMS 还是连不上,仔细看了下,擦,原来连的服务器是从 connection string 复制过来的:

(LocalDB)\\MSSQLLocalDB

而因为 connection string 是保存在 json 文件中的,将“\”写成了“\\”做转义。将它改过来:

(LocalDB)\MSSQLLocalDB

SSMS 也能顺利连上去了。因为这个乌龙,导致排查过程中花了不少时间。

至此,问题都解决了,避免了一次要重装系统的危机。总结一下主要问题在于 SSMS 连接服务器的乌龙,导致排查时间大大增加。另外一个是 localdb 不同版本的兼容性,导致问题。

解决 Package Cache 越来越大的问题

微软系的开发者的 C 盘铁定是不够用的,即使把 visual studio 安装在别的分区,C 盘的使用空间还是不断增长,再加上 dotnet core 各个版本还有其他相关的开发工具等一系列全家桶装下来,C 盘空间每况愈下捉襟见肘。

微软系的开发相关的软件的安装包,绝大部分都是采用 WiX 的安装工具链 Burn 来构建的。这个工具链生成的安装包,在软件顺利安装之后,为了保证以后修复、重新安装或者卸载等动作的顺利进行,会把安装程序包也塞到 %ProgramData%\Package Cache 目录下,即使软件安装的目标目录不是 C 盘也是如此,所以 C 盘就这样没有节制地膨胀起来。为解决这个问题,就需要想办法将 %ProgramData%\Package Cache 里面地那一坨东西放到其他盘,并且让应用知道到其他地方找这些玩意。

基于注册的策略重定向

新版本的 WiX 工具支持基于注册的策略重定向,即先注册一个重定向策略,将另一个位置设置为 Cache 的根目录,以后的软件 update 和安装都会使用新的根目录,不会再写进 %ProgramData%\Package Cache,从而很好地避免 C 盘膨胀的问题。

进行注册在提升权限的命令行中输入下面命令即可:

reg.exe add HKLM\Software\Policies\WiX\Burn /v PackageCache /d {X:\PackageCache}

其中 {X:\PackageCache} 替换为自己实际需要放置 Cache 的目录。

需要注意的是,在执行注册之前安装的程序,还是会使用原来的 %ProgramData%\Package Cache 放东西,而且执行注册不会自动将这些安装包自动转移过去,手动转移过去也是不行的。注册动作只会影响以后的行为。也就意味着如果你想将 visual studio 的安装包换个地方,那还得先卸载了再重新安装一次。当然头脑正常的人都不会这样做。

mount 一个 vhd 虚拟硬盘硬件到 %ProgramData%\Package Cache

主要的操作思路是:

  1. %ProgramData%\Package Cache 的内容复制到其他地方譬如 D:\PackageCache
  2. D:\PackageCache 制作成 vhd 虚拟硬盘文件
  3. 将 vhd 文件 mount 到 %ProgramData%\Package Cache,这样访问 %ProgramData%\Package Cache 实际上访问的就是 vhd 文件空间了。

这个方法比较复杂,详细可以参考 How to relocate the Package Cache

使用 Junction 目录来重定向

主要操作思路类似上面 mount 的,但不需要复杂的命令制作 vhd 文件和 mount。基本步骤如下:

  1. Package CacheC:\ProgramData 移动到 D:\

    move "C:\ProgramData\Package Cache" "D:\"
    
  2. 创建符号链接

    mklink /j "C:\ProgramData\Package Cache" "D:\Package Cache"
    

C:\ProgramData 下就出现了一个 Junction 目录,链接到 D:\Package Cache。程序读写 C:\ProgramData\Package Cache 中的内容实际上读写的是 D:\Package Cache 中的东西,但这个链接是文件系统上面的抽象,访问过程对于系统和应用是透明的,也就是系统和应用会以为一直有一个正常的 C:\ProgramData\Package Cache 在那里。

这个方法的问题在于 WiX 安装包卸载时不认识 Junction 目录,卸载时会把 C:\ProgramData\Package Cache 这个 link 删掉(虽然 D:\Package Cache 的内容还在,删除的只是 link,而不是内容),这样就导致后续系统和应用尝试使用 C:\ProgramData\Package Cache 时会出问题。目前没有特别好的解决方法,只好在卸载之后再创建一次关连。

我自己怎么搞呢?

首先,还是要使用最新的基于注册策略重定向功能来实现目的,这个功能是工具提供方特地提供的,就是为了解决 Package Cache 占空间的痛点,支持是最好的。

另外,由于这个方法不究以往,因此我同时还创建一个 Junction 链接到上面重定向的目标目录(将旧的内容都先复制过去),这样,不管是先前的安装包还是后面的安装包,都能放到同一个目录中,而且都能正常找到了。只需要在卸载程序的时候留意下 link 有没有被破坏,破坏了就重新创建一下就好了。用起来之后,肯定是旧的东西越来越少,新的东西全都走注册重定向,link 被删除的可能性就越来越小了。

号外

上面几种方法,除了基于注册策略的,其他两种可以用于转移 C:\Windows\Installer 目录,这个目录也巨大无比。其他超大的目录也可以视情况使用。

参考
Redirect the Package Cache using registry-based policy
How to relocate the Package Cache

如何避免值类型通过泛型接口使用导致的装箱

在 .NET 中,将 valueType 转化为接口会导致它装箱。那么如何避免装箱,又能使用 interface 的方法呢?fx 里面的代码提供了一种借鉴的思路。

譬如 fx 中,需要比较两个对象是不是相等时,需要类型实现 IEquatable<T> 接口,然后调用 Equals 方法判断。但如果直接使用 (IEquatable<T>)t.Equals 来调用,如果 T 为值类型,必然导致装箱。fx 中很机灵地引入了一个中介类型 EqualityComparer<T> 来解决这个问题,在 fx 的各种集合类中实际上都是用它来判断相等。

这个 EqualityComparer<T> 有一个子类型 GenericEqualityComparer<T>,平时比较主要都是承包给它。先看看它的实现:

internal class GenericEqualityComparer<T> : EqualityComparer<T> where T : IEquatable<T>
{
        [Pure]
        public override bool Equals(T x, T y)
        {
            if (x != null)
            {
                if (y != null) return x.Equals(y);
                return false;
            }
            if (y != null) return false;
            return true;
        }
}

它的奥妙在于,不是将 x 和 y 转换为 IEquatable<T> 来使用,而是利用了泛型的约束,保证 T 有接口的这个方法,但不是利用接口来调用,而是直接用 T.Equals 来调用,这样就避免在运行时进行接口转化,避免了值类型装箱。

Asp.net core model bind 的一个问题

项目中有个 model 类 A,开始使用是没什么问题的。后来为了方便 view 里面根据状态进行界面控制,给它加了个只有 get 的属性 A,代码示例如下:

class A
{
    public bool CanDelete
    {
        get
        {
            //....
            if (someObject.IsOver()) return false;
            //....
        }
    }
}

而这个 someObject 在我的所有代码路径中, CanDelete 被调用的时候,一定是存在一个非空的值的。但在实际使用中, post 到一个使用这个 model 做参数的 action 时,页面抛出了 NullReference 异常。检查了几遍代码也不得其解。

[HttpPost]
public IActionResult CreateA(A model)
{
    if (ModelState.IsValid)
    //....
}

考虑到这个 model 是那个 action 的一个参数,框架会自动对 model 进行绑定,因此推断框架在绑定的时候自作主张访问了 CanDelete 的值,由于绑定时 someObject 还没有存在,因此出现空引用。

解决方法,由于框架因为绑定的目的,只会读取对应 model 的属性,因此将其更改为方法,问题解决。

public bool CanDelete()
{
    //....
    if (someObject.IsOver()) return false;
    //....
}

看来框架干的事情比想象中的多,之前还以为绑定的时候,只需要查看属性类型和调用对应属性的 set 方法呢,没想到连 get 也读。这也提醒了我以后在 model 中非绑定的内容的话,最好用方法,而不是属性。

解决 RB450G 内网访问达不到千兆的问题

这几天架好家里的服务器,发现用笔记本通过路由器(rb450g)复制文件到服务器上,速度最多只有 50MB/s,而且速度不稳定,在 20MB/s ~ 40MB/s 之间来来回回,远远达不到千兆的性能。用笔记本接网线直连服务器复制文件,则轻松达到 110MB/s 的满速(两者都是千兆网卡)。通过路由器复制文件时看路由器的 cpu 使用率也不超过 50%,百思不得其解。google了下,也没发现什么有用资料,反而看到它自家的测试,速度确实达不到千兆。

本来想算了,另外买一个千兆交换机吧,TP-Link 的五口千兆交换机也不过99块。不过多买个设备又要多接电,又要想办法塞到哪个角落里面,比较麻烦。随手到淘宝买 rb450g 店铺维护的那个网站上面翻了下,发现一个帖子讲怎么在 RouterOS 里面设置端口的交换功能,眼前一亮,进去一看,果然是我要找的功能。

之前为了能让家里的设备都在一个网段,只能将所有内网端口都串在一个 bridge 上,然后将 DHCP 设在这个桥上,因此复制文件的时候,数据都在软桥上面走,路由器可能压力比较大。现在学习了交换功能,撤掉了桥,将内网端口设置在同一个交换(将 3/4/5 端口串在 2 上),并且将 DHCP 设置在端口 2 上,既保证了网段一致,又省下了桥接的开销。设置完成之后,内网机器复制文件轻松达到了 110MB/s 而且非常稳定。

设置方式如下:

如下图,我们可以看到一个 RB750 的5 个以太网接口,我们需需要将ether3、ether4 与ether2 进行交换功能配置:
端口列表

打开 ether3 和ether4 接口配置,并将Master Port 为ether2
设置

设置完成后,我们可以在 interface 列表中看到
设置完成

参考资料

[基础] RouterBOARD 交换功能配置

asp.net core Razor 对非英文字符输出编码的问题

问题

在 asp.net core 中,这样写代码:

@{
    string title = "标题"; // this is Chinese
}
<title>@title</title>

生成 html 后,会变成

<title>&#x4E2D;&#x6587;</title>

中文被编码了。虽然浏览器能够识别出里面的中文,显示没有问题,但这样还存在几个问题:

  • 本来两个字符,现在变成了一大堆,显著增加了页面的大小。
  • 页面对人可读性差,看页面源码时造成困扰。
  • 假如你需要跟 javascript 进行配合,直接用 '@title' 输出某个值为 js 某个变量的值,则会造成乱码。

原因

在 asp.net core 中,基于防范 xss 攻击的安全考虑,默认将所有非基本字符(U+0000..U+007F)的字符进行编码。因此基本除了英文字母那一部分,其他的全被编码了。

这个控制来源于 HtmlEncoder 中的 UnicodeRange 被设置成 UnicodeRanges.BasicLatin

解决办法

配置将 UnicodeRange 范围放宽。在 Startup 的 ConfigureServices 加入:

services.Configure<WebEncoderOptions>(options =>
{
    options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All);
});

中文就可以正常输出了。

aspnet/HttpAbstractions#315

Memory<T> 使用 guideline

根据 Memory<T> usage guidelines 总结。

由于 Span<T>Memory<T> 的区别,以及考虑到 ownership 和 consumption,在使用 Memory<T> 家族时最好遵循如下 guideline。

1. 对于同步的 API,尽可能使用 Span<T> 而不是 Memory<T>

因为 Span<T>Memory<T> 能用于更多的情形,而且很容易能变成 Memory<T>。反之则不行。

2. 如果 buffer 是用于不可变的情形,那么应使用 ReadOnlySpan<T>ReadOnlyMemory<T>

3. 如果你的方法接受 Memory<T> 而返回 void,那么禁止在方法返回后再使用那个 Memory<T>

错误示范:

// !!! INCORRECT IMPLEMENTATION !!!
static void Log(ReadOnlyMemory<char> message)
{
    // Run in background so that we don't block the main thread
    // while performing IO.
    Task.Run(() => {
        File.AppendText(message);
    });
}

在方法返回后,异步的代码还在使用 Memory<T>,万一返回后的代码对同样的 buffer 进行了修改,则 buffer 数据就全乱套了。

4. 如果你的方法接受 Memory<T> 而返回 Task,禁止在 Task 变为终结状态后继续使用这个 Memory<T>

3号规则的异步版本。Task<T> ValueTask<T> 和其他类似的类型同理。

5. 如果你的构造方法接受 Memory<T> 作为参数,那么构造出来的对象也被认为作为这个 Memory<T> 的 consumer。

6. 如果你的类型有个可以设置的 Memory<T> 类型属性或者实例方法,那么其他实例方法也被认为是这个 Memory<T> 的 consumer。

同上。

7. 如果你拥有一个 IMemoryOwner<T> 引用,那么你必须负责在某个时候 dispose 它或者将所有权转移到其他地方。(但只能干一个,不能都干。)

8. 如果你的 API 接受 IMemoryOwner<T> 参数,你正接受这个实例的 ownership。

9. 如果你正包装一个同步的 p/invoke 方法,应该接受 Span<T> 参数。

可以使用 fixed 关键字来 pin 住 Span<T> 实例。

using System.Runtime.InteropServices;

[DllImport(...)]
private static extern unsafe int ExportedMethod(byte* pbData, int cbData);

public unsafe int ManagedWrapper(Span<byte> data)
{
    fixed (byte* pbData = &MemoryMarshal.GetReference(data))
    {
        int retVal = ExportedMethod(pbData, data.Length);

        /* error checking retVal goes here */

        return retVal;
    }

    // In the above example, 'pbData' can be null; e.g., if
    // the input span is empty. If the exported method absolutely
    // requires that 'pbData' be non-null, even if 'cbData' is 0,
    // consider the following implementation.

    fixed (byte* pbData = &MemoryMarshal.GetReference(data))
    {
        byte dummy = 0;
        int retVal = ExportedMethod((pbData != null) ? pbData : &dummy, data.Length);

        /* error checking retVal goes here */

        return retVal;
    }
}

10. 如果你正包装一个异步的 p/invoke 方法,应该接受 Memory<T> 参数。

异步操作中不能使用 fixed 来 pin 住内存,取而代之,可以使用 Memory<T>.Pin 来 pin 住 Memory<T> 实例。

using System.Runtime.InteropServices;

[UnmanagedFunctionPointer(...)]
private delegate void OnCompletedCallback(IntPtr state, int result);

[DllImport(...)]
private static extern unsafe int ExportedAsyncMethod(byte* pbData, int cbData, IntPtr pState, IntPtr lpfnOnCompletedCallback);

private static readonly IntPtr _callbackPtr = GetCompletionCallbackPointer();

public unsafe Task<int> ManagedWrapperAsync(Memory<byte> data)
{
    // setup
    var tcs = new TaskCompletionSource<int>();
    var state = new MyCompletedCallbackState {
        Tcs = tcs
    };
    var pState = (IntPtr)GCHandle.Alloc();

    var memoryHandle = data.Pin();
    state.MemoryHandle = memoryHandle;

    // make the call
    int result;
    try {
        result = ExportedAsyncMethod((byte*)memoryHandle.Pointer, data.Length, pState, _callbackPtr);
    } catch {
        ((GCHandle)pState).Free(); // cleanup since callback won't be invoked
        memoryHandle.Dispose();
        throw;
    }

    if (result != PENDING)
    {
        // Operation completed synchronously; invoke callback manually
        // for result processing and cleanup.
        MyCompletedCallbackImplementation(pState, result);
    }

    return tcs.Task;
}

private static void MyCompletedCallbackImplementation(IntPtr state, int result)
{
    GCHandle handle = (GCHandle)state;
    var actualState = (MyCompletedCallbackState)state;
    handle.Free();
    actualState.MemoryHandle.Dispose();

    /* error checking result goes here */

    if (error) { actualState.Tcs.SetException(...); }
    else { actualState.Tcs.SetResult(result); }
}

private static IntPtr GetCompletionCallbackPointer()
{
    OnCompletedCallback callback = MyCompletedCallbackImplementation;
    GCHandle.Alloc(callback); // keep alive for lifetime of application
    return Marshal.GetFunctionPointerForDelegate(callback);
}

private class MyCompletedCallbackState
{
    public TaskCompletionSource<int> Tcs;
    public MemoryHandle MemoryHandle;
}

避免 .net core 中访问 static field 的性能问题

由于规范的限制(beforefieldinit),clr 在访问 static field 之前要先确认它已经被初始化了,因此每次访问 static field 都会导致一次初始化检查,对性能敏感的应用会造成伤害。.net core 就存在这个问题,而 .net framework 4.7 比较激进,直接就不检查了,虽然性能比较好,但其实这并不是符合规范的行为。

考虑如下代码,在 .net framework 和 .net core 中运行的性能就有很大的差别。

using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace BenchmarkApp
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<CustomBenchmarks>();
            Console.ReadLine();
        }
    }

    public class CustomBenchmarks
    {
        private static object _staticField = new object();
        [Benchmark] public object ReadStaticField() => _staticField;
    }
}

在 .net framework 4.7.1 的结果为:

             Method |      Mean |     Error |    StdDev |
------------------- |----------:|----------:|----------:|
    ReadStaticField | 0.0030 ns | 0.0086 ns | 0.0126 ns |

在 .net core 2.1 rc1 的结果为:

            Method |      Mean |     Error |    StdDev |
------------------ |----------:|----------:|----------:|
   ReadStaticField | 2.8088 ns | 0.1467 ns | 0.1372 ns |

显然 .net core 2.1 rc1 比 .net framework 4.7.1 慢得多。

分析它们 JIT 生成的代码分别为:

.framework 4.7.1

; static field access
00007FF9CCE90550  mov         rax,1F9EAE45A38h
00007FF9CCE9055A  mov         rax,qword ptr [rax]
00007FF9CCE9055D  ret

.net core 2.1 rc1

; static field access
00007FF9B3EF1510  sub         rsp,28h
00007FF9B3EF1514  mov         rcx,7FF9B3DD4B30h
00007FF9B3EF151E  mov         edx,3
00007FF9B3EF1523  call        00007FFA139E1D40        ; ensure the static fields are initialized
00007FF9B3EF1528  mov         rax,221AB092940h
00007FF9B3EF1532  mov         rax,qword ptr [rax]
00007FF9B3EF1535  add         rsp,28h
00007FF9B3EF1539  ret

多了跟比较相关的不少指令,因此慢了很多。

解决方法

那么,应该如何解决这个问题呢?

在非泛型的情况下,只需要简单地给 CustomBenchmarks 类加上一个 static 构造方法就行了。

public class CustomBenchmarks
{
    private static object _staticField = new object();
    private object _instanceField = new object();

    static CustomBenchmarks() { }       // <-- add this one
    // ...
}

.net core 2.1 rc1 就会生成直接访问的指令。

对于泛型的版本,在值类型的情况下,由于 jit 对每种值类型都会生成独立的实际实现,因此上面的方法也是可行的。对于引用类型,由于不同的引用类型共用同一个实现,所以 jit 还是会每次都会先检查是否已经初始化。建议在这种情况下,使用一个实例变量来将静态变量缓存起来,后面都使用实例变量来避免性能降低。

另外,还可以通过启用 tiered compilation,让 jit 能够根据实际的运行情况对生成的指令进行再优化来避免这种情况。目前这个选项需要手工打开,以后将会是默认的选项。

参考:
Strange performance fluctuations with static fields access

dotnetcore 无法获取 GBK(GB2312) Encoding 的问题

在使用自带的 zip 库的 ZipFile.ExtractToDirectory 时遇到个问题,解压缩出来的中文文件名/目录名变成了乱码。显然这里存在一个编码问题。查看文档,发现 ZipFile.ExtractToDirectory 有一个支持指定 Encoding 的重载。那么,应该采用哪个 Encoding 呢?

对于这种在中文 Windows 系统上文件名和文本文件的操作,在原来的 .net framework 中,一般是使用 Encoding.Default 就可以解决。Encoding.Default 会读取操作系统默认的 ANSI 编码设定,在中文 Windows 上也就是 GBK 编码。虽然语言不同的操作系统的默认设置不一样,比如英文的一般就是 ASCII,但对于之前只能运行在 Windows 上的 .net framework 来说,而且环境都是中文语言的前提下,这种设定通常都能解决问题。

而从 .net framework 4.6 起,内置的 encoding 就只支持 ASCIIISO-8859-1UTF-7UTF-8UTF-16/UTF-16LEUTF-16BEUTF-32/UTF-32LEUTF-32BE 这几种编码了,dotnet core 自然也延续了这种做法。因此,我们再使用 Encoding.Default,通常就只能得到 ASCII 的 encoding 了。即使你使用 Encoding.GetEncoding("GB2312") 来直接获取对应的 encoding,也只能得到一个异常 "'GB2312' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.",应该如何解决呢?

可以通过下面的步骤来解决:

  1. 引用 System.Text.Encoding.CodePages.dll,可通过 Nuget 引用。
  2. 加入以下代码,通常在应用启动的时候全局注册。
System.Text.EncodingProvider provider = System.Text.CodePagesEncodingProvider.Instance;
System.Text.Encoding.RegisterProvider(provider);

然后,就可以顺利地通过 Encoding.GetEncoding("GB2312")Encoding.GetEncoding("GBK") 获得我们亲切的走地中文编码了。

不建议使用 Encoding.Default 来进行编码操作,毕竟我们现在 dotnet core 应用可能运行在不同的操作系统上,不同的语言环境下,这种会随环境变化的方式实在太不可靠了。

参考
ANSI是什么编码?
Encoding.Default Property

将 RouterOS 用作 DNS 服务器

国内 DNS 解析充满着污染,而且对于墙外的某些不存在的网站,必须指定IP才能够访问,域名解析出来的IP肯定是不能访问的,因此需要实现自己域名解析。
对于单台机器来说,可以写hosts文件,但设备多了就麻烦,而且手机等设备也不好搞hosts,在路由器上面搭DNS服务器,将hosts文件转到路由器上,所有设备访问网络就比较方便了。

1.首先设置DNS服务器

允许 DNS 服务被访问

[admin@MikroTik] ip dns
[admin@MikroTik] ip dns> set allow-remote-requests=yes

DNS 设置 主界面
界面里面的动态DNS服务器,是 pppoe 拨号带过来的电信的 DNS 服务器

2.添加自己的域名解析列表

通过命令行添加

[admin@MikroTik] ip dns static> add name www.example.com address=10.0.0.1
[admin@MikroTik] ip dns static> add name www1.example.com address=10.0.0.2

自己用按照这个格式做个文本,一次性粘贴进去执行就行了。RouterOS也提供 API,可以编程自动添加。这个以后试了再写。

3.在DHCP中将当前路由器作为DNS服务器分发

在IP - DNS Server - Networks 中将 DNS Servers 设置为路由器IP
DHCP set DNS

命令行

[admin@MikroTik] ip dhcp-server network> set 0 dns-server 192.168.0.1
参考资料

Manual:IP/DNS - MikroTik Wiki
Manual:IP/DHCP Server - MikroTik Wiki

.net 中接口声明顺序会对性能造成影响

在 .net 中,将实例转换为接口,是按照接口的声明顺序进行线性查找的,因此某个类实现接口的声明顺序,会对它进行接口访问的性能有影响。对注重高性能的地方尤其需要注意,将注重性能和经常用到的接口放到前面,而将不常用的放到后面。

corefx 中就因为 string 实现接口的顺序变化,造成了大约 30% 的性能倒退修复的方式如下:

string interface order

解决控制面板设备和打印机页面打开非常缓慢的问题

老婆的笔记本打开控制面板的设备和打印机页面非常缓慢,每次打开这个页面,需要花费至少3分钟才能显示内容,而且这段时间里 cpu 使用率飙升,风扇呼呼作响。 cpu 强劲的话,打开速度会快点,但风扇声依然可观。

查看 eventvwr 没有相关信息,尝试将可能涉及的 windows 服务都禁用,也不起作用,将里面的设备,包括 Office 自带的 XPS printer,传真之类都删除,只剩下一个打印机,问题照旧。将多媒体设备全删掉,仍未解决。删掉打印机驱动再重装也不行。

不得已, bing 了一下,遇到类似问题的人很多,通常使用 Windows 8 或者 Windows 10,微软自己的问答网站也有,但微软的回答没什么用。一般说法是涉及蓝牙设备,服务里面将蓝牙服务设置为自动启动就行了。其实是没什么用的。

再仔细翻,有个人提及他经过一项项排查,发现可能是 Realtek 声卡驱动的问题,他装了旧版本的驱动没有问题,更新之后就出事了。受到启发,验证了一下,果然是 Realtex 声卡驱动的问题!真挺莫名其妙的,感觉风马牛不相及的问题啊。

evil driver

解决方法

打开设备管理器,声音、视频和游戏控制器里面找到 Realtek 声卡,右键,卸载,弹出卸载面版,选中删除此设备的驱动程序,确定。然后这个声卡就被删掉了。
uninstall driver

这时再扫描硬件改动,系统就会将声卡加回来,并且装上了微软自家的驱动。问题解决。现在声卡能正常使用,设备和打印机页面也秒开了。
good driver

若过段时间驱动自动更新回 Realtek 家的,在驱动程序 tab 将驱动回滚到上一个版本,即微软的版本即可。微软的驱动还能自动区分扬声器和耳机分别设置音量, Realtek 的还没办法区分,版本还老,还好意思瞎更新。

单纯禁用 RealTek 声卡也能使面版打开加快。

之前看到 Realtek 的声卡也是导致了另一个诡异问题 播放音乐导致计算机 cpu 性能越变越差 的罪魁祸首,这家的驱动还真是不靠谱呢。

MikroTik RouterOS 将http请求重定向到https

由于墙的存在,访问国外的http网站经常被重置,而且即使不重置,http请求也经常被运营商劫持,弹出个广告什么的,体验非常不好。

普通的访问可以通过使用浏览器访问https站点的方式使用,浏览器也可以收藏https站点,但对于google搜索出来的结果,例如stackoverflow的就结果,是http的,这样访问搜索出来的结果就要断个好几次才能访问到内容,实在是闹心。

因此需要进行一个配置,让特定网站的http访问自动转换为https方式。
实现方式可以通过浏览器扩展和路由器修改来实现。由于Edge当前正式版没有扩展支持,而且扩展方式没有路由器方式应用范围广,路由器设置好的话,所有设备所有浏览器都能够统一进行跳转,因此打算在路由器上动手脚。
现在使用路由器是MikroTik,系统是它家的RouterOS,能够很容易添加功能。

实现思路:

  • 路由器上建立一个webproxy
  • 设置透明代理将所有http访问站点IP的数据包都转发到这个webproxy上
  • webproxy禁止这些请求并返回一个错误页面
  • 修改这个错误页面内容,用javascript实现跳转

1. 在RouterOS上面设置一个WebProxy,允许匿名,并点击Reset HTML按钮,将生成默认的error.html,当访问被禁止的时候,会将此error.html的内容返回。

设置webproxy

[admin@MikroTik] ip proxy> set enabled=yes port=8080

Web Proxy Setting
Web Proxy Error File
需要访问并修改路由器文件的话,开放路由器的FTP功能,使用FTP客户端访问比较方便。

禁用所有到达这个webproxy的访问,目的是将访问内容替换为error.html的内容

/ip proxy access add action=deny

deny all

2. 设置透明代理,将路由器范围内的http访问自动重定向到代理服务器,不用浏览器显式进行代理设置

使用firewall将http请求转发到8080的代理端口

[admin@MikroTik] ip firewall nat
[admin@MikroTik] ip firewall nat> add chain=dstnat protocol=tcp dst-address=151.101.65.69 dst-port=80 action=redirect to-ports=8080

本来可以将所有请求都转发到webproxy,然后webproxy根据域名进行选择性重定向的,但这样webproxy要处理的请求太多,负荷大;而且访问经过一道中转,延迟大。
因此只将对应站点的访问重定向过去,例如 151.101.65.69 就是 stackoverflow.com 的IP

3. 修改error.html的内容实现跳转

将error.html的内容更改为:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <script type="text/javascript">
        var url = "$(url)";
        if (url.toLowerCase().indexOf("http:") == 0) {
            url = "https" + url.substring(4, url.length);
            window.location.href = url;
        }
    </script>
    <title>To HTTPS</title>
</head>
<body>
</body>
</html>

其中的 $(url) 为路由器提供的变量,表示当前在访问的 url,其实由于路由器发送error.html内容时,不会改变浏览器的访问地址,所以也可以用window.location.href 来获取。
其他的修改就随便自己喜欢了。

4. 将修改后error.html上传,覆盖原来的文件

然后访问http://stackoverflow.com 的任意一个url,都会跳转到https://stackoverflow.com 的对应url了。
https访问的是443端口,不会经过webproxy。

5. Done

结合将 RouterOS 用作 DNS 服务器使用非常方便。

参考资料

Manual:IP/Proxy - MikroTik Wiki
Howto to enable Mikrotik RouterOS Web Proxy in Transparent Mode

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.