1,stream 流
1.1)stream流的作用
由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()和fs.createWriteStream()方法通过流的方式实现对大文件的操作。
流原理举例:比如Linux命令也有流 | 可以把左边的命令执行以后传到右边,再执行右边的命令
1.2)流的基本类型
在 Node.js 中有四种基本的流类型:Readable(可读流),Writable(可写流),Duplex(双向流),Transform(转换流)。
1.3)node模块 中的流
Node中的大多数模块都有stream的应用,比如fs的createReadStream()和createWriteStream()方法可以分别用于创建文件的可读流和可写流,process模块中的stdin和stdout则分别是可读流和可写流的示例。
http的请求和响应,都是可读可写流,fs文件系统也是可读可写流,zlib 是转换流,crypto 也是转换流,tcp是双向流,stdin可读,stdout可写等
1.4)stream流的作用
- 实现流
- 消费流
1.4.1)消费流
类似 createReadStream()创建一个流,然后输出或者做成接口返回数据,就是消费流
1.4.2)实现流
// 实现一个可读流// const { Writable } = require('stream');// const outStream = new Writable({// write(chunk, encoding, cb) {// console.log(chunk.toString(), '-----')// cb()// }// })// process.stdin.pipe(outStream);// 实现一个可写流const { Readable } = require('stream');// const inStream = new Readable()// inStream.push('qaeqweqweqwe');// inStream.push('asdasdasdd');// inStream.push(null);// inStream.pipe(process.stdout);// push部分数据// const inStream = new Readable({// read(size) {// this.push(String.fromCharCode(this.curCharCode++))// if (this.curCharCode > 90) {// this.push(null);// }// }// })// inStream.curCharCode = 65;// inStream.pipe(process.stdout);// 双向流// const { Duplex } = require('stream');// const inoutStream = new Duplex({// write(chunk, encoding, cb) {// console.log(chunk.toString() + '----');// cb();// },// read(size) {// this.push(String.fromCharCode(this.curCharCode++))// if(this.curCharCode > 90) {// this.push(null);// }// }// })// inoutStream.curCharCode = 65;// process.stdin.pipe(inoutStream).pipe(process.stdout);// 转换流// const { Transform } = require('stream');// const upperCase = new Transform({// transform(chunk, encoding, cb) {// this.push(chunk.toString().toUpperCase())// cb()// }// })// process.stdin.pipe(upperCase).pipe(process.stdout);
1.6)流的事件
在可读流上最重要的事件有:
- data 事件,当流传递给消费者一个数据块的时候会触发。
- end 事件,当在流中没有可以消费的数据的时候会触发。
在可写流上面最重要的事件有:
- drain 事件,当可写流可以接受更多的数据时的一个标志。
- finish 事件,当所有的数据都写入到底层系统中时会触发。
事件和函数可以结合起来自定义和优化流的使用。
1.5)流对象模型
流除了Buffer和 string 还有对象,所有的js对象都可以作为流
举例:
// 下面转换流的结合实现了一个特性,// 可以将逗号分割的字符串转换为 Javascript 对象,// 因此 “a,b,c,d”会变成 {a: b, c: d}// 使用 stream对象const { Transform } = require('stream');const commaSpliitter = new Transform({readableObjectMode: true,transform(chunk, encoding, cb) {this.push(chunk.toString().trim().split(','));cb()}});const arrayToObject = new Transform({readableObjectMode: true,writableObjectMode: true,transform(chunk, encoding, cb) {const obj = {};for (let i = 0; i < chunk.length; i += 2) {obj[chunk[i]] = chunk[i + 1];}this.push(obj);cb()}});const objectToString = new Transform({writableObjectMode: true,transform(chunk, encoding, cb) {this.push(JSON.stringify(chunk) + '\n');cb()}});process.stdin.pipe(commaSpliitter).pipe(arrayToObject).pipe(objectToString).pipe(process.stdout)
参考链接:https://zhuanlan.zhihu.com/p/36728655
2,fs文件系统
2.1)什么是文件系统
属于计算机操作系统的概念,操作系统中负责管理和存储文件信息的 软件机构是文件系统,主要负责用户建立文件,存入,读取,修改,转储文件,撤销文件等。
2.2)node操作文件系统
2.2.1)node 使用fs 模块操作文件系统
node 的fs 模块运行时
V8作为js的引擎,提供了js 调用 c++的可能。Node| 读取jsVV8| C++调用libUVVlibUV ---> 调用系统的API读取文件
js调用libuv执行读写
node.js 的 fs.read() 函数做了什么
1)node.js启动,调用v8,libuv
2)用户调用 fs.read()函数
3)fs.read() 提交一个文件读取的request(请求)因为是异步的,所以会继续执行后边的代码
4)node.js 拿到这个请求,传递给v8,v8解析以后,调用libuv的接口,因为是异步的,所以塞入libuv的线程池队列
5)libuv的线程池队列拿到请求,选择一个线程,执行io操作,此时读取io的线程卡住,但是整个系统依旧在跑
6)io结束,执行io的线程将结果和完成标识符(done)
7)底层libuv调用uv_fs_done 函数,这个是异步文件io结束后的回调函数,在uv_fs_done里面会回调上层c++模块的callback函数,这个callback就是用户执行的js层的回调函数。
参考链接:https://www.zhihu.com/question/63296357/answer/350624409
2.3)文件系统的基本操作
遍历文件目录
function travel(dir, cb) {fs.readdirSync(dir).forEach(item => {let pathName = path.join(dir, item)if (fs.statSync(pathName).isDirectory()) {travel(pathName, cb)} else {cb(pathName)}})}travel('./', (pathName) => {console.log('pathName --- ', pathName)})
参考链接:https://www.cnblogs.com/xiaohuochai/p/6938104.html
3,加解密 -crypto
3.1)什么是加密
参考链接:https://www.bilibili.com/video/BV1EW411u7th?p=33 —— Crash Course Computer Science
3.2)MD5 算法
1,MD5是计算机安全领域广泛使用的散列函数,用于提供消息的完整性保护,是计算机广泛使用的哈希算法之一(哈希map?)
2,MD5的固定长度为128位比特,是16字节,通常用16进制字面值来输出它,也就是每4位比特按照16进制字面值显示,是一个长度为32位的字符串
如:5d41402abc4b2a76b9719d911017c592
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 155d 41 40 2a bc 4b 2a 76 b9 71 9d 91 10 17 c5 92
3,MD5的特点:
3.1)长度固定,无论输入多少字节,输出总是16字节(16B)
3.2)不可逆,从结果无法反推原始数据(在算法处理的时候肯定丢失了信息)
3.3)高度离散性,输出的16字节数据没有任何的规律可言,哪怕输入修改一个1个比特,输出的结果也完全不同,可以说MD5的结果无法预测,
3.4)抗碰撞性,找到两个不同的数据产生的MD5一致是非常困难的,也就是原始数据固定的情况下,想找到另外一份数据生成相同的MD5几乎是不可能的。
4,MD5使用场景
4.1)用户密码保护,在保存用户密码时,不记录密码本身,只是记录密码的MD5结果,只有用户自己知道密码明文,校验时只要密码正确,得到的MD5一定是一样的,校验通过,这样的好处是即使密码数据库被盗,也无法通过MD5反推出密码明文是什么,使密码保存更安全
4.2)文件完整性校验,比如传输非常大的文件,由于网络传输的不可靠因素,有可能导致文件传输不完整或者被篡改,如何校验接收端文件与发送端一致,只需要先在发送端计算一次文件的MD5,并把这个结果发送给接收端,接收端在文件传输完成后,也计算一次MD5,如果两次结果一致,那文件一定是完整的
4.3)数字签名,比如发布了一个程序,为了防止程序被植入木马,可以在发布文件时,同时发布其MD5,别人下载程序以后,自己计算一次MD5是否一致,就能知道程序是否被篡改,根据MD5的不可逆性和抗碰撞性,篡改着不可能使篡改后的MD5与原MD5相同(就是类似程序的唯一标识符,保证程序就是原来的这一个)
4.4)云盘秒传,经常使用云盘,有时上传一个很大的文件几乎是秒传,它并不是真的把文件上传了,它只需要计算文件的MD5 并且在自己的数据库搜索一下这个MD5是否存在,如果存在那就不用上传了,直接使用已存在的文件就可以了,从而实现了文件的秒传。
5,MD5算法如何做到的
5.1)首先补位,使长度变成N*512 + 448,剩下的64位需要记住原始数据长度,这样就是512的整数倍<br /> 5.2)准备标准幻数,用来循环计算初始值<br /> 5.3)然后可以开始计算,原始的数据被处理成n个512的比特,即n个64字节,每次计算其中的一份,每一个64字节又被分为16个小份,计算一轮以后4个标准幻数就和以前不一样了,这样的四个幻数作为第二轮的标准幻数输入,重复这样的计算过程,知道N个64字节全部计算,最终的标准幻数用16进制按照顺序显示出来,就是最终的MD5结果
3.3)SHA算法
1,共同点:SHA和MD5类似,都是密码散列函数,加密不可逆,并且都是可以对任意长度对象加密,输出固定长度的字符串。
2,两个算法都不能完全的100%防止碰撞,但是SHA256碰撞的几率小于MD5,这也是MD5被逐渐抛弃的原因,除非被加密的数据没有那么大的价值才会使用MD5加密。
3,以一个60M的文件为测试样本,经过1000次的测试平均值,三种算法的表现为:
MD5算法运行1000次的平均时间为:226msSHA1算法运行1000次的平均时间为:308msSHA256算法运行1000次的平均时间为:473ms123
安全性方面,显然SHA256(又称SHA2)的安全性最高,但是耗时要比其他两种多很多。MD5相对较容易破解,因此,SHA1应该是这三种中性能最好的一款加密算法。
3.4)crypto使用
3.1)基础的MD5,SHA1,SHA256,SHA512加密算法的使用
// node 加密const crypto = require('crypto');const md5hash = crypto.createHash('md5').update('hello').update('xu') // 表示可以在原始数据上添加了新的数据再进行加密.digest('hex') // 用于指定输出的编码格式,参数 hex,binary,base64console.log('md5hash', md5hash);const sha1hash = crypto.createHash('sha1').update('hello').update('xu').digest('hex')console.log('sha1hash', sha1hash);const sha256hash = crypto.createHash('sha256').update('hello').update('xu').digest('hex')console.log('sha256hash', sha256hash);const sha512hash = crypto.createHash('sha512').update('hello').update('xu').digest('hex')console.log('sha512hash', sha512hash);// 重构函数 —— hash 摘要算法function createHash(text, hashtype) {const hash = crypto.createHash(hashtype).update(text).digest('hex');console.log(text, hashtype, hash, hash.length);}const hashes =['md5', 'sha1','sha256','sha512']hashes.forEach( type => {createHash('jean', type);})
注意:
1)不同算法加密出来的长度不一样
md5hash —- e9bbe3e428c8967212e5f4d26c2cbe25
sha1hash —- b5eb26fca4c8c79218cddd9c9ac7fb916249c0b7
sha256hash —- 2a679d5dea4e8b34730d3050b40fd3086699bcbbeac4601111cf23e66a2dca9b
sha512hash —- 8465a79b87d45e8419a792e289005bc014b9e8c15bb1f10db31cc1d41b9592cbb54e1c405ffa0da55c4ca2c89f50818825cdb0c384cb47808e9c1dca7b1475db
2)使用加密的方法的话,需要创建加密算法的对象,然后使用update方法来创建一个摘要,即hmac.update(data),data表示数据,就是使用原数据创建摘要,然后还可以继续使用update 增加内容的摘要
3)digest 表示使用digest 方法作为输出,之后不可以再追加内容,并且这个方法的参数,表示你需要输出摘要的编码格式,可以为 hex,binary,base64
// Hmacconst secret = '123123'const hmac = crypto.createHmac('sha256', secret)// 添加摘要内容const up = hmac.update('hello')// 追加内容const up2 = up.update('xu')// 输出up2const res2 = up2.digest('hex')console.log('res2', res2); // 可以输出// 使用digest方法输出const res = up.digest('hex')console.log('res', res); // 无法输出,一个hmac对象只能调用一次digest
3.5)SHA和Hmac区别
1)两者都可以都可以生成20位的hex加密签名,那为什么有了SHA,还要Hmac把SHA使用一次?
举例说明,因为如果使用SHA生成一个摘要,然后把生成的摘要和数据发送给别人,但是如果黑客工具,把数据和摘要同时都替换了,就重新替换了信息,所以双方可以共同商量一个一样的秘钥,每次计算原始数据的时候,都加上这个一样的秘钥计算,这样如果后边在发送信息的时候,即使被黑客把数据和摘要同时替换,如果计算出来的秘钥不一样,就表示不是可信数据,所以黑客简单的替换原始数据和摘要的方法就不可行了
2)SHA使用的迭代算法计算,所以消息可以是任意长度的,如果黑客不修改你的信息只是添加了更多的信息到你的数据,就不会被秘钥所影响,而Hmac可以在hash的过程中可以密封消息隐藏秘钥,并且不能在尾部追加数据,所以Hmac 没有发现任何已知的扩展攻击。
3)此外,Hmac需要密钥,如果加密的文本一样,密钥不一样,结果也完全不一样。
function createHmca() {const text = '文字';const key = Math.random().toString().slice(-6);const res = crypto.createHmac('sha1', key).update(text).digest('hex');console.log(key, res);}let n = 10;while(n--) {createHmca();}
参考链接:https://www.cnblogs.com/panpanwelcome/p/15015623.html
3.6)AES加密算法
AES是对称加密,就是可以从把文件加密以后,再反推拿到文件原始内容
createCipher 和 createCipheriv 的区别:AES除了密钥外还可以指定IV(Initial Vector),不同的系统只要IV不同,用相同的密钥加密相同的数据得到的加密结果也是不同的。所以createCipher 目前已经被 node.js废弃,而 createCipheriv 则更安全。
function encode(src, key, iv) {let sign = '';const ciper = crypto.createCipheriv('aes-128-cbc', key, iv);sign += ciper.update(src, 'utf8', 'hex');sign += ciper.final('hex');return sign;}function decode(sign, key, iv) {let src = '';const ciper = crypto.createDecipheriv('aes-128-cbc', key, iv);src += ciper.update(sign, 'hex', 'utf8')src += ciper.final('utf8');return src;}let key = '37725295ea78b626';let iv = 'efcf77768be478cb';const src = 'hello, my name is jean! my password is `etu^&&*(^123)`';const sign = encode(src, key, iv);const _src = decode(sign, key, iv);console.log('原文', src);console.log('加密后签名', sign);console.log('解密后原文', _src);
使用Buffer作为key 和 iv
let key = Buffer.from('37725295ea78b626', 'utf8');let iv = Buffer.from('efcf77768be478cb', 'utf8')const src = 'hello, my name is jean! my password is `etu^&&*(^123)`';const sign = encode(src, key, iv);const _src = decode(sign, key, iv);console.log('原文', src);console.log('加密后签名', sign);console.log('解密后原文', _src);
注意:
1,加密结果通常有两种表示方法:hex和base64,这些功能Nodejs全部都支持,但是在应用中要注意,如果加解密双方一方用Nodejs,另一方用Java、PHP等其它语言,需要仔细测试。
2,如果无法正确解密,要确认双方是否遵循同样的AES算法,字符串密钥和IV是否相同,加密后的数据是否统一为hex或base64格式。
3,密钥的长度规则:key: 加密解密的密钥:密钥必须是 8/16/32 位,如果加密算法是 128,则对应的密钥是 16 位,如果加密算法是 256,则对应的密钥是 32 位;iv: 初始向量,规则与 key 一样
参考链接:https://zhuanlan.zhihu.com/p/126502869
3.7)Diffie-Hellman 算法
1)DH算法是一个秘钥交换协议,可以让双方在不泄漏秘钥的情况协商出一个秘钥来。
2)这个算法使用的是单向函数,是模幂运算 :g^a mod p,所以只给余数和基数很难知道指数是多少
3)DH算法是如何做的
- 首先有公开的值:基数和模数
- 自己使用自己的密钥 a,做算法 g^a mod p 得到结果然后发给对方,对方也选一个密码指数 b,计算得出结果 g^b mod p,然后发给我
- 为了得到双方共用的密钥,把对方给的数,用自己的 a,再做模幂运算:(g^b mod p)a,对方也是使用自己的密钥b,做模幂运算:(ga mod p)^b
这样两个的结果是一样的:(g^b mod p)^a = (g^a mod p)^b = g^ab mod p 所以可以拿到一样的密钥
4)DH算法的使用:可以DH算法的大数据作为密钥,使用AES之类加密通信
5)这样双方使用一样的密钥加密和解密消息成为对称加密,因为密钥一样,凯撒加密,英格玛,AES 都是对称加密
6)node中使用DH算法
// DH加密// A 的 keyslet A = crypto.createDiffieHellman(512);let AKey = A.generateKeys();let prime = A.getPrime();let generator = A.getGenerator();// console.log('prime', prime.toString('hex'))// console.log('generator', generator.toString('hex'))// B 的 keylet B = crypto.createDiffieHellman(prime, generator);let Bkey = B.generateKeys();// 交换产生密钥let ASecret = A.computeSecret(Bkey);let BSecret = B.computeSecret(AKey);console.log('A secret', ASecret.toString('hex'))console.log('B secret', BSecret.toString('hex'))
3.8)crypto接收stream
1)使用stream流
const crypto = require('crypto');const fs = require('fs');const filename = './node-crypto.md';const hash = crypto.createHash('sha1');const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流fsStream.on('readable', () => {const data = fsStream.read();if(data) {hash.update(data)} else {console.log(`${hash.digest('hex')} - ${filename}`)}})
2)使用pipe管理
const crypto = require('crypto');const fs = require('fs');const filename = './node-crypto.md';const hash = crypto.createHash('sha1');const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流fsStream.pipe(hash).pipe().pipe(process.stdout) // 使用pipe管理,但是出现乱码
3)处理pipe的乱码
const crypto = require('crypto');const fs = require('fs');const filename = './node-crypto.md';const hash = crypto.createHash('sha1');const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流const { Writable} = require('stream') // 创建一个可写流,写入process.stdout()const write = Writable();write._write = function(data, enc, next) {// 把流中的数据写入底层process.stdout.write(hash.digest('hex') + '\n');process.nextTick(next);}fsStream.pipe(hash).pipe(write);
4)使用Writable 对象 write的方法改写代码
const crypto = require('crypto');const fs = require('fs');const filename = './node-crypto.md';const hash = crypto.createHash('sha1');const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流const { Writable} = require('stream') // 创建一个可写流,写入process.stdout()const write = new Writable({write(data, enc, cb) {process.stdout.write(hash.digest('hex') + '\n');// process.nextTick(cb);cb() // 看似可以直接调用回调,不需要借用nextTick?}})fsStream.pipe(hash).pipe(write);
5)stream 处理 hmac
// 使用 stream处理 hmacconst fs = require('fs');const filename = './node-crypto.md';const key = Math.random().toString().slice(-6);const hmac = crypto.createHmac('sha1', key);const fileStream = fs.createReadStream(filename);console.log('key', key);const { Writable } = require('stream');const write = Writable();write._write = function(data, enc, next) {process.stdout.write(hmac.digest('hex') + '\n');process.nextTick(next);}// fileStream.pipe(hmac).pipe(write); // 无法输出摘要fileStream.pipe(write);
fileStream.pipe(hmac).pipe(write),考虑应该是hmac不能在后边追加内容,所以只能直接输出,所以修改为 fileStream.pipe(write) 就可以拿到摘要。
3.9)非对称加密
const fs = require('fs');const pub_key = fs.readFileSync('./rsa_public.key');const pri_key = fs.readFileSync('./rsasa_private.key');const text = 'hello, my name is jean! my password is `etu^&&*(^123)`';const secret = crypto.publicEncrypt(pub_key, Buffer.from(text));const res = crypto.privateDecrypt(pri_key, secret);console.log('secret', secret.toString()); // buffer转字符串是乱码,表示加密了console.log('res', res.toString()); // 解密还原字符串
参考链接:https://zhuanlan.zhihu.com/p/126502869
3.10)签名
为了防止对于数据的篡改,有的可以对服务器进行签名,就是使用私钥进行加密,使用对应的公钥解密签名验证。
const fs = require('fs');const pub_key = fs.readFileSync('./rsa_public.key');const pri_key = fs.readFileSync('./rsasa_private.key');const text = 'hello, my name is jean! my password is `etu^&&*(^123)`';const sign = crypto.createSign('RSA-SHA256').update(text).sign(pri_key, 'hex');const verify = crypto.createVerify('RSA-SHA256').update(text).verify(pub_key, sign, 'hex');console.log(sign);console.log(verify);
