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)实现流

  1. // 实现一个可读流
  2. // const { Writable } = require('stream');
  3. // const outStream = new Writable({
  4. // write(chunk, encoding, cb) {
  5. // console.log(chunk.toString(), '-----')
  6. // cb()
  7. // }
  8. // })
  9. // process.stdin.pipe(outStream);
  10. // 实现一个可写流
  11. const { Readable } = require('stream');
  12. // const inStream = new Readable()
  13. // inStream.push('qaeqweqweqwe');
  14. // inStream.push('asdasdasdd');
  15. // inStream.push(null);
  16. // inStream.pipe(process.stdout);
  17. // push部分数据
  18. // const inStream = new Readable({
  19. // read(size) {
  20. // this.push(String.fromCharCode(this.curCharCode++))
  21. // if (this.curCharCode > 90) {
  22. // this.push(null);
  23. // }
  24. // }
  25. // })
  26. // inStream.curCharCode = 65;
  27. // inStream.pipe(process.stdout);
  28. // 双向流
  29. // const { Duplex } = require('stream');
  30. // const inoutStream = new Duplex({
  31. // write(chunk, encoding, cb) {
  32. // console.log(chunk.toString() + '----');
  33. // cb();
  34. // },
  35. // read(size) {
  36. // this.push(String.fromCharCode(this.curCharCode++))
  37. // if(this.curCharCode > 90) {
  38. // this.push(null);
  39. // }
  40. // }
  41. // })
  42. // inoutStream.curCharCode = 65;
  43. // process.stdin.pipe(inoutStream).pipe(process.stdout);
  44. // 转换流
  45. // const { Transform } = require('stream');
  46. // const upperCase = new Transform({
  47. // transform(chunk, encoding, cb) {
  48. // this.push(chunk.toString().toUpperCase())
  49. // cb()
  50. // }
  51. // })
  52. // process.stdin.pipe(upperCase).pipe(process.stdout);

1.6)流的事件

在可读流上最重要的事件有:

  • data 事件,当流传递给消费者一个数据块的时候会触发。
  • end 事件,当在流中没有可以消费的数据的时候会触发。

在可写流上面最重要的事件有:

  • drain 事件,当可写流可以接受更多的数据时的一个标志。
  • finish 事件,当所有的数据都写入到底层系统中时会触发。

事件和函数可以结合起来自定义和优化流的使用。

1.5)流对象模型

流除了Buffer和 string 还有对象,所有的js对象都可以作为流
举例:

  1. // 下面转换流的结合实现了一个特性,
  2. // 可以将逗号分割的字符串转换为 Javascript 对象,
  3. // 因此 “a,b,c,d”会变成 {a: b, c: d}
  4. // 使用 stream对象
  5. const { Transform } = require('stream');
  6. const commaSpliitter = new Transform({
  7. readableObjectMode: true,
  8. transform(chunk, encoding, cb) {
  9. this.push(chunk.toString().trim().split(','));
  10. cb()
  11. }
  12. });
  13. const arrayToObject = new Transform({
  14. readableObjectMode: true,
  15. writableObjectMode: true,
  16. transform(chunk, encoding, cb) {
  17. const obj = {};
  18. for (let i = 0; i < chunk.length; i += 2) {
  19. obj[chunk[i]] = chunk[i + 1];
  20. }
  21. this.push(obj);
  22. cb()
  23. }
  24. });
  25. const objectToString = new Transform({
  26. writableObjectMode: true,
  27. transform(chunk, encoding, cb) {
  28. this.push(JSON.stringify(chunk) + '\n');
  29. cb()
  30. }
  31. });
  32. process.stdin
  33. .pipe(commaSpliitter)
  34. .pipe(arrayToObject)
  35. .pipe(objectToString)
  36. .pipe(process.stdout)

参考链接:https://zhuanlan.zhihu.com/p/36728655

2,fs文件系统

2.1)什么是文件系统

属于计算机操作系统的概念,操作系统中负责管理和存储文件信息的 软件机构是文件系统,主要负责用户建立文件,存入,读取,修改,转储文件,撤销文件等。

2.2)node操作文件系统

2.2.1)node 使用fs 模块操作文件系统

  1. node 的fs 模块运行时
    V8作为js的引擎,提供了js 调用 c++的可能。

    1. Node
    2. | 读取js
    3. V
    4. V8
    5. | C++调用libUV
    6. V
    7. libUV ---> 调用系统的API读取文件
  2. 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)文件系统的基本操作

遍历文件目录

  1. function travel(dir, cb) {
  2. fs.readdirSync(dir).forEach(item => {
  3. let pathName = path.join(dir, item)
  4. if (fs.statSync(pathName).isDirectory()) {
  5. travel(pathName, cb)
  6. } else {
  7. cb(pathName)
  8. }
  9. })
  10. }
  11. travel('./', (pathName) => {
  12. console.log('pathName --- ', pathName)
  13. })

参考链接: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

  1. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  2. 5d 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相同(就是类似程序的唯一标识符,保证程序就是原来的这一个)

  1. 4.4)云盘秒传,经常使用云盘,有时上传一个很大的文件几乎是秒传,它并不是真的把文件上传了,它只需要计算文件的MD5 并且在自己的数据库搜索一下这个MD5是否存在,如果存在那就不用上传了,直接使用已存在的文件就可以了,从而实现了文件的秒传。

5,MD5算法如何做到的

  1. 5.1)首先补位,使长度变成N*512 + 448,剩下的64位需要记住原始数据长度,这样就是512的整数倍<br /> 5.2)准备标准幻数,用来循环计算初始值<br /> 5.3)然后可以开始计算,原始的数据被处理成n512的比特,即n64字节,每次计算其中的一份,每一个64字节又被分为16个小份,计算一轮以后4个标准幻数就和以前不一样了,这样的四个幻数作为第二轮的标准幻数输入,重复这样的计算过程,知道N64字节全部计算,最终的标准幻数用16进制按照顺序显示出来,就是最终的MD5结果

3.3)SHA算法

1,共同点:SHA和MD5类似,都是密码散列函数,加密不可逆,并且都是可以对任意长度对象加密,输出固定长度的字符串。

2,两个算法都不能完全的100%防止碰撞,但是SHA256碰撞的几率小于MD5,这也是MD5被逐渐抛弃的原因,除非被加密的数据没有那么大的价值才会使用MD5加密。

3,以一个60M的文件为测试样本,经过1000次的测试平均值,三种算法的表现为:

  1. MD5算法运行1000次的平均时间为:226ms
  2. SHA1算法运行1000次的平均时间为:308ms
  3. SHA256算法运行1000次的平均时间为:473ms123

安全性方面,显然SHA256(又称SHA2)的安全性最高,但是耗时要比其他两种多很多。MD5相对较容易破解,因此,SHA1应该是这三种中性能最好的一款加密算法。

3.4)crypto使用

3.1)基础的MD5,SHA1,SHA256,SHA512加密算法的使用

  1. // node 加密
  2. const crypto = require('crypto');
  3. const md5hash = crypto.createHash('md5')
  4. .update('hello')
  5. .update('xu') // 表示可以在原始数据上添加了新的数据再进行加密
  6. .digest('hex') // 用于指定输出的编码格式,参数 hex,binary,base64
  7. console.log('md5hash', md5hash);
  8. const sha1hash = crypto.createHash('sha1')
  9. .update('hello')
  10. .update('xu')
  11. .digest('hex')
  12. console.log('sha1hash', sha1hash);
  13. const sha256hash = crypto.createHash('sha256')
  14. .update('hello')
  15. .update('xu')
  16. .digest('hex')
  17. console.log('sha256hash', sha256hash);
  18. const sha512hash = crypto.createHash('sha512')
  19. .update('hello')
  20. .update('xu')
  21. .digest('hex')
  22. console.log('sha512hash', sha512hash);
  23. // 重构函数 —— hash 摘要算法
  24. function createHash(text, hashtype) {
  25. const hash = crypto.createHash(hashtype)
  26. .update(text)
  27. .digest('hex');
  28. console.log(text, hashtype, hash, hash.length);
  29. }
  30. const hashes =['md5', 'sha1','sha256','sha512']
  31. hashes.forEach( type => {
  32. createHash('jean', type);
  33. })

注意:

1)不同算法加密出来的长度不一样

md5hash —- e9bbe3e428c8967212e5f4d26c2cbe25
sha1hash —- b5eb26fca4c8c79218cddd9c9ac7fb916249c0b7
sha256hash —- 2a679d5dea4e8b34730d3050b40fd3086699bcbbeac4601111cf23e66a2dca9b
sha512hash —- 8465a79b87d45e8419a792e289005bc014b9e8c15bb1f10db31cc1d41b9592cbb54e1c405ffa0da55c4ca2c89f50818825cdb0c384cb47808e9c1dca7b1475db

2)使用加密的方法的话,需要创建加密算法的对象,然后使用update方法来创建一个摘要,即hmac.update(data),data表示数据,就是使用原数据创建摘要,然后还可以继续使用update 增加内容的摘要

3)digest 表示使用digest 方法作为输出,之后不可以再追加内容,并且这个方法的参数,表示你需要输出摘要的编码格式,可以为 hex,binary,base64

  1. // Hmac
  2. const secret = '123123'
  3. const hmac = crypto.createHmac('sha256', secret)
  4. // 添加摘要内容
  5. const up = hmac.update('hello')
  6. // 追加内容
  7. const up2 = up.update('xu')
  8. // 输出up2
  9. const res2 = up2.digest('hex')
  10. console.log('res2', res2); // 可以输出
  11. // 使用digest方法输出
  12. const res = up.digest('hex')
  13. console.log('res', res); // 无法输出,一个hmac对象只能调用一次digest

3.5)SHA和Hmac区别

1)两者都可以都可以生成20位的hex加密签名,那为什么有了SHA,还要Hmac把SHA使用一次?

举例说明,因为如果使用SHA生成一个摘要,然后把生成的摘要和数据发送给别人,但是如果黑客工具,把数据和摘要同时都替换了,就重新替换了信息,所以双方可以共同商量一个一样的秘钥,每次计算原始数据的时候,都加上这个一样的秘钥计算,这样如果后边在发送信息的时候,即使被黑客把数据和摘要同时替换,如果计算出来的秘钥不一样,就表示不是可信数据,所以黑客简单的替换原始数据和摘要的方法就不可行了

2)SHA使用的迭代算法计算,所以消息可以是任意长度的,如果黑客不修改你的信息只是添加了更多的信息到你的数据,就不会被秘钥所影响,而Hmac可以在hash的过程中可以密封消息隐藏秘钥,并且不能在尾部追加数据,所以Hmac 没有发现任何已知的扩展攻击。

3)此外,Hmac需要密钥,如果加密的文本一样,密钥不一样,结果也完全不一样。

  1. function createHmca() {
  2. const text = '文字';
  3. const key = Math.random().toString().slice(-6);
  4. const res = crypto.createHmac('sha1', key).update(text).digest('hex');
  5. console.log(key, res);
  6. }
  7. let n = 10;
  8. while(n--) {
  9. createHmca();
  10. }

参考链接:https://www.cnblogs.com/panpanwelcome/p/15015623.html

3.6)AES加密算法

AES是对称加密,就是可以从把文件加密以后,再反推拿到文件原始内容

createCipher 和 createCipheriv 的区别:AES除了密钥外还可以指定IV(Initial Vector),不同的系统只要IV不同,用相同的密钥加密相同的数据得到的加密结果也是不同的。所以createCipher 目前已经被 node.js废弃,而 createCipheriv 则更安全。

  1. function encode(src, key, iv) {
  2. let sign = '';
  3. const ciper = crypto.createCipheriv('aes-128-cbc', key, iv);
  4. sign += ciper.update(src, 'utf8', 'hex');
  5. sign += ciper.final('hex');
  6. return sign;
  7. }
  8. function decode(sign, key, iv) {
  9. let src = '';
  10. const ciper = crypto.createDecipheriv('aes-128-cbc', key, iv);
  11. src += ciper.update(sign, 'hex', 'utf8')
  12. src += ciper.final('utf8');
  13. return src;
  14. }
  15. let key = '37725295ea78b626';
  16. let iv = 'efcf77768be478cb';
  17. const src = 'hello, my name is jean! my password is `etu^&&*(^123)`';
  18. const sign = encode(src, key, iv);
  19. const _src = decode(sign, key, iv);
  20. console.log('原文', src);
  21. console.log('加密后签名', sign);
  22. console.log('解密后原文', _src);

使用Buffer作为key 和 iv

  1. let key = Buffer.from('37725295ea78b626', 'utf8');
  2. let iv = Buffer.from('efcf77768be478cb', 'utf8')
  3. const src = 'hello, my name is jean! my password is `etu^&&*(^123)`';
  4. const sign = encode(src, key, iv);
  5. const _src = decode(sign, key, iv);
  6. console.log('原文', src);
  7. console.log('加密后签名', sign);
  8. 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算法

  1. // DH加密
  2. // A 的 keys
  3. let A = crypto.createDiffieHellman(512);
  4. let AKey = A.generateKeys();
  5. let prime = A.getPrime();
  6. let generator = A.getGenerator();
  7. // console.log('prime', prime.toString('hex'))
  8. // console.log('generator', generator.toString('hex'))
  9. // B 的 key
  10. let B = crypto.createDiffieHellman(prime, generator);
  11. let Bkey = B.generateKeys();
  12. // 交换产生密钥
  13. let ASecret = A.computeSecret(Bkey);
  14. let BSecret = B.computeSecret(AKey);
  15. console.log('A secret', ASecret.toString('hex'))
  16. console.log('B secret', BSecret.toString('hex'))

3.8)crypto接收stream

1)使用stream流

  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const filename = './node-crypto.md';
  4. const hash = crypto.createHash('sha1');
  5. const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流
  6. fsStream.on('readable', () => {
  7. const data = fsStream.read();
  8. if(data) {
  9. hash.update(data)
  10. } else {
  11. console.log(`${hash.digest('hex')} - ${filename}`)
  12. }
  13. })

2)使用pipe管理

  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const filename = './node-crypto.md';
  4. const hash = crypto.createHash('sha1');
  5. const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流
  6. fsStream.pipe(hash).pipe().pipe(process.stdout) // 使用pipe管理,但是出现乱码

3)处理pipe的乱码

  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const filename = './node-crypto.md';
  4. const hash = crypto.createHash('sha1');
  5. const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流
  6. const { Writable} = require('stream') // 创建一个可写流,写入process.stdout()
  7. const write = Writable();
  8. write._write = function(data, enc, next) {
  9. // 把流中的数据写入底层
  10. process.stdout.write(hash.digest('hex') + '\n');
  11. process.nextTick(next);
  12. }
  13. fsStream.pipe(hash).pipe(write);

4)使用Writable 对象 write的方法改写代码

  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const filename = './node-crypto.md';
  4. const hash = crypto.createHash('sha1');
  5. const fsStream = fs.createReadStream(filename); // 把文件创建一个可读流
  6. const { Writable} = require('stream') // 创建一个可写流,写入process.stdout()
  7. const write = new Writable({
  8. write(data, enc, cb) {
  9. process.stdout.write(hash.digest('hex') + '\n');
  10. // process.nextTick(cb);
  11. cb() // 看似可以直接调用回调,不需要借用nextTick?
  12. }
  13. })
  14. fsStream.pipe(hash).pipe(write);

5)stream 处理 hmac

  1. // 使用 stream处理 hmac
  2. const fs = require('fs');
  3. const filename = './node-crypto.md';
  4. const key = Math.random().toString().slice(-6);
  5. const hmac = crypto.createHmac('sha1', key);
  6. const fileStream = fs.createReadStream(filename);
  7. console.log('key', key);
  8. const { Writable } = require('stream');
  9. const write = Writable();
  10. write._write = function(data, enc, next) {
  11. process.stdout.write(hmac.digest('hex') + '\n');
  12. process.nextTick(next);
  13. }
  14. // fileStream.pipe(hmac).pipe(write); // 无法输出摘要
  15. fileStream.pipe(write);

fileStream.pipe(hmac).pipe(write),考虑应该是hmac不能在后边追加内容,所以只能直接输出,所以修改为 fileStream.pipe(write) 就可以拿到摘要。

3.9)非对称加密

  1. const fs = require('fs');
  2. const pub_key = fs.readFileSync('./rsa_public.key');
  3. const pri_key = fs.readFileSync('./rsasa_private.key');
  4. const text = 'hello, my name is jean! my password is `etu^&&*(^123)`';
  5. const secret = crypto.publicEncrypt(pub_key, Buffer.from(text));
  6. const res = crypto.privateDecrypt(pri_key, secret);
  7. console.log('secret', secret.toString()); // buffer转字符串是乱码,表示加密了
  8. console.log('res', res.toString()); // 解密还原字符串

参考链接:https://zhuanlan.zhihu.com/p/126502869

3.10)签名

为了防止对于数据的篡改,有的可以对服务器进行签名,就是使用私钥进行加密,使用对应的公钥解密签名验证。

  1. const fs = require('fs');
  2. const pub_key = fs.readFileSync('./rsa_public.key');
  3. const pri_key = fs.readFileSync('./rsasa_private.key');
  4. const text = 'hello, my name is jean! my password is `etu^&&*(^123)`';
  5. const sign = crypto.createSign('RSA-SHA256')
  6. .update(text)
  7. .sign(pri_key, 'hex');
  8. const verify = crypto.createVerify('RSA-SHA256')
  9. .update(text)
  10. .verify(pub_key, sign, 'hex');
  11. console.log(sign);
  12. console.log(verify);