4. buffer&fs&stream

编码

buffer的概念

Buffer用于描述内存, 内存是2进制,buffer是16进制

  • 在服务端,我们需要一个东西可以来标识内存,但是不能是字符串,因为字符串无法标识图片
  • node 中用 Buffer 来标识内存的数据,把内容转换成了 16 进制来显示(因为 16 进制比较短)
  • 10 进制 255 => 2 进制 0b11111111 => 16 进制 0xff => buffer 每个字节的取值范围就是 0 – 0xff
  • JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。但在处理像 TCP 流或文件流时,必须使用到二进制数据。因此在 Node.js 中,定义了一个 Buffer 类,该类用来创建一个 专门存放二进制数据的缓存区
  • (utf-8) 1 个汉字 3 个字节
  • buffer 对象类似于数组,他的 元素都是 16 进制的两位数 ,即 0~255 的数值
  • node 中 buffer 和字符串任意转换 (可能会出现乱码)

编码

  • node的编码目前只支持utf8,不同的编码占用的字节数不同,对于utf8而言,一个汉字是3个字节
  • 字节是能看得到的最小单位
  • 位 1个字节由8个位组成(二进制)组成
  • 将任何进制转换成10进制: 当前位的值 * 当前位^(所在位-1)累加的结果 => 对应方法: parseInt("1010", 2);
  • 将整数转换成其他进制 不停取余反向输出 => 对应方法: 10..toString(2)

2进制以0b开头 8进制以0o开头 16进制以0x开头

  • 小数也要转化成 2 进制
    • 10 进制中的 0.5 是 2 进制中的 0.1(因为 10 => 0.5 20 倍 所以 2 => 0.1 20 倍)
    • 十进制小数转为二进制的方法:乘 2 取整法可以将一个小数转化成 2 进制数
// 0.1 + 0.2的问题
// 计算并不是直接相加,而是把两个值都转化成2进制来计算

0.1 * 2 = 0.2 0
0.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 1
0.1 * 2 = 0.2 0
0.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 1

// 0.1转为二进制是一个无穷尽的小数
// 0.1转为二进制进行存储的时候会稍微比0.1大一点
// 0.2也是这样
// 所以 0.1 + 0.2 会大于 0.3

console.log(0.1+0.2) // 考察的是进制转化的问题

// js没有将小数转化成2进制方法
// 为什么 0.2+0.2 没有问题?

// 如果出现精度问题怎么解决?
复制代码

编码规范

  • ASCII
    • 一个字节最大是255,也就是可以代表255个数,大小写和一些特殊符号 最多就用了127个,一个字节就搞定了所有编码
  • GB8030/GBK
    • 255 对于中文是不够用的
    • 从127之后编制自己的, 不包含其他国家的
  • unicode
    • 组织 一套编码
  • UTF8
    • 借用unicode实现自己的一套机制
    • 如果是字符还是要遵循ASCII
    • 字符集
  • node 不支持 gbk,只支持 utf8

常见的编码实现–base64

由于某些系统中只能使用ASCII字符。Base64就是用来将非ASCII字符的数据转换成ASCII字符的一种方法。

  • base64 编码 : base64 只是一个编码规则,没有加密功能

  • base64 字符串可以放到任何路径的链接里,可以减少请求的发送,但是文件大小会变大,base64 转化完毕后会比之前的文件大 1/3

  • base64 的来源就是将每个字节都转化成小于 64 的值

  • base64原理

    • 一个汉字有3个字节,24位
    • 把一个汉字的24位 转换成4个字节, 每个字节就6位, 但是一个字节应该是有8位,所以不足的补0(注意是在前面补0)
// base64的方法
// 调用toString转化成指定编码
// 汉字“珠” 是3个字节 转换后是 54+g 是4个字节 比之前打了1/3
const r = Buffer.from('珠').toString('base64');
复制代码
/*
进制转换

base64 是一种进制转换
buffer里就有一个方法可以将buffer转换成base64
let buffer = Buffer.from("珠");
buffer.toString("base64")

-1.buffer是16进制,要将16进制转换成2进制 => toString()
(0xe7).toString(2);
(0x8f).toString(2);
(0xa0).toString(2);
// 11100111 10001111 10100000
=> 转换成4个字节, 每个字节就6位,
// 111001 111000 111110 100000
// 00111001 00111000 00111110 00100000 (前两位定死都是00, 最大值是00111111十进制就是63, 即转换后的值不会大于64)
-2.将这些值转化成10进制,去可见编码中取值 => parseInt()


parseInt("00111001", 2) 或者 parseInt(0b00111001) //57
parseInt("00111000", 2) //56
parseInt("00111110", 2) //62
parseInt("00100000", 2) //32


可见编码: 标准base64只有64个字符(英文大小写,数字,+, /)
let str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
str += "abcdefghijklmnopqrstuvwxyz";
str += "0123456789";
str += "+/";
console.log(str[57] + str[56] + str[62] + str[32]) // 就得到转化而成的base64编码

base64就是做了编码映射 并没有加密功能
*/
复制代码

Buffer

  • buffer 代表的是内存 内存是一段“固定空间” 产生的内存是固定大小 不能随意添加
  • 如果想要更改buffer的大小,改小可以截取内存,改大的话需要创造一个大的内存空间,将数据拷贝过去
  • 一般会用 alloc 声明一个 buffer 或者把字符串转换成一个 buffer
const buffer1 = Buffer.alloc(5);

// 像数组,但是和数组有区别,数组可以扩展,buffer不能扩展,可以用索引取值
console.log(buffer[0]); // 打印的是当前buffer里的这个字节所代表的十进制是多少

// 非常少使用,一般不会直接填16进制
const buffer2 = Buffer.from([0x24, 0x25, 0x26]);

const buffer3 = Buffer.from('珠峰'); // 对应6个字节
复制代码
  • 后台获取的数据都是 buffer,包括文件操作也都是 buffer 形式

定义 buffer 的三种方式

  1. let buf1 = Buffer.alloc(6);
  2. let buf2 = Buffer.from('珠峰');
  3. let buf3 = Buffer.from([65,66,67]);
  • 通过长度定义 buffer

    let buffer = Buffer.alloc(6)

    指定buffer大小, 参数是数字,最小是3, 数字指的是字节

    let buffer = Buffer.allocUnsafe(6)

  • 通过数组定义 buffer

    let buffer = Buffer.from([1,2,3,4]) 会自动将数组中的十进制数转化成 16 进制

    参数是数组,告诉要存入buffer的内容是什么,使用场景很少 ,数组里只能放数字0-255,超过255会取余

  • 字符串创建 buffer — 用得最多

    let buffer = Buffer.from(‘哈哈’) => 字符串转buffer

    buffer.length 指的是转换成 16 进制的 buffer 后的长度

    buffer.toString() => 将 buffer 转换成字符串
    buffer.toString() 不传参数默认是utf8

buffer 方法

  • buffer 功能是用来存储二进制数据,buffer 中的每一位都表示的是一个字节,如果显示成二进制,每一个字节就是显示成 8 位, 让其每个字节显示成 16 进制,就是显示两位,比较短,这就是 buffer 每个元素(每个字节)显示成 16 进制的原因
  • 无论是 2 进制还是 16 进制,他们表现的东西都是一样的
  • buffer 常用的方法

    buffer.fill 填充 buffer 中的内容

    buffer.toString 将 buffer 转化为字符串

    buffer.slice 截取想要的 buffer

    buffer.copy 拷贝 buffer

    buffer.concat buffer 的拼接方法

    buffer.isBuffer 判断是否是 buffer 类型

var buffer = Buffer.from([1, 2, 3]);
var newBuffer = buffer.slice(0, 1); // 拷贝出来的存放的是内存地址空间
newBuffer[0] = 100; //修改newBuffer,buffer也跟着修改

var buf1 = Buffer.from('珠峰');
var buf2 = Buffer.from('培训');
var buf = Buffer.allocUnsafe(12);
// 拷贝buffer(copy)
// targetBuffer目标buffer targetStart目标的开始 sourceStart源的开始 sourceEnd源的结束 this.length
// 将buf1/buf2拷贝到buf
buf1.copy(buf, 0);
buf2.copy(buf, 6);
console.log(buf, buf.toString());
//但这种做法 如果buffer很长,还要计算复制的位置,而且buffer定了就不能改变大小,
//所以concat更方便 ,内部自动开辟两个要拼接的buffer空间的总和然后进行拼接(concat的原理就是copy)
//连接buffer
Buffer.concat([buf1, buf2]).toString();

Buffer.prototype.MyCopy = function (
  targetBuffer,
  targetStart,
  sourceStart = 0,
  sourceEnd = this.length
) {
  for (let i = sourceStart; i < sourceEnd; i++) {
    targetBuffer[targetStart++] = this[i];
  }
};

Buffer.MyConcat = function (list, totalLength) {
  // 判断长度是否传递,如果传递了就用,没传自己算一个总长度
  if (typeof totalLength === 'undefined') {
    totalLength = list.reduce((prev, next) => prev + next.length, 0);
  }
  // 通过长度创建一个这么大的buffer, Buffer.alloc(len)
  let buffer = Buffer.alloc(totalLength);
  // 再循环list将每一项拷贝到这个大buffer上 buf.copy
  let offset = 0;
  list.forEach((buff) => {
    if (!Buffer.isBuffer(buff)) return;
    buff.copy(buffer, offset);
    offset += buff.length;
  });
  // 如果长度过长fill 或者采用slice截取有效长度
  // 返回一个新的buffer
  return buffer.slice(0, offset);
};
复制代码

fs

  • I/O input output 读文件 => 写操作 (以内存为参照物)
  • 读取的时候默认不写编码是 buffer 类型,如果文件不存在则报错
  • 写入的时候默认会将内容以 utf8 格式写入,如果文件不存在会创建(会将内容自动 toString(‘utf8’))
  • 读取的概念是将读取到的文件内容放到内存中,写入是读取内存中的内容写入到文件中

文件状态判断 — fs.stat

fs.stat('1.txt', function (err, stats) {
  if (err)
    // 文件不存在
    stats.isFile(); //是否是文件
  stats.isDirectory(); // 是否是文件夹
});
复制代码

创建文件夹 — fs.mkdir

// 不能跳级创建文件夹 创建如下文件夹d的前提是 a/b/c存在
fs.mkdir('a/b/c/d', function (err) {});
// 递归创建文件夹
function makep(url, cb) {
  let urlArr = url.split('/');
  let idnex = 0;
  function make(p) {
    if (urlArr.length < index) return; // 终止循环
    // 在创建之前看是否存在,如果不存在就创建,存在继续下一次创建
    fs.stat(p, function (err) {
      if (err) {
        //文件不存在
        fs.mkdir(p, function (err) {
          index++;
          make(urlArr.slice(0, index + 1).join('/'));
        });
      } else {
        // 如果存在跳到下一次创建
        make(urlArr.slice(0, ++index + 1).join('/'));
      }
    });
  }
  make(urlArr[index]);
}
makep('a/b/c/d', function (err) {
  console.log('创建成功');
});
复制代码

读取文件

  1. 去读文件 文件必须存在 不能通过/读取内容, /表示的是根目录
  2. 读取的默认类型是 buffer
let result = fs.readFileSync('log.js');
// 两种方式转result的格式:
result.toString();
fs.readFileSync('log.js', 'utf8');
复制代码

写入文件

  1. 读取类型都是 buffer, 写入的时候都是 utf8
  2. 读的文件必须存在, 写的时候文件不存在会自动创建,里面的内容会被覆盖掉
  3. 会对第二个参数(文件内容)默认调用 toString 方法【读取文件是 buffer, 调用 toString 可转为 utf8】
fs.writeFile('1.txt', 文件内容, (err) => {});
fs.writeFileSync('1.txt', 文件内容);
复制代码

读写文件(拷贝)

// 实现同步/异步读写文件函数
let fs = require("fs");

fucntion copySync(source, target) { // 同步 readFileSync + writeFileSync
    let result = fs.readFileSync(source, "utf8");
    fs.writeFileSync(target, result);
}
function copy(source, target, callback){ // 异步 readFile + writeFile
    fs.readFile(source, function(err, data) {
        if(err) return callback(err)
        fs.writeFile(target, data, callback)
    })
}
copy("1.txt", "2.txt", function(res) {
    console.log(res);
})
复制代码
  • 上面的写法只能读取完毕后再次写入,此方式适合小文件。大文件使用此方法会导致淹没可用内存
  • 边读边写
// 可忽略此代码块

// 实现边读边写 每次读三个  a.txt:123456789
const fs = require('fs');
const path = require('path');
let buf = Buffer.alloc(3);
fs.open(path.resolve(__dirname, 'a.txt'), 'r', function (err, fd) {
  // fd:file descriptor 是一个number类型,文件描述符 用完要关闭掉
  console.log(fd, 'fd');
  // 读取a.txt 将读取到的内容写入buffer的第0个位置,写3个,从文件的第6个位置开始写入
  fs.read(fd, buf, 0, 3, 6, function (err, bytesRead) {
    // bytesRead是读取到的真正个数
    console.log(bytesRead, 'bytesRead', buf);
    // 打开进行w操作就会将文件清空 写入的永远是前3个字节
    fs.open(path.resolve(__dirname, 'b.txt'), 'w', function (err, wfd) {
      // 将buffer的数据从0开始读取3个 写入文件的第0位置
      fs.write(wfd, buf, 0, 3, 0, function (err, written) {
        console.log(written);
        // 内部还要递归.....
        // 还得关闭文件
        fs.close(fd, () => {});
        fs.close(wfd, () => {});
      });
    });
  });
});
复制代码
  • w 写入操作 r 读取操作 a 追加操作 r+ 以读取为准可以写入操作 w+以写入为准可以执行读取操作
  • 权限: 3 组(当前用户的权限 用户所在组的权限 其他人权限) rwx 的组合(可读可写可执行) 421 => 7 => 最高权限 777(8 进制)
  • fs.open(source,”r”,0o666, function(){}) // 默认权限是 0o666 表示可读可写

基于文件系统操作的流

  • 下面的实现读和写耦合在了一起
  • fs是文件流,是文件操作中自己实现的流。文件流是继承于stream的,底层的实现用的就是fs.open

fs.read fs.write…

// 实现边读边写,一次拷贝三个字节
const fs = reuqire('fs');
function copy(source, target, cb) {
  const BUFFER_SIZE = 3;
  const buffer = Buffer.alloc(BUFFER_SIZE);
  const r_offset = 0;
  const w_offset = 0;
  // 读取一部分数据就写一部分数据
  fs.open(source, 'r', function (err, rfd) {
    fs.open(target, 'w', function (err, wfd) {
      //  回调的方式实现功能 需要用递归
      // 同步代码 可以采用while循环
      function next() {
        fs.read(rfd,buffer,0,BUFFER_SIZE,r_offset,function (err, bytesRead) {
            if (err) return cb(err);
            if (bytesRead) {
              fs.write(wfd,buffer,0,bytesRead,w_offset,function (err, written){
                  r_offset += bytesRead;
                  w_offset += written;
                  next();
                }
              );
            } else {
              fs.close(rfd, () => {});
              fs.close(wfd, () => {});
              cb();
            }
          }
        );
      }
      next();
    });
  });
}
copy('./a.txt', './b.txt', function (err) {});
复制代码

为了将读和写解耦, 采取发布订阅模式, 有了可读流/可写流

可读流

  • buffer 中的一个字节如果显示的是 30 => 16 进制的 30 转 10 进制:48 => 在 ascii 码中找到对应的
  • 流的概念和 fs 没有关系
  • fs 基于 stream 模块底层扩展了一个文件的读写方法
  • fs.createReadStream() 创建一个可读流对象 rs,rs.on 可以监听事件
  • 如果不监听“data”事件,是非流动模式,相当于买了个水管,但是水不会流出来
  • 当监听了“data”事件,就从非流动模式=>流动模式, 会不停的触发 data 事件的回调函数, 每次都拿到 highWaterMark 设置的值大小的数据
  • 当文件读取完毕后会触发 end 事件
  • 所以流是基于事件的
  • 可读流具备的方法:data/end/error/resume/pause, 只要具备这些方法就称为可读流
  • open 和 close 是文件流独有的方法

可读流的使用

const fs = require('fs');
// 返回一个可读流对象
const rs = fs.createReadStream('./a.txt', {
  // 创建可读流一般不用自己传递参数
  flags: 'r', // r读取 给fs.open用的
  encoding: null, // 默认读取出来的就是buffer
  autoClose: true, // 读取完毕后需要关闭流 fs.close
  emitClose: true, // 读取完毕后触发一个close事件
  start: 0,
  highWaterMark: 3, // 每次读取的数据个数 默认是64 * 1024 字节
});
// 如果不监听“data”事件,是非流动模式,相当于买了个水管,但是水不会流出来
// 会监听用户,绑定了data事件就会触发对应的回调,不停的触发
// open/close是文件流独有的
rs.on('open', function (fd) {
  // 打开文件事件
  console.log(fd);
});
rs.on('data', function (chunk) { // 如果绑定了data事件会不停的触发data
事件将内部的数据传递出来
  // 读取文件事件
  console.log(chunk);
  rs.pause(); // 暂停 不继续触发data事件
});
rs.on('end', function () {
  // 文件读取完毕事件
  console.log('end');
});
rs.on('close', function () {
  // 关闭文件事件
  console.log('close');
});
rs.on('error', function (err) {
  console.log(err, 'error');
});

setInterval(() => {
  rs.resume(); // 恢复触发data事件
}, 1000);
复制代码

可读流的实现

这里用到的思路值得学习

  • open 方法是异步的, 实现当 open 执行完成后再调用 read 方法读取文件
const fs = require('fs');
const EventEmitter = require('events');
class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0;
    this.end = options.end;
    this.highWaterMark = options.highWaterMark || 64 * 1024;

    this.open(); //  文件打开操作 注意这个方法是异步的

    // 注意用户监听了data事件才需要读取文件
    // this.read();

    // EventEmitter提供的,只要绑定方法,就在内部emit newListener
    // 这样就能监听到用户绑定了哪些事件,一绑定就触发
    // newListener的实现是:当绑定的方法名不是newListener,就触发newListener执行
    this.on('newListener', function (type) {
      console.log(type); // 当绑定某个事件的时候就会触发newListener事件
      if (type === 'data') {
        this.read();
      }
    });
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        return this.destroy(err);
      }
      this.fd = fd; // 供read方法使用
      this.emit('open', fd); // 触发open事件,这时候文件打开完成,可以拿到fd
    });
  }
  read() {
    // read方法需要拿到fd
    // 要在open异步回调执行后才能拿到fd
    if (typeof this.fd !== 'number') {
      // return this.once("open", (fd)  => {
      //     console.log(fd);
      // })
      return this.once('open', () => this.read());
    }
    console.log(this.fd); //一定能拿到fd
  }
  destroy(err) {
    if (err) {
      this.emit('error', err);
    }
  }
}
module.exports = ReadStream;
复制代码
// 完整实现
const fs = require('fs');
const EventEmitter = require('events');
class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0;
    this.end = options.end;
    this.highWaterMark = options.highWaterMark || 64 * 1024;

    // 偏移量
    this.offset = this.start;
    
    // 开关
    // false非流动模式,还没有开始读取文件中的内容。后续pause、resume就是更新这个flowing属性 
    this.flowing = false;

    this.open(); //  文件打开操作 注意这个方法是异步的

    // 注意用户监听了data事件才需要读取文件
    // this.read();

    // EventEmitter提供的,只要绑定方法,就在内部emit newListener
    // 这样就能监听到用户绑定了哪些事件,一绑定就触发
    // newListener的实现是:当绑定的方法名不是newListener,就触发newListener执行
    this.on('newListener', function (type) {
      console.log(type); // 当绑定某个事件的时候就会触发newListener事件
      if (type === 'data') {
        this.flowing = true;
        this.read();
      }
    });
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        return this.destroy(err);
      }
      this.fd = fd; // 供read方法使用
      this.emit('open', fd); // 触发open事件,这时候文件打开完成,可以拿到fd
    });
  }
  read() {
    // read方法需要拿到fd
    // 要在open异步回调执行后才能拿到fd this.open是异步的
    if (typeof this.fd !== 'number') {
      // return this.once("open", (fd)  => {
      //     console.log(fd);
      // })
      return this.once('open', () => this.read());
    }
    console.log(this.fd); //一定能拿到fd
    const buffer = Buffer.alloc(this.highWaterMark);
    // 读取文件中的内容,每次读取this.highWaterMark个
    let howMuchToRead = this.end
      ? Math.min(this.end - this.offset + 1, this.highWaterMark)
      : this.highWaterMark;
    fs.read(this.fd,buffer,0,howMuchToRead,this.offset,(err, bytesRead) => {
        if (bytesRead) {
          this.offset += bytesRead;
          this.emit('data', buffer.slice(0, bytesRead));
          if (flowing) {
            this.read(); // 继续读
          }
        } else {
          this.emit('end');
          this.destroy();
        }
      }
    );
  }
  destroy(err) {
    if (err) {
      this.emit('error', err);
    }
    if (this.autoClose) {
      fs.close(this.fd, () => this.emit('close'));
    }
  }
  pause() {
    this.flowing = false;
  }
  resume() {
    if (!this.flowing) {
      this.flowing = true;
      this.read();
    }
  }
}
module.exports = ReadStream;
复制代码

实现过程小结:

构造函数中设置默认参数,直接把文件打开,并且监听用户的事件,如果用户绑定了 data 事件就开始读取

但是在读取文件的过程中可能文件还没有打开,就绑定一个 open 事件,等待文件打开完毕后再调用 read 自己,相当于把 read 方法做了一个延迟

文件打开后会调用 open,这时候会再去调用 read,这时候 fd 已经有了,可以进行文件读取

每次读取的文件放到 buffer 当中,并将其抛出

如果是流动模式 就接着下一次读取,直到读取完毕,触发 end/close 事件

可写流

  • fs.createWriteStream(“./b.txt”)
  • 方法: fs.open / fs.write / fs.close
const fs = require('fs');
const ws = fs.createWriteStream('./b.txt', {
  flags: 'w',
  encoding: 'utf8',
  autoClose: true,
  start: 0,
  highWaterMark: 3, // 可写流的highWaterMark和可读流的不一样,表示的是期望用多少内存来写
});
let flag = ws.write('1');
flag = ws.write('2');
flag = ws.write('345');
ws.end(); // 相当于write+end
// 由于write方法是异步的,所以如果多个write方法同时操作一个文件,就会有出错的情况,
// 解决方式:除了第一次的write,将其他的排队,放入栈中,第一个完成,清空缓存区,
// 如果缓存区过大会导致浪费内存 ,所以会设置一个预期的值,来进行控制,达到预期 后就不要再调用write方法了
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享