编码
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 的三种方式
let buf1 = Buffer.alloc(6);
let buf2 = Buffer.from('珠峰');
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
截取想要的 bufferbuffer.copy
拷贝 bufferbuffer.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('创建成功');
});
复制代码
读取文件
- 去读文件 文件必须存在 不能通过/读取内容, /表示的是根目录
- 读取的默认类型是 buffer
let result = fs.readFileSync('log.js');
// 两种方式转result的格式:
result.toString();
fs.readFileSync('log.js', 'utf8');
复制代码
写入文件
- 读取类型都是 buffer, 写入的时候都是 utf8
- 读的文件必须存在, 写的时候文件不存在会自动创建,里面的内容会被覆盖掉
- 会对第二个参数(文件内容)默认调用 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方法了
复制代码