0%

第二十七章 加快农业转移人口市民化

放开放宽除个别超大城市外的落户限制,试行以经常居住地登记户口制度。全面取消城区常住人口300万以下的城市落户限制,确保外地与本地农业转移人口进城落户标准一视同仁。全面放宽城区常住人口300万至500万的I型大城市落户条件。完善城区常住人口500万以上的超大特大城市积分落户政策,精简积分项目,确保社会保险缴纳年限和居住年限分数占主要比例,鼓励取消年度落户名额限制。健全以居住证为载体、与居住年限等条件相挂钩的基本公共服务提供机制,鼓励地方政府提供更多基本公共服务和办事便利,提高居住证持有人城镇义务教育、住房保障等服务的实际享有水平

第三十一章 深入实施区域重大战略

聚焦实现战略目标和提升引领带动能力,推动区域重大战略取得新的突破性进展,促进区域间融合互动、融通补充。

第一节 加快推动京津冀协同发展

紧抓疏解北京非首都功能“牛鼻子”,构建功能疏解政策体系,实施一批标志性疏解项目。高标准高质量建设雄安新区,加快启动区和起步区建设,推动管理体制创新。高质量建设北京城市副中心,促进与河北省三河、香河、大厂三县市一体化发展。推动天津滨海新区高质量发展,支持张家口首都水源涵养功能区和生态环境支撑区建设。提高北京科技创新中心基础研究和原始创新能力,发挥中关村国家自主创新示范区先行先试作用,推动京津冀产业链与创新链深度融合。基本建成轨道上的京津冀,提高机场群港口群协同水平。深化大气污染联防联控联治,强化华北地下水超采及地面沉降综合治理。

第四十八章 优化收入分配结构

坚持居民收入增长和经济增长基本同步、劳动报酬提高和劳动生产率提高基本同步,持续提高低收入群体收入,扩大中等收入群体,更加积极有为地促进共同富裕。

第一节 拓展居民收入增长渠道

坚持按劳分配为主体、多种分配方式并存,提高劳动报酬在初次分配中的比重。健全工资决定、合理增长和支付保障机制,完善最低工资标准和工资指导线形成机制,积极推行工资集体协商制度。完善按要素分配政策制度,健全各类生产要素由市场决定报酬的机制,探索通过土地、资本等要素使用权、收益权增加中低收入群体要素收入。完善国有企业市场化薪酬分配机制,普遍实行全员绩效管理。改革完善体现岗位绩效和分级分类管理的事业单位薪酬制度。规范劳务派遣用工行为,保障劳动者同工同酬。多渠道增加城乡居民财产性收入,提高农民土地增值收益分享比例,完善上市公司分红制度,创新更多适应家庭财富管理需求的金融产品。完善国有资本收益上缴公共财政制度,加大公共财政支出用于民生保障力度。

第二节 扩大中等收入群体

实施扩大中等收入群体行动计划,以高校和职业院校毕业生、技能型劳动者、农民工等为重点,不断提高中等收入群体比重。提高高校、职业院校毕业生就业匹配度和劳动参与率。拓宽技术工人上升通道,畅通非公有制经济组织、社会组织、自由职业专业技术人员职称申报和技能等级认定渠道,提高技能型人才待遇水平和社会地位。实施高素质农民培育计划,运用农业农村资源和现代经营方式增加收入。完善小微创业者扶持政策,支持个体工商户、灵活就业人员等群体勤劳致富。

第三节 完善再分配机制

加大税收、社会保障、转移支付等调节力度和精准性,发挥慈善等第三次分配作用,改善收入和财富分配格局。健全直接税体系,完善综合与分类相结合的个人所得税制度,加强对高收入者的税收调节和监管。增强社会保障待遇和服务的公平性可及性,完善兜底保障标准动态调整机制。规范收入分配秩序,保护合法收入,合理调节过高收入,取缔非法收入,遏制以垄断和不正当竞争行为获取收入。建立完善个人收入和财产信息系统。健全现代支付和收入监测体系

runtime-dom dom操作

  • createElement() 方法通过指定名称创建一个元素
  • createTextNode() 可创建文本节点。
  • node.nodeValue 设置节点值
  • el.innerText 设置标签中的文本内容
  • removeAttribute() 方法删除指定的属性
  • setAttribute() 方法添加指定的属性,并为其赋指定的值
  • insertBefore() 方法在您指定的已有子节点之前插入新的子节点。
  • appendChild() 方法向节点添加最后一个子节点。
  • removeChild() 方法可从子节点列表中删除某个节点。

李永乐老师讲笔记

融资融券

1、看涨 买入 融资
2、看跌 卖出 融券(借别人的钱卖)

借 p1
A —-> B —- > C
A <—- B <—- C
还 p2
空头 多头
3、股价上升 P1 < P2 B亏损 C盈利(无限)
股价下跌 P1 > P2 B盈利 C亏损

做空必须追加保证金

期货

有权利以某个价格卖/买股票

  • 1、看涨call 合约:一月后250买入一股。 (权利金)期权价格 10
    卖call
    A —-> B
    A <—- B
    返10

p > 250 B行权 得到 250 + 10
p < 250 B不行权 B亏了 10 A赚了10

B 杠杆买入 A白赚10的人。

  • 2、看跌 put 合约:一月后250卖入一股。 (权利金)期权价格 10
    卖call
    A —-> B
    A <—- B
    返10
    p > 250 B不行权 B亏了 10 A赚了10
    p < 250 B行权 B得到以股票付出250 价值 240

轧空

流通股 100
借80 卖80 借60 卖60
A —-> B —-> C —-> D —-> E
100 -80 80 -60 60

140 空头股比流通股多。
投资者抬高价格
华尔街大佬 买股票
买call

远期合约

(钱 <—-)
农民 将军
(—-> 稻米)

  • 作用:套期保值,通过合约锁定价值
  • 问题:毁约,质量,交易

标准期货合约

早期模型 {
质量 数量 时间 地点…,
场内交易
}

  • wtz原油期货 就是一份合同
    确定规定:{
    买卖合约内 获利
    }

期货投机

  • 价格 = 单价 * 规模
    某个时间点交割。
  • 开仓 平仓
    卖(空) — 卖(多) === 一定时间会平仓。
    价格高 多方赚了。

保证金

价格高
保证金
2000 卖(空3000) — 卖(多1000)
2100 卖(空3000) — 卖(剩1000) (卖方追加1100)
2500 卖(空7000) — 卖(剩-1900) (卖方追加4400)
此时平仓:空方赚5000,多方保证金赔光,还倒欠期货公司1900.
期货有杠杆,不能长期持有(到期,交割)。
卖方必须追加保证金,否则平仓。

到期,交割

  • 平仓 移仓换月(交易日前几天) (什么都不做)交割
  • 多单 以某个价格获取1000桶原油 空方(有油)不接价格,等交割,多方只有压价直到价格变负数 空方接受双方平仓。

新房合同开发商提供的制式合同,购房人想修改合同的可能性小,改合同,房子别买了。

  1. 仔细阅读合同

二手房,可以对合同进行补充。

  1. 房产证产权证有几个人需要几个人签字
  2. 打印房屋权属信息 (到房管部门打印一张房屋权属信息,可以看到房屋查封和抵押)
  3. 房子有没有对外出租 ”买卖不破租赁“ 租客时间长影像入住时间
  4. 对买房人迁移户口期限进行违约金约定 (卖方户口迁出)
  5. 征信
  6. 约定维权费用承担

注意

  1. 房产证单独共有,还是共同共有。
    房产证如何标注所有权。若共同共有,夫妻签字。
  2. 购房资质
    社保比较快。
  3. 购房是否能贷款。(如果贷不了会产生违约责任)
  4. 违约责任
  • 买方
    • 购房资质
    • 贷款资质 征信 (中介签合同,合同约定如果不能贷款的话,后期要把房款凑足)
    • 家具 基础设施(合同标注)
    • 交房 办证的日期 贷款日期银行定(贷款之后,多少日必须交房)

补充

  • 签合同
    1. 税费(买家承担契税,增值税,印花税)所有的合同都会写”所有税费都有买房承担“ (包个人所得税 按前房主人头交)
  • 贷款
    中介会安排按揭专员对接
    自己准备一下材料
    1.收入证明
    2.无房证明
    3.半年银行流水
    4.身份证
    5.户口本
    6.首付款
    面签(客户经理询问个人信息)–> 等审核
  • 房产过户 过完户才会放款给卖家(房产局双方都要到场,身份证,户口本,首付款,交税费,大头契税 1%征收 个税 土地增值税 手续费)
  • 领取房产证
  • 公积金提取
  • 银行办理抵押登记 (准备材料)中介代办 领到房产证
    1. 身份证
    2. 户口本
    3. 房产证
    4. 各种税票
  • 房屋交接
    1. 物业费是否结清
    2. 物业更名
    3. 水电燃气过户(中介办理)

面试指南

缓冲(Buffer)与缓存(Cache)的区别?

Buffer初识

在引入 TypedArray 之前,JavaScript 语言没有用于读取或操作二进制数据流的机制。 Buffer 类是作为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。这是来自 Node.js 官网的一段描述,比较晦涩难懂,总结起来一句话 Node.js 可以用来处理二进制流数据或者与之进行交互

Buffer 用于读取或操作二进制数据流,做为 Node.js API 的一部分使用时无需 require,用于操作网络协议、数据库、图片和文件 I/O 等一些需要大量二进制数据的场景。Buffer 在创建时大小已经被确定且是无法调整的,在内存分配这块 Buffer 是由 C++ 层面提供而不是 V8 具体后面会讲解。

在这里不知道你是否认为这是很简单的?但是上面提到的一些关键词二进制、流(Stream)、缓冲区(Buffer),这些又都是什么呢?下面尝试做一些简单的介绍。

什么是二进制数据?

谈到二进制我们大脑可能会浮想到就是 010101 这种代码命令
二进制数据使用 0 和 1 两个数码来表示的数据,为了存储或展示一些数据,计算机需要先将这些数据转换为二进制来表示。例如,我想存储 66 这个数字,计算机会先将数字 66 转化为二进制 01000010 表示,印象中第一次接触这个是在大学期间 C 语言课程中,转换公式如下所示:

128 64 32 16 8 4 2 1
0 1 0 0 0 0 1 0

上面用数字举了一个示例,我们知道数字只是数据类型之一,其它的还有字符串、图像、文件等。例如我们对一个英文 M 操作,在 JavaScript 里通过 'M'.charCodeAt() 取到对应的 ASCII 码之后(通过以上的步骤)会转为二进制表示。

什么是 Stream?

流,英文 Stream 是对输入输出设备的抽象,这里的设备可以是文件、网络、内存等。
流是有方向性的,当程序从某个数据源读入数据,会开启一个输入流,这里的数据源可以是文件或者网络等,例如我们从 a.txt 文件读入数据。相反的当我们的程序需要写出数据到指定数据源(文件、网络等)时,则开启一个输出流。当有一些大文件操作时,我们就需要 Stream 像管道一样,一点一点的将数据流出。
fountain_stream.jpg
通过上面的讲解进一步的理解了 Stream 是什么?那么 Stream 和 Buffer 之间又是什么关系呢?看以下介绍,关于 Stream 本身也有很多知识点

什么是 Buffer?

通过以上 Stream 的讲解,我们已经看到数据是从一端流向另一端,那么他们是如何流动的呢?

通常,数据的移动是为了处理或者读取它,并根据它进行决策。伴随着时间的推移,每一个过程都会有一个最小或最大数据量。如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要等待一定量的数据到达之后才能被处理。

这里的等待区就指的缓冲区(Buffer),它是计算机中的一个小物理单位,通常位于计算机的 RAM 中。这些概念可能会很难理解,不要担心下面通过一个例子进一步说明。

公共汽车站乘车例子

举一个公共汽车站乘车的例子,通常公共汽车会每隔几十分钟一趟,在这个时间到达之前就算乘客已经满了,车辆也不会提前发车,早到的乘客就需要先在车站进行等待。假设到达的乘客过多,后到的一部分则需要在公共汽车站等待下一趟车驶来。
bus-stop-1516549_1280.jpg
在上面例子中的等待区公共汽车站,对应到我们的 Node.js 中也就是缓冲区(Buffer),另外乘客到达的速度是我们不能控制的,我们能控制的也只有何时发车,对应到我们的程序中就是我们无法控制数据流到达的时间,可以做的是能决定何时发送数据。

Buffer基本使用

了解了 Buffer 的一些概念之后,我们来看下 Buffer 的一些基本使用,这里并不会列举所有的 API 使用,仅列举一部分常用的,更详细的可参考 Node.js 中文网。

创建Buffer

在 6.0.0 之前的 Node.js 版本中, Buffer 实例是使用 Buffer 构造函数创建的,该函数根据提供的参数以不同方式分配返回的 Buffer new Buffer()
现在可以通过 Buffer.from()、Buffer.alloc() 与 Buffer.allocUnsafe() 三种方式来创建
Buffer.from()

1
2
3
4
5
6
const b1 = Buffer.from('10');
const b2 = Buffer.from('10', 'utf8');
const b3 = Buffer.from([10]);
const b4 = Buffer.from(b3);

console.log(b1, b2, b3, b4); // <Buffer 31 30> <Buffer 31 30> <Buffer 0a> <Buffer 0a>

Buffer.alloc
返回一个已初始化的 Buffer,可以保证新创建的 Buffer 永远不会包含旧数据。

1
2
3
const bAlloc1 = Buffer.alloc(10); // 创建一个大小为 10 个字节的缓冲区

console.log(bAlloc1); // <Buffer 00 00 00 00 00 00 00 00 00 00>

Buffer.allocUnsafe
创建一个大小为 size 字节的新的未初始化的 Buffer,由于 Buffer 是未初始化的,因此分配的内存片段可能包含敏感的旧数据。在 Buffer 内容可读情况下,则可能会泄露它的旧数据,这个是不安全的,使用时要谨慎。

1
2
3
const bAllocUnsafe1 = Buffer.allocUnsafe(10);

console.log(bAllocUnsafe1); // <Buffer 49 ae c9 cd 49 1d 00 00 11 4f>

Buffer 字符编码
通过使用字符编码,可实现 Buffer 实例与 JavaScript 字符串之间的相互转换,目前所支持的字符编码如下所示:

  • ‘ascii’ - 仅适用于 7 位 ASCII 数据。此编码速度很快,如果设置则会剥离高位。
  • ‘utf8’ - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8。
  • ‘utf16le’ - 2 或 4 个字节,小端序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  • ‘ucs2’ - ‘utf16le’ 的别名。
  • ‘base64’ - Base64 编码。当从字符串创建 Buffer 时,此编码也会正确地接受 RFC 4648 第 5 节中指定的 “URL 和文件名安全字母”。
  • ‘latin1’ - 一种将 Buffer 编码成单字节编码字符串的方法(由 RFC 1345 中的 IANA 定义,第 63 页,作为 Latin-1 的补充块和 C0/C1 控制码)。
  • ‘binary’ - ‘latin1’ 的别名。
  • ‘hex’ - 将每个字节编码成两个十六进制的字符。
    1
    2
    const buf = Buffer.from('hello world', 'ascii');
    console.log(buf.toString('hex')); // 68656c6c6f20776f726c64

    字符串与 Buffer 类型互转

字符串转 Buffer
这个相信不会陌生了,通过上面讲解的 Buffer.form() 实现,如果不传递 encoding 默认按照 UTF-8 格式转换存储

1
2
3
4
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17

Buffer 转换为字符串
Buffer 转换为字符串也很简单,使用 toString([encoding], [start], [end]) 方法,默认编码仍为 UTF-8,如果不传 start、end 可实现全部转换,传了 start、end 可实现部分转换(这里要小心了)

1
2
3
4
5
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 9)); // Node.js �

运行查看,可以看到以上输出结果为 Node.js � 出现了乱码,为什么?

转换过程中为什么出现乱码?

首先以上示例中使用的默认编码方式 UTF-8,问题就出在这里一个中文在 UTF-8 下占用 3 个字节,技 这个字在 buf 中对应的字节为 8a 80 e6 而我们的设定的范围为 0~9 因此只输出了 8a,这个时候就会造成字符被截断出现乱码。
下面我们改下示例的截取范围:

1
2
3
4
5
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 11)); // Node.js 技

Buffer内存机制

由于 Buffer 需要处理的是大量的二进制数据,假如用一点就向系统去申请,则会造成频繁的向系统申请内存调用,所以 Buffer 所占用的内存不再由 V8 分配,而是在 Node.js 的 C++ 层面完成申请,在 JavaScript 中进行内存分配。因此,这部分内存我们称之为堆外内存
注意:以下使用到的buffer.js源码为 Node.js v10.x 版本,地址:https://github.com/nodejs/node/blob/v10.x/lib/buffer.js

Buffer内存分配原理

Node.js 采用了 slab 机制进行预先申请、事后分配,是一种动态的管理机制。
使用 Buffer.alloc(size) 传入一个指定的 size 就会申请一块固定大小的内存区域,slab 具有如下三种状态:

  • full:完全分配状态
  • partial:部分分配状态
  • empty:没有被分配状态

8KB 限制
Node.js 以 8KB 为界限来区分是小对象还是大对象,在 buffer.js 中可以看到以下代码

1
Buffer.poolSize = 8 * 1024; // 102 行,Node.js 版本为 v10.x

在Buffer初识一节里有提到过 Buffer在创建时大小已经被确定且是无法调整的到这里应该就明白了。
Buffer 对象分配
以下代码示例,在加载时直接调用了 createPool() 相当于直接初始化了一个 8 KB 的内存空间,这样在第一次进行内存分配时也会变得更高效。另外在初始化的同时还初始化了一个新的变量poolOffset = 0这个变量会记录已经使用了多少字节。

1
2
3
4
5
6
7
8
9
10
11
Buffer.poolSize = 8 * 1024;
var poolSize, poolOffset, allocPool;

... // 中间代码省略

function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeArrayBuffer(poolSize);
poolOffset = 0;
}
createPool(); // 129 行

此时,新构造的 slab 如下所示:
slab_poolOffset_0.png
现在让我们来尝试分配一个大小为 2048 的 Buffer 对象,代码如下所示:

1
Buffer.alloc(2 * 1024)

现在让我们先看下当前的 slab 内存是怎么样的?如下所示:
slab_poolOffset_2048.png
那么这个分配过程是怎样的呢?让我们再看 buffer.js 另外一个核心的方法 allocate(size)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// https://github.com/nodejs/node/blob/v10.x/lib/buffer.js#L318
function allocate(size) {
if (size <= 0) {
return new FastBuffer();
}

// 当分配的空间小于 Buffer.poolSize 向右移位,这里得出来的结果为 4KB
if (size < (Buffer.poolSize >>> 1)) {
if (size > (poolSize - poolOffset))
createPool();
var b = new FastBuffer(allocPool, poolOffset, size);
poolOffset += size; // 已使用空间累加
alignPool(); // 8 字节内存对齐处理
return b;
} else { // C++ 层面申请
return createUnsafeBuffer(size);
}
}

读完上面的代码,已经很清晰的可以看到何时会分配小 Buffer 对象,又何时会去分配大 Buffer 对象。

Buffer 内存分配总结

这块内容着实难理解,翻了几本 Node.js 相关书籍,朴灵大佬的「深入浅出 Node.js」Buffer 一节还是讲解的挺详细的,推荐大家去阅读下。

  1. 在初次加载时就会初始化 1 个 8KB 的内存空间,buffer.js 源码有体现
  2. 根据申请的内存大小分为 小 Buffer 对象 和 大 Buffer 对象
  3. 小 Buffer 情况,会继续判断这个 slab 空间是否足够
  4. 如果空间足够就去使用剩余空间同时更新 slab 分配状态,偏移量会增加
  5. 如果空间不足,slab 空间不足,就会去创建一个新的 slab 空间用来分配
  6. 大 Buffer 情况,则会直接走 createUnsafeBuffer(size) 函数
  7. 不论是小 Buffer 对象还是大 Buffer 对象,内存分配是在 C++ 层面完成,内存管理在 JavaScript 层面,最终还是可以被 V8 的垃圾回收标记所回收。

Buffer应用场景

以下列举一些 Buffer 在实际业务中的应用场景

I/O 操作

关于 I/O 可以是文件或网络 I/O,以下为通过流的方式将 input.txt 的信息读取出来之后写入到 output.txt 文件,关于 Stream 与 Buffer 的关系不明白的在回头看下 Buffer 初识 一节讲解的 什么是 Stream?、什么是 Buffer?

1
2
3
4
5
6
const fs = require('fs');

const inputStream = fs.createReadStream('input.txt'); // 创建可读流
const outputStream = fs.createWriteStream('output.txt'); // 创建可写流

inputStream.pipe(outputStream); // 管道读写

在 Stream 中我们是不需要手动去创建自己的缓冲区,在 Node.js 的流中将会自动创建。

zlib.js

zlib.js 为 Node.js 的核心库之一,其利用了缓冲区(Buffer)的功能来操作二进制数据流,提供了压缩或解压功能。参考源代码 zlib.js 源码

加解密

在一些加解密算法中会遇到使用 Buffer,例如 crypto.createCipheriv 的第二个参数 key 为 String 或 Buffer 类型,如果是 Buffer 类型,就用到了本篇我们讲解的内容,以下做了一个简单的加密示例,重点使用了 Buffer.alloc() 初始化一个实例(这个上面有介绍),之后使用了 fill 方法做了填充,这里重点在看下这个方法的使用。
buf.fill(value[, offset[, end]][, encoding])

  • value: 第一个参数为要填充的内容
  • offset: 偏移量,填充的起始位置
  • end: 结束填充 buf 的偏移量
  • encoding: 编码集

以下为 Cipher 的对称加密 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const crypto = require('crypto');
const [key, iv, algorithm, encoding, cipherEncoding] = [
'a123456789', '', 'aes-128-ecb', 'utf8', 'base64'
];

const handleKey = key => {
const bytes = Buffer.alloc(16); // 初始化一个 Buffer 实例,每一项都用 00 填充
console.log(bytes); // <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
bytes.fill(key, 0, 10) // 填充
console.log(bytes); // <Buffer 61 31 32 33 34 35 36 37 38 39 00 00 00 00 00 00>

return bytes;
}

let cipher = crypto.createCipheriv(algorithm, handleKey(key), iv);
let crypted = cipher.update('Node.js 技术栈', encoding, cipherEncoding);
crypted += cipher.final(cipherEncoding);

console.log(crypted) // jE0ODwuKN6iaKFKqd3RF4xFZkOpasy8WfIDl8tRC5t0=

Buffer VS Cache

缓冲(Buffer)与缓存(Cache)的区别?
缓冲(Buffer)
缓冲(Buffer)是用于处理二进制流数据,将数据缓冲起来,它是临时性的,对于流式数据,会采用缓冲区将数据临时存储起来,等缓冲到一定的大小之后在存入硬盘中。视频播放器就是一个经典的例子,有时你会看到一个缓冲的图标,这意味着此时这一组缓冲区并未填满,当数据到达填满缓冲区并且被处理之后,此时缓冲图标消失,你可以看到一些图像数据。
缓存(Cache)
缓存(Cache)我们可以看作是一个中间层,它可以是永久性的将热点数据进行缓存,使得访问速度更快,例如我们通过 Memory、Redis 等将数据从硬盘或其它第三方接口中请求过来进行缓存,目的就是将数据存于内存的缓存区中,这样对同一个资源进行访问,速度会更快,也是性能优化一个重要的点。

Buffer VS String

通过压力测试来看看 String 和 Buffer 两者的性能如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http');
let s = '';
for (let i=0; i<1024*10; i++) {
s+='a'
}

const str = s;
const bufStr = Buffer.from(s);
const server = http.createServer((req, res) => {
console.log(req.url);

if (req.url === '/buffer') {
res.end(bufStr);
} else if (req.url === '/string') {
res.end(str);
}
});

server.listen(3000);

以上实例我放在虚拟机里进行测试,你也可以在本地电脑测试,使用 AB 测试工具。

测试 string

看以下几个重要的参数指标,之后通过 buffer 传输进行对比

  • Complete requests: 21815
  • Requests per second: 363.58 [#/sec] (mean)
  • Transfer rate: 3662.39 [Kbytes/sec] received
    1
    $ ab -c 200 -t 60 http://192.168.6.131:3000/string
    ab_string

    测试 buffer

    可以看到通过 buffer 传输总共的请求数为 50000、QPS 达到了两倍多的提高、每秒传输的字节为 9138.82 KB,从这些数据上可以证明提前将数据转换为 Buffer 的方式,可以使性能得到近一倍的提升。
  • Complete requests: 50000
  • Requests per second: 907.24 [#/sec] (mean)
  • Transfer rate: 9138.82 [Kbytes/sec] received
    1
    $ ab -c 200 -t 60 http://192.168.6.131:3000/buffer
    ab_buffer
    在 HTTP 传输中传输的是二进制数据,上面例子中的 /string 接口直接返回的字符串,这时候 HTTP 在传输之前会先将字符串转换为 Buffer 类型,以二进制数据传输,通过流(Stream)的方式一点点返回到客户端。但是直接返回 Buffer 类型,则少了每次的转换操作,对于性能也是有提升的。

在一些 Web 应用中,对于静态数据可以预先转为 Buffer 进行传输,可以有效减少 CPU 的重复使用(重复的字符串转 Buffer 操作)。

Crypto加解密模块

Crypto 加密模块是 C/C++ 实现这些算法后,暴露为 javascript 接口的模块,包含对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。

Cipher

Cipher 类用于加密数据,属于对称密钥加密,假设通信双方 A、B 通讯方 A 使用 key 对明文进行加密传输,通讯方 B 接收到密文后,使用同样的 key 进行解密得到明文。

AES/ECB/PKCS5Padding

  • AES:代表算法
  • ECB:代表模式
  • PKCS5Padding:代表填充量

    与java、c#等语言交互踩过的坑

    和 java 程序进行交互的时候,Java 那边使用 AES 128 位填充模式:AES/CBC/PKCS5Padding 加密方法,在 Nodejs 中采用对应的 aes-128-cbc 加密方法就能对应上,因为有使用向量(iv),但是 Nodejs 语言本身不默认自动填充,所以 Nodejs 中要用 createCipheriv 方法,来补全填充量,而不是 createCipher。

    查看系统所支持的算法

    使用 openssl list-cipher-algorithms 可以查看系统所支持的算法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    aes-128-cbc    aes-128-ecb    aes-192-cbc    aes-192-ecb    aes-256-cbc
    aes-256-ecb base64 bf bf-cbc bf-cfb
    bf-ecb bf-ofb cast cast-cbc cast5-cbc
    cast5-cfb cast5-ecb cast5-ofb des des-cbc
    des-cfb des-ecb des-ede des-ede-cbc des-ede-cfb
    des-ede-ofb des-ede3 des-ede3-cbc des-ede3-cfb des-ede3-ofb
    des-ofb des3 desx rc2 rc2-40-cbc
    rc2-64-cbc rc2-cbc rc2-cfb rc2-ecb rc2-ofb
    rc4 rc4-40 seed seed-cbc seed-cfb
    seed-ecb seed-ofb

    Cipher加解密实例演示

    推荐使用 crypto.createCipheriv

    数据加密

  • crypto.createCipher(algorithm’, pwd) 指定算法、密码创建 cipher 加密对象
  • crypto.createCipheriv(algorithm, pwd, iv) 指定算法、密码、向量创建 cipher 加密对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function cipher(str){
    try{
    const crypto = require('crypto');
    // const cipher = crypto.createCipher('des-ecb', '123456');

    const cipher = crypto.createCipheriv('des-ecb', '12345678', ''); 与其他语言加密采用这种写法

    /**
    * update方法
    * 第一个参数代表加密的数据
    * 第二参数代表传入数据的格式,可以是'utf8', 'ascii', 'latin1'
    * 第三个参数代表加密数据的输出格式,可以是'latin1', 'base64' 或者 'hex'。没有执行则返回Buffer
    */
    let encrypted = cipher.update(str, 'utf8', 'hex');

    /**
    * final方法,返回任何加密的内容
    * 参数可以是'latin1', 'base64' 或者 'hex',没有指定返回Buffer
    */
    encrypted += cipher.final('hex');

    return encrypted;
    }catch(e){
    console.log('加密失败');

    return e.message || e;
    }
    }

    cipher('hello world !!!') // 81c66a1d39d302205c55f0afac95c06bc985155d4ddb751c

    数据解密

  • crypto.createDecipher(algorithm, pwd) 指定算法、密码创建 decipher 解密对象
  • crypto.createDecipheriv(algorithm, pwd, iv) 指定算法、密码、向量创建 decipher 解密对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function decipher(encrypted){
    try{
    const crypto = require('crypto');
    // const decipher = crypto.createDecipher('des-ecb', '123456');

    const decipher = crypto.createDecipheriv('des-ecb', '12345678', '');

    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
    }catch(e){
    console.log('解密失败');

    return e.message || e;
    }
    }

    decipher('81c66a1d39d302205c55f0afac95c06bc985155d4ddb751c'); // hello world !!!

MD5加密

MD5作用与特点

  • 作用: 是让大容量信息在数字签名软件签署私人秘钥前被 “压缩” 成一种保密格式,也就是把一个任意长度的字节串变换成一定长度的十六进制数字串(32个字符) 一致性验证
  • 特点
    • 不可逆
    • 输入两个不同的明文不会得到相同的输出值
    • 根据输出值,不能得到原始的明文,即过程不可逆

      MD5三种实现方式

  • crypto.createHash(algorithm)
    创建并返回一个 hash 对象,它是一个指定算法的加密 hash,用于生成 hash 摘要。
    参数 algorithm 可选择系统上安装的 OpenSSL 版本所支持的算法。例如:sha1、md5、sha256、sha512 等。在近期发行的版本中,openssl list-message-digest-algorithms 会显示这些可用的摘要算法。
  • hash.update(data)
    更新 hash 的内容为指定的 data。当使用流数据时可能会多次调用该方法。
  • hash.digest(encoding=’binary’)
    计算所有传入数据的 hash 摘要。参数 encoding(编码方式)可以为 hex、binary、base64
  • MD5加解密实例演示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const crypto = require('crypto');
    const md5 = str => {
    return crypto.createHash('md5').update(str, 'utf8').digest('hex')
    };

    // 默认输出长度为32位小写字母
    // 25f9e794323b453885f5181f1b624d0b
    console.log(md5('123456789'));

    // 以下转换为32位大写字母
    // 25F9E794323B453885F5181F1B624D0B
    console.log(md5('123456789').toUpperCase());

在 Node.js 中一个很重要的模块 Events(EventEmitter 事件触发器),也称为发布/订阅模式,为什么说它重要,因为在 Node.js 中绝大多数模块都依赖于此,例如 Net、HTTP、FS、Stream 等,除了这些系统模块比较知名的 Express、Koa 框架中也能看到 EventEmitter 的踪迹。

谈起事件前端的同学可能会联想到浏览器中的事件,与浏览器中的事件不同的是它不存在事件冒泡、preventDefault()、stopPropagation() 等方法,EventEmitter 提供了 on()、once()、removeListener() 等方法来对事件进行监听移除。

通过本文你能学到什么

  • 了解 EventEmitter 是什么?一些基础 API 的使用
  • 在 Node.js 的一些核心模块(Stream、Net)中是如何使用 EventEmitter 的?
  • 主流的 Express/Koa 框架也是基于此实现,我们如何实现一个基于 EventEmitter 的自定义对象?
  • 高并发场景下雪崩问题如何利用 EventEmitter 特性解决?
  • 事件是否等价于异步?

先从一个简单的例子开始

事件驱动是 Node.js 的核心,怎么体现事件驱动呢?通常一种最常见的形式就是回调,触发一次事件,然后通过回调来接收一些处理,关于这种形式在 JavaScript 编程中屡见不鲜,例如 fs.readFile(path, callback)、TCP 中的 server.on(‘data’, callback) 等。

一个简单的实现

主要用到以下两个 API,触发、注册一个监听函数。

自定义 EventEmitter 类

当你了解了 EventEmitter,你会发现它在 Node.js 中无所不在,Node.js 的核心模块、Express/Koa 等知名框架中,你都会发现它的踪迹,例如,下面在 Koa 中 new 一个 app 对象,通过 app.emit() 触发一个事件,实现在整个系统中进行传递。

1
2
3
4
5
6
7
8
const Koa = require('koa');
const app = new Koa();

app.on("koa", function() {
console.log("在 Koa 中使用 EventEmitter");
});

app.emit("koa");

系统模块自定义 EventEmitter 类的实现

在这开始之前让我们先看下 Node.js 中的 Stream、Net 模块是怎么实现的?
在 Stream 模块中的实现

1
2
3
4
5
6
7
8
9
// https://github.com/nodejs/node/blob/v10.x/lib/internal/streams/legacy.js#L6

const EE = require('events');
const util = require('util');

function Stream() {
EE.call(this);
}
util.inherits(Stream, EE);

在 Net 模块中的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
// https://github.com/nodejs/node/blob/v10.x/lib/net.js#L1121
const EventEmitter = require('events');
const util = require('util');

function Server(options, connectionListener) {
if (!(this instanceof Server))
return new Server(options, connectionListener);

EventEmitter.call(this);

...
}
util.inherits(Server, EventEmitter);

观察上面两个 Node.js 模块的自定义 EventEmitter 实现,都有一个共同点使用了 util.inherits(constructor, superConstructor) 方法,这个是 Node.js 中的工具类,这让我想起来了之前在看 JavaScript 权威指南(第 6 章 122 页)中的一个方法 function inherit(p),意思为通过原型继承创建一个新对象,而 util.inherits 是通过原型复制来实现的对象间的继承。

例如上面的 util.inherits(Server, EventEmitter) 函数,也就是 Server 对象继承了 EventEmitter 在原型中定义的函数,也就拥有了 EventEmitter 事件触发器中的 on、emit 等方法。但是现在 Node.js 官网不建议使用 util.inherits() 方法,而是使用 ES6 中的 class 和 extends 关键词获得语言层面的继承支持,那么在原声 JS 中还是使用 Object.setPrototypeOf() 来实现的继承,因此在 Node.js 12x 版本中你会看到如下代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
// https://github.com/nodejs/node/blob/v12.x/lib/net.js#L1142
function Server(options, connectionListener) {
if (!(this instanceof Server))
return new Server(options, connectionListener);

EventEmitter.call(this);

...
}

// https://github.com/nodejs/node/blob/v12.x/lib/net.js#L1188
Object.setPrototypeOf(Server.prototype, EventEmitter.prototype);
Object.setPrototypeOf(Server, EventEmitter);

实现一个基于 EventEmitter 的自定义类

这里用一个例子一天的计划来展示如何基于 EventEmitter 自定义类,在不同的时间触发相应的事件,通过监听事件来做一些事情。
下面展示了我们自定义的 OneDayPlan 是如何继承于 EventEmitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const EventEmitter = require('events');
const oneDayPlanRun = {
"6:00": function() {
console.log(`现在是早上 6:00,起床,开始新的一天加油!`);
},
"7:00": function() {
console.log(`现在是早上 7:00,吃早饭!`);
}
}

function OneDayPlan() {
EventEmitter.call(this);
}

Object.setPrototypeOf(OneDayPlan.prototype, EventEmitter.prototype);
Object.setPrototypeOf(OneDayPlan, EventEmitter);

现在让我们实例化上面自定义的 OneDayPlan 类,实现事件的触发/监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const oneDayPlan = new OneDayPlan();

oneDayPlan.on("6:00", function() {
oneDayPlanRun["6:00"]();
});

oneDayPlan.on("7:00", function() {
oneDayPlanRun["7:00"]();
});

async function doMain() {
oneDayPlan.emit("6:00");

await sleep(2000); // 间隔 2 秒钟输出

oneDayPlan.emit("7:00");
}

doMain();

async function sleep(s) {
return new Promise(function(reslve) {
setTimeout(function() {
reslve(1);
}, s);
});
}
1
2
现在是早上 6:00,起床,开始新的一天加油!
现在是早上 7:00,吃早饭!

EventEmitter 解决高并发下雪崩问题

对于需要查询 DB 的数据,我们一般称之为热点数据,这类数据通常是要在 DB 之上增加一层缓存,但是在高并发场景下,如果这个缓存正好失效,此时就会有大量的请求直接涌入数据库,对数据库造成一定的压力,对于缓存雪崩的解决方案,网上也不乏有更好的解决方案,但是在 Node.js 中我们可以利用 events 模块提供的 once() 方法来解决。

once 方法介绍

当触发多次相同名称事件,通过 once 添加的侦听器只会执行一次,并且在执行之后会接触与它关联的事件,相当于 on 方法和 removeListener 方法的组合,

1
2
3
4
5
6
7
8
9
10
11
12
proxy.once('我很帅', function() {
console.log('once: 我很帅!');
});

proxy.on('我很帅', function() {
console.log('on: 我很帅!');
});


proxy.emit('我很帅');
proxy.emit('我很帅');
proxy.emit('我很帅');

上面触发了三次 “我很帅” 事件,on 方法乖乖的重复了三次,但是 once 方法说我知道我很帅我只说一次就够了。

1
2
3
4
once: 我很帅!
on: 我很帅!
on: 我很帅!
on: 我很帅!

上面说的 once 方法是 on 和 removeListener 的结合体,在源码中也可看到 https://github.com/nodejs/node/blob/v10.x/lib/events.js#L282 once 方法接收到信息之后使用 on 方法监听,在 onceWrapper 方法中通过 removeListener 删掉监听函数自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function onceWrapper(...args) {
if (!this.fired) {
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
return Reflect.apply(this.listener, this.target, args);
}
}

function _onceWrap(target, type, listener) {
var state = { fired: false, wrapFn: undefined, target, type, listener };
var wrapped = onceWrapper.bind(state);
wrapped.listener = listener;
state.wrapFn = wrapped;
return wrapped;
}

EventEmitter.prototype.once = function once(type, listener) {
checkListener(listener);

this.on(type, _onceWrap(this, type, listener));
return this;
};

编码实现

利用 once 方法将所有请求的回调都压入事件队列中,对于相同的文件名称查询保证在同一个查询开始到结束的过程中永远只有一次,如果是 DB 查询也避免了重复数据带来的数据库查询开销。代码编写参考了深入浅出 Nodejs Events 模块一书,这里使用 fs 进行文件查询,如果是 DB 也同理,另外注意使用 status 键值对形式保存了触发/监听的事件名称和状态,最后建议进行清除,避免引起大对象导致内存泄露问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const events = require('events');
const emitter = new events.EventEmitter();
const fs = require('fs');
const status = {};

const select = function(file, filename, cb) {
emitter.once(file, cb);

if (status[file] === undefined) {
status[file] = 'ready'; // 不存在设置默认值
}
if (status[file] === 'ready') {
status[file] = 'pending';
fs.readFile(file, function(err, result) {
console.log(filename);
emitter.emit(file, err, result.toString());
status[file] = 'ready';

setTimeout(function() {
delete status[file];
}, 1000);
});
}
}

for (let i=1; i<=11; i++) {
if (i % 2 === 0) {
select(`/tmp/a.txt`, 'a 文件', function(err, result) {
console.log('err: ', err, 'result: ', result);
});
} else {
select(`/tmp/b.txt`, 'b 文件', function(err, result) {
console.log('err: ', err, 'result: ', result);
});
}
}

控制台运行以上代码进行测试,虽然发起了多次文件查询请求,fs 模块真正只执行了两次,分别查询了 a、b 两个文件,对于相同的请求,通过利用事件监听器 once 的特性避免了相同条件重复查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
b 文件
err: null result: b
err: null result: b
err: null result: b
err: null result: b
err: null result: b
err: null result: b
err: null result: b
a 文件
err: null result: a
err: null result: a
err: null result: a
err: null result: a
err: null result: a

默认情况下,如果为特定事件添加了超过 10 个监听器,则 EventEmitter 会打印一个警告。 但是,并不是所有的事件都要限制 10 个监听器。 emitter.setMaxListeners() 方法可以为指定的 EventEmitter 实例修改限制。

1
2
(node:88835) Warning: Possible EventEmitter memory leak detected. 11 /tmp/b.txt listeners added. Use emitter.setMaxListeners() to increase limit
(node:88835) Warning: Possible EventEmitter memory leak detected. 11 /tmp/a.txt listeners added. Use emitter.setMaxListeners() to increase limit

EventEmitter 循环调用问题

如下代码所示,尝试分析以下两种情况的输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const events = require('events');
const emitter = new events.EventEmitter();
const test = () => console.log('test');

/** 例一 */
emitter.on('test', function() {
test();
emitter.emit('test');
})

emitter.emit('test');

/** 例二 */
emitter.on('test', function() {
test();
emitter.on('test', test);
})

emitter.emit('test');

例一因为在监听函数 on 里执行了 emit 事件触发,会陷入死循环导致栈溢出。
例二结果为只输出一次 test,emitter.on(‘test’, test); 这行代码只是在当前的事件回调中添加了一个事件监听器。

1
2
例一:RangeError: Maximum call stack size exceeded
例二:test

同步还是异步

换一个问题事件是否等于异步?答案是不等的,看以下代码示例执行顺序,先输出 111 再输出 222,为什么这样?摘自官方 API 的一段话 “EventEmitter 会按照监听器注册的顺序同步地调用所有监听器。 所以必须确保事件的排序正确,且避免竞态条件。”

1
2
3
4
5
6
7
8
9
10
11
12
const events = require('events');
const emitter = new events.EventEmitter();

emitter.on('test',function(){
console.log(111)
});
emitter.emit('test');
console.log(222)

// 输出
// 111
// 222

也可以使用 setImmediate() 或 process.nextTick() 切换到异步模式,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const events = require('events');
const emitter = new events.EventEmitter();

emitter.on('test',function(){
setImmediate(() => {
console.log(111);
});
});
emitter.emit('test');
console.log(222)

// 输出
// 222
// 111

错误处理

最后一个最重要的错误处理,在 Node.js 中错误处理是一个需要重视的事情,一旦抛出一个错误没有人为处理,可能造成的结果是进程自动退出,如下代码因为事件触发器带有错误信息,而没有相应的错误监听在,会导致进程退出。

1
2
3
4
5
const events = require('events');
const emitter = new events.EventEmitter();

emitter.emit('error', new Error('This is a error'));
console.log('test');

调用后程序崩溃导致 Node 进程自动退出,因受上一行的影响,之后的 console.log(‘test’); 也不会得到执行。

1
2
3
4
5
events.js:167
throw er; // Unhandled 'error' event
^

Error: This is a error

作为最佳实践,应该始终为 ‘error’ 事件注册监听器

1
2
3
4
5
6
7
8
9
10
const events = require('events');
const emitter = new events.EventEmitter();

emitter.on('error', function(err) {
console.error(err);
})

emitter.emit('error', new Error('This is a error'));

console.log('test');
1
2
3
Error: This is a error
at Object.<anonymous> ...
test

如上代码所示,第一次调用后错误 error 事件会被监听,Node 进程也不会像之前的程序一样会自动退出,console.log(‘test’); 也得到了正常运行。

总结

许多 Node.js 成功的模块和框架都是基于 EventEmitter 的,学会 EventEmitter 的使用,并且知道该在什么时候去使用是非常有用的