JAMES

  • 首页

  • 关于

  • 标签

  • 归档

ECC-ECDH

发表于 2019-01-31 | 更新于 2019-02-01

背景

加密是一个很有深度的话题,本文只是谈一下项目中的一点 POC 过程和思考。这里主要介绍一下 ECDH 的 Node.Js 实现,后续会再探讨一下 Golang,虽说语言是一部分,但是实际应用的过程中还是有非常多的坑要踩.

ECDH

了解 ECDH,首先要了解 ECC,也就是椭圆曲线加密。当然,本文主要讲实现,具体数学逻辑过程有兴趣的可以另外找相关文档,就当我是搬一下砖。
ECDH 一个秘钥磋商的过程,秘钥磋商,就是 Server & Client 双方可以在不通过任何请求传输秘钥的情况下,只根据对方的公钥,结合自身的私钥,磋商出共同的一个秘钥(AES key), 以此保证用于加密的秘钥的安全性。

结合公私钥加解密的思路最简而言之就是:

1
2
3
Server PrivateKey + Client PublicKey ===
Client PrivateKey + Server PublicKey ===
Secret Key

实践

首先用到的模块有:crypto, jsrsasign, fs . 因为没有研究透 jsrsasign.js 这个模块,所以偷懒结合了一下 Node 原生模块 crypto.js 来处理。

值得注意的是:

Node 相关的加密模块,crypto,jsrsasign 所操作的 key,基本都是需要传 PEM 格式的;ECDH API 是例外,用的是 ECPoint ,下文会提到。

  1. 我这里服务端只存了一对固定的 Private/Public Key,存的是 PEM 格式的。生成的方式可以使用在线工具,或者直接代码实现;
    这里用的是 ‘secp256k1’ 曲线算法,算法列表可以通过 crypto.getCurves() 获取:
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
generateKeyPair(namedCurve) {
const {publicKey, privateKey} = generateKeyPairSync('ec', {
publicExponent: 0x10001,
namedCurve: namedCurve || 'secp256k1',
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
}
});
writeFileSync(`${dirPath}/ecdh_pub.pem`, publicKey, err => {
if (err) {
console.log('--[CREATE_PUB_KEY]--', err);
throw new Error(err.message);
}
});
writeFileSync(`${dirPath}/ecdh_priv.pem`, privateKey, err => {
if (err) {
console.log('--[CREATE_PRIV_KEY]--', err);
throw new Error(err.message);
}
});
this.privateKeyPEM = publicKey;
this.publicKeyPEM = privateKey;
};
  1. 因为这次是跟 Android 做集成,其他客户端也一样,通常传递的 PublicKey 不会是完整的 PEM 格式,或是没有 —BEGIN PUBLIC KEY— / —END PUBLIC KEY— 的头尾结构,所以我们在接收到客户端的 KEY 时需要重新拼接首位结构,这里用两个很蠢的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Generate base key to PEM, add 'BEGIN/END'
* @param {base64} baseKey
* @returns {string} PEM string
*/
const generateBaseKeyToPEM = baseKey => {
return `-----BEGIN PUBLIC KEY-----\n${baseKey}\n-----END PUBLIC KEY-----`;
};
/**
* Generate PEM to base key, remove 'BEGIN/END'
* @param {base64} keyPEM
* @returns {string} base string
*/
const generatePEMToBaseKey = keyPEM => {
const keys = keyPEM.split('\n');
return keys[1] + keys[2];
};
  1. 这一步是 ECDH 最重要的一步,根据 Node Crypto 的 API,官方提供了三个关键的API:
1
2
3
4
5
6
7
8
crypto.createECDH(curveName)
<!-- 创建 ECDH 公私钥-->
ecdh.generateKeypair();
ecdh.setPrivateKey(privateKey[, encoding])
ecdh.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])

这里用到的 privateKey 是我们服务端的私钥,otherPublicKey 是客户端的公钥,而且 crypto 模块提供的 API 能接受的 privateKey 的格式,实际上是 ECPoint,而不是完整的 PEM 或者 X.509 标准之类的 KEY。

该结论可以从第二个 API - ecdh.generateKeypair(); 的输出得出,默认 generate 出来的公私钥都是核心 ECPoint 那一段,而不是标准的 X.509 或者 ASN.1

所以,我们需要从服务端的私钥 PEM 文件,提取出该 privateKey 的 ECPoint,和从客户端的公钥提取对应格式的 ECPoint。

1
2
3
4
5
6
7
8
9
constructor() {
const privateKey = readFileSync(`${dirPath}/ecdh_priv.pem`).toString();
const publicKey = readFileSync(`${dirPath}/ecdh_pub.pem`).toString();
const {pubKeyHex, prvKeyHex} = KEYUTIL.getKeyFromPlainPrivatePKCS8PEM(privateKey);
this.privateKeyPEM = privateKey;
this.publicKeyPEM = publicKey;
this.privateKeyPoint = hextob64(prvKeyHex);
this.publicKeyPoint = hextob64(pubKeyHex);
};
  1. 最后是磋商,计算秘钥:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * Compute secret by public key PEM
    * @param {base64} otherPublicKeyPoint ecdh public key from other client/server
    * @returns {string | base64} secret key
    */
    computeSecretByPEM(otherPublicKeyPEM) {
    const {pubKeyHex} = KEYUTIL.getKey(otherPublicKeyPEM);
    const ecdh = createECDH('secp256k1');
    ecdh.setPrivateKey(this.privateKeyPoint, 'base64');
    const secret = ecdh.computeSecret(hextob64(pubKeyHex), 'base64', 'base64');
    console.log('[secret]:', secret);
    return secret;
    };
  2. 至于上面提到的,为什么我们要自己 setPrivateKey,是因为我们项目实际应用中还有签名和验签。所以需要使用我们自己的 privatekey、publicKey 来操作,而不用默认 Generate 出来的 Key.

下面是签名和验签:

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
/**
* Signature by private, from Jsrsasign.js
* @param {string | object} data encrypt plain text
* @returns {base64} signature
*/
signatureByKJUR(data) {
try {
const sig = new KJUR.crypto.Signature({
'alg': 'SHA1withECDSA'
});
console.log(this.privateKeyPEM);
sig.init(this.privateKeyPEM);
sig.updateString(data);
return hextob64(sig.sign());
}
catch (err) {
throw new Error(err);
}
};
/**
* Verify signature by public key, from Jsrsasign.js
* @param {string} data
* @param {base64} signature
* @param {base64} publicKey
* @returns {Boolean} verify result
*/
verifySignatureByKJUR({data, signature, publicKey}) {
const sig = new KJUR.crypto.Signature({
'alg': 'SHA1withECDSA'
});
sig.init(generateBaseKeyToPEM(publicKey));
sig.updateString(data);
return sig.verify(b64tohex(signature));
};

总结

上述是 node 中的简单应用,我把我遇到的坑都一起总结在完整的代码里了,有些做法很粗糙,但是也是一个 POC 的结果,模块的 API 也没有很深入的研究,没有做到物尽其用,但是相信结论是相似的,如果有说错的地方,烦请纠正我,我也是刚学了一点点,不吝赐教。

完整代码

Nginx-RTMP

发表于 2018-12-08 | 更新于 2018-12-10

Environment

本次打在的 Nginx-RTMP 流媒体服务器,是在 Win10 下虚拟机安装配置的。因为网上大多数分享都是 ubuntu 的,而这里主要是用了 centos,主要是 Lib 的下载有细小的差别。

Linux 系统: Linux-CentOS 7 x64


Stage 1

安装编译 nginx 需要的所有库

1
2
3
4
5
6
7
8
9
<!-- centOS 是不能直接找到这些 lib 的 -->
<!-- sudo apt-get install build-essential -->
sudo yum groupinstall "Development Tools"
sudo yum -y install openssl openssl-devel
<!-- 可以通过 yum search "%package%" 查找-->
sudo yum -y install pcre2-devel pcre2-tools

Stage 2

  1. 下载 nginx-rtmp-module:
1
git clone https://github.com/arut/nginx-rtmp-module.git
  1. 安装 nginx
1
2
3
4
5
6
7
8
9
10
<!-- 如果没有 wget 先安装 yum install -y wget -->
> wget http://nginx.org/download/nginx-1.9.15.tar.gz
> tar -zxvf nginx-1.9.15.tar.gz
> unzip master.zip
> cd nginx-1.9.15
> ./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-master
> sudo make && make install
  1. 检查 nginx 是否安装成功
1
sudo /usr/local/nginx/sbin/nginx

Stage 3

  1. 修改nginx配置文件:
1
2
3
> vi /usr/local/nginx/conf/nginx.conf
<!-- 键盘 i 进入编写状态,编写结束按 ESC 键,输入 "::wq" 并回车 —— 保存退出-->
  1. 添加配置:
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
... ...
rtmp {
server {
listen 1935; #监听的端口
chunk_size 4096;
application hls { #rtmp推流请求路径
live on;
hls on;
hls_path /usr/share/nginx/html/hls;
hls_fragment 5s;
}
application live { #多个app配置
live on;
record off;
}
}
}
# 注意 rtmp 配置的编写位置是跟 http 同级
http {
include mime.types;
... ...
... ...
}
... ...
  1. 重启 nginx
1
2
> sudo /usr/local/nginx/sbin/nginx -s stop
> sudo /usr/local/nginx/sbin/nginx
  1. 检查 nginx 是否运行成功
1
2
<!-- Linux 中每个应用运行都会产生一个进程 -->
> ps -ef | grep nginx

到此 nginx-rtmp 服务器基本搭建完成,接下来是测试推流,测试刚刚搭建这个服务器


推流测试篇

这里还没有代码实现,所以暂时用工具替代来帮助我们测试
这里用的是 OBS 推流

  • obs 基本设置如下,可以选用 “显示器内容” 进行串流测试,主要要配置的是 “串流服务” 的 ++”FMS URL”++

image

- Note

此处如果跟我一样是用 Linux 虚拟器跑的,应该先获取到虚拟机的 ip 地址,linux 下的 ip 获取命令为:

1
ip addr

  • 具体获取方式网上有很多分析这里不再赘述,而我们测试地址和 nginx 是否可用的方式则是在本地PC直接使用 虚拟机 ip+端口号访问 nginx 首页,如访问成功,则证明 ip 和 nginx 都是正确可用的,此时可以直接配置到 FMS URL 中,开始串流测试
1
FMS URL: rtmp://192.168.XXX.28:1935/live
  • 上述都正常配置,点击 “开始串流”,如一切正常,可在右下角看到当前传输的网络速度,此时可以点击 “开始录制” 进行直播录制,在对应存储文件下可以找到 .flv 格式的文件生成。

异常记录:

  • “Connect timeout”:nginx 没有正常开启,或 本机/虚拟机的防火墙没有打开(网上有很多详细介绍),或 FMS URL 中的 ip 地址、端口不对
  • “RTMPSockBuf_Fill, remote host closed connection”:地址和端口都对了,但是 URL 的文件位置不对,没做过测试,但是感觉应该是路径上的文件必须是已经存在的文件夹;即:
1
2
Work: rtmp://192.168.XXX.28:1935/live
Fail: rtmp://192.168.XXX.28:1935/live/test

除非 test 文件夹存在 (猜的,太懒不做测试了–!)

更新

因为测试拉流时使用 obs 总是没有很好地测试到结果,所以又找了一个新的测试工具,是在线网页版本的,亲测感觉很好

Web Tools

注:Stream 不填,如下

image

Vue-Input 限制数字&小数位输入

发表于 2018-10-10

使用 keyup 事件时,输入非法字符时,会有闪动,用 input 事件没有闪动

Show Like This

conv_ops

实现效果:

1
2
3
4
5
1、限制数量、金额等输入框,可输入字符类型:阿拉伯数字 0-9,及“.”;”.” 数 ≤ 1。
2、限制数字长度:小数点后 4 位,不能再输入
3、特殊输入:如:输入:01. -> 1 输入:.230 -> 0.23

代码实现如下:

dom

image

common pops data

image

implement method

image

Useing demo

image


实例参考自:http://www.hifrontend.com/20180108610.html

MySql - 事务 & 锁

发表于 2018-08-21

事务四大特性(ACID)

简单理解,事务是一组原子性的的SQL操作,这一组SQL要么全部执行成功,要么全部执行失败

  • 原子性:一个事物是不可分割的最小工作单元,整个事务要么全部执行成功,要么全部执行失败,不可能只执行中间的一部分操作
  • 一致性:执行事务是使数据库从一个一致性状态到另一个一致性状态,如果事务最终没有被提交,那么事务所做的修改也不会保存到数据库中
  • 隔离性:通常,一个事务提交前对于其他事务是不可见的,但是隔离性需要参考“隔离级别”
  • 持久性:事务一旦被提交,那么对数据库的修改将会被永久保存,即时数据库奔溃后修改的数据也不会丢失

隔离级别

SQL 标准中定义了四种隔离级别

  • 未提交读:未提交读是指,在事务中的修改,即使没有提交,对其他事务也都是可见的,但是这样会出现脏读,一般情况下都不会使用 “未提交读”
  • 提交读:提交读指的是,一个事务所做的修改在提交之前对其他事务都是不可见的,这个级别也叫做 “不可重复读”,因为执行两次相同的操作,可能会得到不同的结果
  • 可重复读:可重复读解决了 “脏读” 的问题,这个级别保证了同一个事务多次读取同样记录的结果是一致的,但是这个隔离级别无法解决 “幻读” 的问题。所谓 “幻读” 就是当某个事务读取范围数据时,另一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围数据时,会产生 “幻行” 。 InnoDB 存储引擎通过 MVCC 解决了幻读的问题,可重复读是 MySQL 默认的事务隔离级别
  • 可串行化:是最高的隔离级别,避免了前面说到的 “幻读问题”。可串行化会给读取的每一行都加锁,所以可能导致大量超时和锁争用的问题,实际使用中很少使用这个隔离级别

死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源。
解决方案是:回滚一个或者多个事务

1
2
3
graph LR
A-->B
B-->A

MCVV

MVVC 是行锁的一个比那种,在很多情况下MVVC可以避免加锁,因此开销较小,而不同的事务型存储引擎对于MVVC的实现各有不同。

MVVC是通过保存数据在某个时间点的快照来实现的。不管执行多长时间,每个事务看到的数据都是一致的。根据事务的开始时间不同,每个事务对同一张表,同一时刻看到的数据可能不一样。

InnoDB 的 MVVC 通过在每行记录后面保存两个隐藏的列来实现。这两个列,一个是保存了行的创建时间,一个保存了行的过期时间,存储的不是实际时间,而是版本号。每个新开始的事务,系统版本号都会自动增加。事务开始时刻的系统版本号会作为事务的版本号,用来与查询到的每行记录的版本号作对比。


  • SELECT

InnoDB 会根据以下两个条件检查每条记录:

  1. 只查找版本小于等于失误版本号的行
  2. 只查找未定义删除事件或者删除时间大于事务版本号的行
  • INSERT

InnoDB 为新插入的每一行保存当前的系统版本号作为行版本号

  • DELETE

InnoDB 为删除的每一行保存当前的系统版本号作为行的删除版本号

  • UPDATE
  1. InnoDB 新增一条记录,保存当前系统版本号作为新增行的版本号
  2. 在被删除记录的原始航,保存当前系统版本号作为被删除记录行的删除版本号

优点
  1. 因为有了两个隐藏列来记录数据的状态,所以大多数读操作都可以不加锁
  2. 性能好,同时可以保证读取的数据是正确的
缺点
  1. 需要额外的空间记录每行的状态
  2. 需要行状态的维护和检查

参考自 掘金

Node 连接 MySql

发表于 2018-03-28 | 更新于 2018-08-06

MySql 安装

在使用 node 连接 mysql前,准备好 mysql 环境

进入 MySql 官网 下载 MySql Server

另外前端工具可以使用官方提供的 MySql Workbench,似乎没中文版

  1. 打开 MySql 压缩包,免安装,进入 bin 目录
  2. 在使用 mysql 之前,需要启动服务,这里直接注册为服务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 初始化安装
    > mysqld --initialize --user=root --console (仔细看log,记录下生成的临时密码)
    > mysql -u root -p (Can not connect to MySQL server on localhost (10061))
    > mysqld --install
    > net start mysql (启动服务)
    > mysql -u root -p
    > Enter password:******* (输入临时密码)
    > (登录成功)
    > set password=password("newPassword"); (对于8.0以前有效)
    // 对于 8.0 以后
    > ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
    SELECT plugin FROM mysql.user WHERE User = 'root';

Node 连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
port: '3306',
charset: 'UTF8_GENERAL_CI',
timezone: 'local',
connectTimeout: 3000 * 30,
supportBigNumbers: true, // 数据库支持bigint或decimal类型列时,需要设此option为true (默认:false)
user: 'root',
password: 'Password1!',
database: 'sample',
});
connection.connect();
connection.query('SELECT * from websites', function (error, results, fields) {
if (error) throw error;
console.log('The websites array is: ', results);
});

错误记录

  1. ERROR 1130: Host ’xx.xx.x.x′ is not allowed to connect to this MySQL
    server [这是没有权限连接指定 IP 的主机]

-> 解决办法:

1
2
3
4
5
6
(1) 授权 myuser使用mypassword从任何主机连接到mysql服务器
GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%' IDENTIFIED BY 'mypassword' WITH GRANT OPTION;
(2) 允许用户myuser从ip为192.168.x.x 主机连接到mysql服务器,并使用mypassword作为密码
GRANT ALL PRIVILEGES ON *.* TO 'root'@'192.168.x.x' IDENTIFIED BY 'mypassword' WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'10.10.xx.xx' IDENTIFIED BY '123456' WITH GRANT OPTION;

MySql Noted(持续更新)

  1. 取消外键约束
1
2
3
4
5
6
7
8
9
10
11
Mysql中如果表和表之间建立的外键约束,则无法删除表及修改表结构。
解决方法是在Mysql中取消外键约束:
SET FOREIGN_KEY_CHECKS=0;
然后将原来表的数据导出到sql语句,重新创建此表后,再把数据使用sql导入,
然后再设置外键约束:
SET FOREIGN_KEY_CHECKS=1;
  1. MySql 5.7+ 版本使用 Group By 出现错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"code": "ER_WRONG_FIELD_WITH_GROUP",
"errno": 1055,
"sqlState": "42000",
"sqlMessage": "Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'wxmall.order.orderId' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with 'sql_mode=only_full_group_by'"
**-**
only_full_group_by :使用这个就是使用和oracle一样的group 规则, select的列都要在group中,或者本身是聚合列(SUM,AVG,MAX,MIN) 才行,其实这个配置目前感觉和distinct差不多的,所以去掉就好
-- 解决方案:
(1)添加聚合函数;
(2)修改全局配置
set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
  1. MySql 使用 DELETE 操作表出现错误:
1
Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect. 0.000 sec

原因是在safe mode下,要强制安全点,update只能跟where了,
要取消这个限制,可以:

1
2
3
SET SQL_SAFE_UPDATES=0; // 取消安全模式
SET SQL_SAFE_UPDATES=1; // 恢复安全模式


.sql
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
drop database sample;
CREATE DATABASE sample;
use sample;
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0; /* 取消外键约束 */
DROP TABLE IF EXISTS `websites`;
CREATE TABLE `websites` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(40) NOT NULL DEFAULT '' COMMENT '名称',
`url` varchar(250) NOT NULL DEFAULT '',
`country` char(10) NOT NULL DEFAULT '' COMMENT '国家',
PRIMARY KEY (`id`)
) AUTO_INCREMENT = 6 DEFAULT CHARSET=utf8;
BEGIN;
INSERT INTO `websites` VALUES
('1', 'Google', 'https://www.google.cm/', 'USA'),
('2', '淘宝', 'https://www.taobao.com/', 'CN'),
('3', '菜鸟教程', 'http://www.runoob.com/', 'CN'),
('4', '微博', 'http://weibo.com/', 'CN'),
('5', 'Facebook', 'https://www.facebook.com/', 'USA');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1; /* 重新建立外键约束 */

Event-Loop

发表于 2018-03-20

Event Loop

  • 事件循环是在主线程上完成的

  • 事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行

  • setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行

image

下面是一个官方的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');
const timeoutScheduled = Date.now();
// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms`);
}, 100);
// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('test.js', () => {
const startCallback = Date.now();
while (Date.now() - startCallback < 200) {
// 什么也不做
}
});
事件循环分析:
  1. 第一轮事件,首先执行完同步代码之后,事件队列 push 进了 setTimeout, fs.readFile 两个异步操作。 主线程首先在 timers 阶段检查,没有到期的定时器,便离开这一阶段,同时没有可执行 I/O 操作,进入第二轮事件循环
  2. 由于读取小文件一般不会超过 100ms,所以此时依然没有到期的定时器,而 I/O 已经有callback返回,则 Poll 阶段就会得到结果,所以会执行 I/O 函数 ++readFile()++,而该 I/O 还未执行完的时候,定时器已经到期,但是必须执行完当前阶段,才会离开当前阶段继续往下执行
  3. 进入第三轮事件循环,此时 timers 检测到定时器到期,执行定时器,因此此时输出时间大概在
    200ms 左右

关于 Macrotask / Microtask
  • Macrotask: setTimeout, setInterval, setImmediate, I/O, UI rendering

  • Microtask: process.nextTick, Promise, Object.observe, MutationObserver

  1. Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环
  2. 其中 process.nextTick 是所有异步任务里面最快执行的. Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列
  • 特殊举例1:
1
2
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

注:结果不唯一, 1/2, 2/1 都可能,因为事件的取值范围在1毫秒到2147483647毫秒之间。

因此 setTimeout(()=>{}, 0) 和 setTimeout(()=>{}, 1) 是一样的, Node 做不到0毫秒,最少也需要1毫秒。


特殊举例2:

1
2
3
4
5
6
const fs = require('fs');
fs.readFile('test.js', () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
});

上述代码中,输出结果一定是: 2 1

注:setTimeout 是在 timers 阶段执行,setImmediate 是在 check 阶段执行。 该轮事件循环中,只有 I/O 函数 fs.readFile ,会先进入 I/O callback 阶段,然后进入 check 阶段,所以先执行了 setImmediate, 结束后第二轮循环,进入 timers 阶段,执行 setTimeout 定时器


  • 特殊举例3:
    1
    2
    3
    4
    5
    6
    // 下面两行,次轮循环执行
    setTimeout(() => console.log(1));
    setImmediate(() => console.log(2));
    // 下面两行,本轮循环执行
    process.nextTick(() => console.log(3));
    Promise.resolve().then(() => console.log(4));

思考: 按照上述例子,此处为什么不是输出 ++3 4 1 2++ 或 ++3 4 2 1++ ? setTimeout/setImmediate 不是应该都可能有先后顺序吗?

注:因为此时 setTimeout,setImmediate 已经是处于第二轮事件循环队列中了,已经执行过一轮事件循环,node现阶段再快,1ms 也近乎是极限了,所以 timers 开始执行 setTimeout, 然后到了 check 再执行了 setImmediate


  • 例 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setImmediate(function () { // s1
console.log(1);
setImmediate(function () {
console.log(6);
})
process.nextTick(function () {
console.log(2);
});
});
process.nextTick(function () {
console.log(3);
setImmediate(function () { // s2
console.log(4);
})
setImmediate(function () { // s3
console.log(5);
})
});

上述输出结果为:3 1 4 5 2 6

注:

  1. process.nextTick 的回调会在 timers 阶段和I/O callbacks 阶段之间执行。执行同步代码结束,执行 process.nextTick 输出 3
  2. s1 是处于主线程的同步代码中,所以它的回调会排在 s2, s3 之前,但是三个 setImmediate s1, s2, s3处于同一轮事件循环中,因为第二个 setImmediate 执行的时候,check 阶段还没有过,所以此时依次输出 1 4 5
  3. 进入新一轮事件循环,先执行 process.nextTick 回调,输出 2, 再进入 check 阶段执行 setImmediate 输出 6

参考了 阮一峰 很多知识点的讲解,非常受用,也让自己对 Event Loop 有了更加清晰的理解

参考链接:Node 定时器

HTTP-HTTPS-TCP/IP协议

发表于 2018-03-09

1. HTTP协议

HTTP 协议, 是应用最广泛的一种网络协议,是无状态协议,是基于 TCP/IP 通信协议来传输数据。 客户机可以通过URL向HTTP服务端(服务器)发送请求,然后服务端响应该请求,之后关闭这个连接。

  • 支持客户/服务器模式
  • 简单快速
  • 灵活
  • 无连接
  • 无状态

无连接?

-> 含义是限制每次连接只处理一次请求。服务器处理完客户端请求,并收到客户端的应答之后,立即断开连接。以此可以节省传输时间

无状态?

-> HTTP协议即无状态协议,即协议对事务处理没有记忆能力,服务器无法区别是否已响应过用户的请求。 而++每次发送http请求时,客户端都要发起一个到服务端的TCP连接++,TCP的建立又涉及“三次握手”的过程。对于大流量的服务器来说这个过程的开销是十分大的。因此出现了“++短连接++”、“++长连接++” 的设计。

HTTP工作过程 ?

  1. 地址解析:浏览器从URL中解析出协议吗,主机名,端口,对象路径等,域名系统DNS解析域名得到主机IP地址
  2. 封装HTTP请求数据包:将以上部分结合本机自己的信息,打包成一个HTTP请求包
  3. 封装TCP包,建立TCP连接:发送HTTP请求数据包前,客户端需要通过网络与服务器建立连接,该连接是通过TCP来完成的,而TCP需要和IP来共同构建Internet(TCP/IP网络),即TCP/IP协议。构建成功之后才可以发起HTTP协议
  4. 建立连接后,客户机向服务端发起一个请求,格式为:URL,协议版本号,MIME信息,客户机信息等
  5. 服务器响应:响应信息包括,状态行,信息协议版本号,成功、错误的代码,MIME信息,服务器信息,实体信息等
  6. 服务器关闭TCP连接:通常,当服务器响应客户端返回数据时,它就要关闭TCP连接了

TCP 长短连接?

  • 短连接(Persistent Connection):客户机发送一个HTTP请求,会先发起一个TCP连接,待服务端响应请求之后,立即关闭TCP链接

  • 长连接(Keep-Alive) :客户机发送一个HTTP请求,发起一个TCP连接,服务端响应之后,保持TCP连接打开,在相同的客户机和服务器之间的后续请求和响应报文则通过相同的TCP连接来通信,不需要再次建立TCP

2. TCP/IP

  • 四层协议
C T
应用层 TELNET FTP HTTP SMTP DNS等
传输层 TCP UDP
网络层 IP ICMP ARP RARP
网络接口层 物理通信接口
  • 一个TCP的连接,需要经过“三次握手”才能建立起来。
    -> 主机A向主机B发送请求数据包(ACK=0,SYN=1,seq=x),主机B向主机A发送同意连接和要求同步(SYN=1,ACK=1,seq=x,ack=x+1),主机A在向主机B发送一个数据包确认B主机的同步要求(ACK=1,seq=x+1,ack=y+1)

  • A为什么会再确认一次呢,主要是为了防止已失效的连接请求报文段又突然传送给B,从而产生了错误

  • 为什么建立连接是三次握手,断开连接是四次握手? 因为当Server收到Client的SYN连接请求后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。 而断开连接时,当Server端收到FIN请求报文时,很可能不会立即关闭SOCKET,所以只能先回复一个ACK报文告诉Client已经收到了FIN报文,只有等Server端所有的数据都发送完了之后,才能发送FIN报文,因此需要四次握手

3. HTTPS

HTTPS = SSL/TLS + HTTP; 安全的一种HTTP传输协议。

  • Client 发起HTTPS请求时,在TCP连接建立的过程中,Server返回非对称加密公钥(证书)给Client,Client生成对称加密秘钥,并使用公钥进行加密,然后将加密的对称秘钥发送给Server。开始进行密文传输

Express 实现带参上传文件

发表于 2018-01-15 | 更新于 2018-03-09

Why?

  • 实现图片上传是比较简单的,但是在传输参数和上传图片在同一个请求去解析时却遇到了问题。

  • Express 4.X之后,并不再集成 body-parser 模块,需要手动添加配置。 而 body-parser 是Express常用的用于解析http请求的模块; 在使用 Express应用生成器
    $ npm install express-generator -g
    自动构建目录时,会自动加入该模块,并自动添加了部分相关配置,如下:imageimage

  • 该配置指定了body-parser可以解析 application/json 但是不能解析 文本格式的请求体,text/plain, application/x-www-form-urlencoded etc.. 因此对于普通上传文件我们假设使用 multipart/form-data 表单上传,是无法正常获取到相关参数的。


HOW?

  • body-parser 模块提供了许多API,可配置性高,具体此处不再赘叙,详细请查看 body-parser 官方文档介绍

  • 首先是可配置支持的解析类型:
    image

1
2
3
4
在此感谢这位博主的整理分享
Node.js(Express) HTTP请求体解析中间件:
http://blog.csdn.net/yanyang1116/article/details/54847560
  • 经上述配置之后,express 便可支持text等格式的参数解析了。话不多说直接看示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** User avatar upload */
exports.uploadAvatar = async (req, res, next) => {
let result = { status: CodeConstants.FAIL, data: {}, message: "" };
atavarUpload.single('uploadAvatar')(req, res, async err => {
if (err) {
console.error(err)
result.message = err;
} else if (req.file) {
let file = req.file;
let fileName = req.body.userID + '-' + uuidv4() + '-' + file.filename;
try {
let uploadAvatarResult = await QiniuProxie.uploadAvatar(fileName, file.path);
result.status = CodeConstants.SUCCESS;
result.data = { avatar: uploadAvatarResult.key };
} catch (err) {
result.message = err;
}
} else {
result.message = "Parameters is incompleteness";
}
return res.json(result);
})
}

此处是文件上传相关代码,其中,atavarUpload.single() 是引用了 multer 模块来完成文件上传,它是以中间件形式存在,是一个可插拔上传模块。具体配置可参照官方介绍或文末介绍。

此处并未体现中间件的形式,纯粹是本人结构需要,如这样处理有不好之处请告知,十分感谢!

Note:

在上述代码中,获取参数 [req.body] 是在回调函数中才可获取到正确的参数内容,在回调方法外是无法获取到参数的; 因为在回调方法中请求经过 multer 模块的进一步解析。因此使用 req.body 可获取到相关请求体的内容,即form表单除 file 以外的参数; 而 req.file 即使所上传的图片。此处是上传文件到服务器本地之后,再上传至“七牛云”。


END

  • 以上即是 Node + Express + Multer + body-parser 实现文件上传的完整过程以及相关配置,核心代码就是上述所示,关键在于 body-parser 的配置,此处本人依然有不明白的疑点:为什么 Express 应用生成器默认是将 application/x-www-form-urlencoded 等解析权限关闭,为什么不是默认打开的???

  • 如有说错的地方,请大家包涵,也请告知本人,十分感谢!


附件:multer 配置

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
const multer = require('multer')
const bytes = require('bytes')
const uuidv4 = require('uuid/v4');
let storage = multer.diskStorage({
// 设置上传文件路径
destination: "public/qiniu/avatar/",
// 文件重命名
filename: function (req, file, cb) {
let fileFormat = (file.originalname).split(".");
cb(null, uuidv4() + "." + fileFormat[fileFormat.length - 1]);
}
});
// 添加配置文件到muler对象。
let upload = multer({
storage: storage,
limits: {
fileSize: bytes('2MB') // 限制文件大小
},
fileFilter: function (req, files, callback) {
// 只允许上传 jpg|png|jpeg|gif|svg 格式的文件
var type = '|' + files.mimetype.slice(files.mimetype.lastIndexOf('/') + 1) + '|';
var fileTypeValid = '|jpg|png|jpeg|gif|svg'.indexOf(type) !== -1;
callback(null, !!fileTypeValid);
}
});
module.exports = upload;

MongoDB-Role-Management.md

发表于 2017-12-11 | 更新于 2018-01-16

Overview

MongoDB

MongoDB is a NoSql database which is very easy to install and use.

In fact, mongod has no administrator account by default. So if you build your database in a open public net environment that will be very vulnerable. Others can operation your database data at will.


Role Kinds

The users of MongoDB are divided into two types, one is admin, the other is a specific database user.

Admin users have the highest permissions, while specific database users can only access specific databases


Role Management

Before you create your database user. We need to noted that MondoDB provide a database names admin. If your want to control the access user, firstly have to create a admin user into admin database. Then you can create the user for the database.

1
2
3
4
5
6
7
8
9
10
11
12
13
> use admin
> db.createUser({user: "admin", pwd: "adminPassword", roles: [{role: "root", db: "admin"}]})
> exit
// restart mongo
mongo --port 27017 -u admin -p adminPassword --authenticationDatabase admin
> use testDB
> db.createUser({user: "testUser", pwd: "dbPassword", roles: [{role: "readWrite", db: "testDB"}]})
> db.auth("testUser", "dbPassword")

Start DB Auth

1
> mongod --auth

Node.Js RSA & AES

发表于 2017-11-01 | 更新于 2018-01-16

Why

When the client or the front end calls the API of the server to request the data, it is easy to intercept the user request / response data.
Such as fiddler, Wireshark etc.

This means that our data is not always in a safe environment.
So we need to encrypt out data when having data transmission.

For Module

Node official provide Crypto.js module, it offers a variety of encryption methods, such as RSA & AES etc.

The following are the use of methods provided by the Crypto.js module

RSA

  • Prepare a pair of public and private keys and save in files.
  • Ensure your key’s padding, it need to be same type when encrypt or decrypt.

  • Padding
    1. An optional padding value defined in crypto.constants, which may be: crypto.constants.RSA_NO_PADDING, RSA_PKCS1_PADDING, or crypto.constants.RSA_PKCS1_OAEP_PADDING.
    2. key can be an object or a string. If key is a string, it is treated as the key with no passphrase and will use RSA_PKCS1_OAEP_PADDING. Because RSA public keys can be derived from private keys, a private key may be passed instead of a public key.

  • Encrypt

    1. crypto.publicEncrypt(publicKey, buffer)
      1
      2
      3
      4
      5
      6
      7
      exports.publicEncrypt = (plainText, cb) => {
      let encryptBuff = crypto.publicEncrypt({
      key: this.getPublicKey(),
      padding: crypto.constants.RSA_PKCS1_PADDING
      }, Buffer.from(plainText));
      cb(encryptBuff.toString('base64'))
      }
  • Decrypt

    1. crypto.privateDecrypt(privateKey, buffer)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      exports.privateDecrypt = (encryptText, cb) => {
      let encryptBuff = Buffer.alloc(Constants.MAX_DECRYPT_BLOCK || 128);
      encryptBuff.write(encryptText, 'base64');
      let decryptBuff = crypto.privateDecrypt({
      key: this.getPrivateKey(),
      padding: crypto.constants.RSA_PKCS1_PADDING
      }, encryptBuff);
      cb(JSON.parse(decryptBuff.toString()));
      }
  • Signature

    1. crypto.privateDecrypt(privateKey, buffer)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      exports.signature = (plainText, cb) => {
      let sign = crypto.createSign('SHA256');
      try {
      sign.update(plainText);
      let privateKey = this.getPrivateKey();
      let result = sign.sign(privateKey, 'base64');
      cb(result)
      } catch (err) {
      console.log(err)
      }
      }

  • Block Encrypt
    1. crypto.publicEncrypt(publicKey, buffer)
    2. image
  • Block Decrypt
    1. crypto.publicEncrypt(publicKey, buffer)
    2. image

AES

  • Save your secret key as a variable or in a file

  • Cipher

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const crypto = require('crypto');
    const Constants = require('./Constants')
    exports.cipher = (data, cb) => {
    const cipher = crypto.createCipher('aes192', Constants.AES_SECRET);
    let encrypted = cipher.update(data, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    cb(encrypted)
    }
    ```
    - Decipher
    ``` bash
    exports.decipher = (cipherText, cb) => {
    const decipher = crypto.createDecipher('aes192', Constants.AES_SECRET);
    let decrypted = decipher.update(cipherText, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    cb(decrypted)
    }
12
James.Yang

James.Yang

我不知道未来我将去何方,但我一直在路上

11 日志
23 标签
© 2019 James.Yang
由 Hexo 强力驱动 v3.6.0
|
主题 – NexT.Mist v6.4.2