Buffer和文件流读写

6,835 阅读10分钟

一、进制转换


1. 二进制转化为十进制
1001 => 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0 = 9

parseInt('1001', 2); => 9
2. 十进制转化为二进制

除2取余

3. 任意进制转化为任意进制
# 0b 二进制  0o 八进制  0x 十六进制

(0x64).toString(2) => '1100100';
4. 0.1+0.2为啥不等于0.3

十进制中的0.5,二进制是多少

10 => 0.5202  => 0.1

小数转化为二进制怎么转?
乘二取整

0.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.2 * 2 ...

0.1转化为二进制可以看到无限循环了,IEEE754标准中,8个字节也就是64位表示一个数字,超出以后,会将超出部分截取掉,这就造成了精度缺失。

开发中怎么解决这种问题呢?

parseFloat(0.1+0.2).toFixed(10)  // "0.3000000000"

第三方库:github.com/MikeMcl/dec…

二、Buffer


Node中,用Buffer来标识内存的数据。node把内容转换成16进制来显示(16进制比较短,16进制最大0xff即最大是255)。

Node只支持UTF8编码格式。utf8一个汉字3个字节,GBK一个汉字两个字节。

Buffer代表的是内存,内存是一段固定的空间,阐述的内存是固定大小,不能随意添加。因此有了扩容的概念,动态创建一个新的内存,把内容迁过去。

1.Buffer.alloc() 声明buffer

// 开辟5个字节的内存
let buffer = Buffer.alloc(5) // <buffer 00 00 00 00 00>

2.Buffer.from() 字符串转buffer

// 指定每个字节的数值(超过255会取余)
let buffer = Buffer.from([1, 2, 3, 4, 5]); // <Buffer 01 02 03 04 05>
let buffer = Buffer.from([0x25, 0x26, 0x64]) // <buffer 25 26 64>

// 每个汉字,3个字节
let buffer = Buffer.from('天师'); // <Buffer e5 a4 a9 e5 b8 88>

3.Buffer.toString() 转化为指定编码

// 转化为base64
let buffer = Buffer.from('师').toString('base64'); // 5biI
// 转化为utf8
let buffer2 = Buffer.from('师').toString('utf8'); // 师
// 不传参默认是utf8
let buffer2 = Buffer.from('师').toString(); // 师

4.Buffer.slice 截取buffer

let buffer = Buffer.from([1, 2, 3, 4, 5]); // <Buffer 01 02 03 04 05>
let sliceBuffer = buffer.slice(0, 1);
sliceBuffer[0] = 100;
console.log(buffer); // <Buffer 64 02 03 04 05>

5.Buffer.copy buffer拷贝

let buf1 = Buffer.from('小小');
let buf2 = Buffer.from('天');
let buf3 = Buffer.from('师');

let bigBuffer = Buffer.alloc(12);

buf1.copy(bigBuffer, 6, 0, 6); // 跳过6个字节后,从第0个开始拷贝6个字节
buf2.copy(bigBuffer, 0, 0, 3);
buf3.copy(bigBuffer, 3); // 默认全拷贝进去

console.log(bigBuffer.toString()); // 天师小小

6.Buffer.concat buffer拼接

let buf1 = Buffer.from('小小');
let buf2 = Buffer.from('天');
let buf3 = Buffer.from('师');

let bigBuffer = Buffer.concat([buf1, buf2, buf3], 6).toString();
console.log(bigBuffer); // 小小

let bigBuffer = Buffer.concat([buf1, buf2, buf3]).toString();
console.log(bigBuffer); // 小小天师

7.Buffer.isBuffer

Buffer.isBuffer(bigBuf);

8.buffer.length

9.base64编码

base64编码过程:

let buffer = Buffer.from('师'); // <Buffer e5 b8 88>
let base64 = buffer.toString('base64'); // 5biI

// 将每个字节转化为二进制
console.log(0xe5.toString(2)); // 11100101
console.log(0xb8.toString(2)); // 10111000
console.log(0x88.toString(2)); // 10001000

// 11100101  10111000  10001000     // 8位 x 3字节 => 6位 x 4字节  // 多了三分之一字节
// 111001  011011  100010  001000

// 转化成十进制
console.log(parseInt('111001', 2)); // 57
console.log(parseInt('011011', 2)); // 27
console.log(parseInt('100010', 2)); // 34
console.log(parseInt('001000', 2)); // 8

let str = 'ABCDEFGHIJKLMNOPKRSTUVWXYZ';
str += str.toLocaleLowerCase();
str += '0123456789+/';

console.log(str[57] + str[27] + str[34] + str[8]); // 5biI

base64可以将字符串放到任意路径的链接里,但是文件体积会变大,base64转化完毕后会比之前大1/3。

三、文件操作


1. 文件读写

const fs = require('fs');
const path = require('path');

fs.readFile(path.resolve(__dirname, 'index.html'), (err, data) => {
    fs.writeFile(path.resolve(__dirname, './test.html'), data, (err, data) => {
        console.log(data);
    })
})

这种方式有个问题:必须全部读取完成后才可以写入,读取到的内容放到内存里,会造成内存淹没。

怎么做到边读边写呢?使用readwrite可以做到。

function copy(source, target, cb) {
    const BUFFER_SIZE = 3; // 每次读写3个字节
    const buffer = Buffer.alloc(BUFFER_SIZE);
    let r_offset = 0; // 偏移量
    let w_offset = 0;

    fs.open(source, 'r', (err, rfd) => { // rfd是打开文件的文件描述符
        fs.open(target, 'w', (err, wfd) => {
            function next() {
                fs.read(rfd, buffer, 0, BUFFER_SIZE, r_offset, (err, bytesRead) => {
                    if (err) return cb(err);
                    if (bytesRead) {
                        fs.write(wfd, buffer, 0, bytesRead, w_offset, (err, bytesWrite) => {
                            r_offset += bytesRead;
                            w_offset += bytesWrite;
                        });
                        next();
                    } else {
                        fs.close(rfd, () => {});
                        fs.close(wfd, () => {});
                        cb();
                    }
                })
            }
            next();
        })
    })
}

copy('./a.txt', './b.txt', (err) => {
    if (err) return console.log('err', err);
    console.log('copy success');
})

功能实现了,但是读写不分离,我们需要把读和写拆分开,怎么做呢?发布订阅实现读写分离。

四、可读流


可读流,可以控制读取的个数和读取的速率,可以实现边读编写,且读写分离。

fs模块基于stream流实现了文件读写流。fs.createReadStream用来读文件。

// 使用ReadStream

const fs = require('fs');
const ReadStream = require('./ReadStream.js');

// let rs = createReadStream('./a.txt', {
let rs = new ReadStream('./a.txt', {
    flags: 'r',
    encoding: null, // 编码是buffer类型
    autoClose: true, // 读取完成后自动关闭
    start: 0, // 从哪儿开始读
    // end: 1, // 包前包后,应该输出两个
    highWaterMark: 3 // 每次读取的数据个数,默认是64*1024字节
});

rs.on('open', function(fd) {
    console.log(fd);
})

rs.on('data', function(chunk) {
    console.log(chunk);
    // rs.pause(); // 暂停读取
})

rs.on('end', function () {
    console.log('end');
})

rs.on('close', function () {
    console.log('close');
})
rs.on('error', function (err) {
    console.log('err', err);
})

// setInterval(() => {
//     rs.resume(); // 恢复读取
// }, 1000)

1. read拿到文件标识符fd,接下来可以进行读取操作

// ReadStream.js

const EventEmitter = require('events');
const fs = require('fs');

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;

        // open之后要开始read,open是异步的
        this.open();

        // 用户监听了data事件,才需要读取
        // EventEmitter底层实现,绑定了监听事件,就会触发newListener
        this.on('newListener', function(type) { // type => open data end close error
            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);
        })
    }
    read() {
        // 为了拿到fd,多做了一次检测。
        // open是移步的,read是同步执行,直接拿的话是拿不到的。
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this.read())
        }
        console.log('拿到', this.fd);
    }
    destroy(err) {
        if (err) this.emit('error', err);
    }
}

module.exports = ReadStream;

2. 文件读取

如果配置了end,则根据end截取:

const EventEmitter = require('events');
const fs = require('fs');

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;

        // open之后要开始read,open是异步的
        this.open();

        // 用户监听了data事件,才需要读取
        // EventEmitter底层实现,绑定了监听事件,就会触发newListener
        this.on('newListener', function(type) { // type => open data end close error
            if (type === 'data') {
                this.read();
            }
+           this.offset = this.start;
        })
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) return this.destroy(err);
            this.fd = fd; // 文件标识绑定到实例上,read要用它
            this.emit('open', fd);
        })
    }
    read() {
        // 为了拿到fd,多做了一次检测。
        // open是移步的,read是同步执行,直接拿的话是拿不到的。
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this.read())
        }
        // console.log(this.fd);
        // 如果配置了end,则按照end截取
+       let howMatchToRead = this.end ? Math.min((this.end - this.offset + 1), this.highWaterMark) : this.highWaterMark;
+       const buffer = Buffer.alloc(howMatchToRead);
        // 读取文件,buffer类型,从第0个开始读,读取多少个,偏移量
+       fs.read(this.fd, buffer, 0, howMatchToRead, this.offset, (err, bytesRead) => {
            // 读取到数据
+           if (bytesRead) {
+               this.offset += bytesRead;
+               this.emit('data', buffer.slice(0, bytesRead));
+               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'));
+       }
    }
}

module.exports = ReadStream;

3. 暂停、继续读取

const EventEmitter = require('events');
const fs = require('fs');

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.flowing = false; // 是否递归读取

        // open之后要开始read,open是异步的
        this.open();

        // 用户监听了data事件,才需要读取
        // EventEmitter底层实现,绑定了监听事件,就会触发newListener
        this.on('newListener', function(type) { // type => open data end close error
            if (type === 'data') {
+               this.flowing = true;
                this.read();
            }
            this.offset = this.start;
        })
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) return this.destroy(err);
            this.fd = fd; // 文件标识绑定到实例上,read要用它
            this.emit('open', fd);
        })
    }
    read() {
        // 为了拿到fd,多做了一次检测。
        // open是移步的,read是同步执行,直接拿的话是拿不到的。
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this.read())
        }
        // console.log(this.fd);
        // 如果配置了end,则按照end截取
        let howMatchToRead = this.end ? Math.min((this.end - this.offset + 1), this.highWaterMark) : this.highWaterMark;
        const buffer = Buffer.alloc(howMatchToRead);

        fs.read(this.fd, buffer, 0, howMatchToRead, this.offset, (err, bytesRead) => {
            if (bytesRead) {
                this.offset += bytesRead;
                this.emit('data', buffer);
+               if (this.flowing) this.read();
            } else{
                this.emit('end');
                this.destroy();
            }
        })
    }
+   resume() {
+       if (!this.flowing) {
+           this.flowing = true;
+           this.read();
+       }
+   }
+   pause() {
+       this.flowing = false;
+   }
    destroy(err) {
        if (err) this.emit('error', err);
        if (this.autoClose) {
            fs.close(this.fd, () => this.emit('close'))
        }
    }
}

module.exports = ReadStream;

4. 完整可读流

const EventEmitter = require('events');
const fs = require('fs');

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.flowing = false;

        // open之后要开始read,open是异步的
        this.open();

        // 用户监听了data事件,才需要读取
        // EventEmitter底层实现,绑定了监听事件,就会触发newListener
        this.on('newListener', function(type) { // type => open data end close error
            if (type === 'data') {
                this.flowing = true;
                this.read();
            }
            this.offset = this.start;
        })
    }
    // 监听可读流的触发事件,实现读一点,写一点
+   pipe(ws) {
+       this.on('data', (data) => {
+           let flag = ws.write(data);
+           if (!flag) {
+               this.pause();
+           }
+       })
+       ws.on('drain', () => {
+           this.resume();
+       })
+   }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) return this.destroy(err);
            this.fd = fd; // 文件标识绑定到实例上,read要用它
            this.emit('open', fd);
        })
    }
    read() {
        // 为了拿到fd,多做了一次检测。
        // open是移步的,read是同步执行,直接拿的话是拿不到的。
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this.read())
        }
        // console.log(this.fd);
        // 如果配置了end,则按照end截取
        let howMatchToRead = this.end ? Math.min((this.end - this.offset + 1), this.highWaterMark) : this.highWaterMark;
        const buffer = Buffer.alloc(howMatchToRead);

        fs.read(this.fd, buffer, 0, howMatchToRead, this.offset, (err, bytesRead) => {
            if (bytesRead) {
                this.offset += bytesRead;
                this.emit('data', buffer);
                if (this.flowing) this.read();
            } else{
                this.emit('end');
                this.destroy();
            }
        })
    }
    resume() {
        if (!this.flowing) {
            this.flowing = true;
            this.read();
        }
    }
    pause() {
        this.flowing = false;
    }
    destroy(err) {
        if (err) this.emit('error', err);
        if (this.autoClose) {
            fs.close(this.fd, () => this.emit('close'))
        }
    }
}

module.exports = ReadStream;

五、可写流


let ws = fs.createWriteStream('./b.txt', {
    flags: 'w',
    encoding: 'utf-8',
    autoClose: true,
    start: 0,
    highWaterMark: 3, // 期望用多少内存来写,和文件读取的含义不同
})

ws.on('open', (fd) => {
    console.log('open', fd);
})
ws.on('close', () => {
    console.log('close');
})

let flag = ws.write('1');
console.log(flag); // true
flag = ws.write('2');
console.log(flag); // true
flag = ws.write('3');
console.log(flag); // false

ws.end();
  1. write方法是并发异步的,如果多个write方法同时操作一个文件,就会有出错的情况。
  2. 解决办法是:第一次允许直接写入文件,在第一次没有写入完成前,再有写入操作,则放入内存队列中。等到第一次写入完毕,将并发异步操作改为串行异步,一个一个从内存队列中拿出来执行写入操作。
  3. 写入超过highWaterMark预期的情况下,flag返回false,不超过返回true。

这种解法会额外占用内存,我们希望的是读一点,写一点,再没有写入完成前,暂停读取,写入完成后继续读取。如何做到,见第3小节。

1. 实现可写流

const fs = require('fs');
const EventEmitter = require('events');
// const Queue = require('./queue.js');

class WriteStream extends EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.flags = options.flags || 'w';
        this.encoding = options.encoding || 'utf8';
        this.mode = options.mode || 0o666;
        this.autoClose = options.autoClose || true;
        this.start = options.start || 0;
        this.highWaterMark = options.highWaterMark || 16 * 1024;
        
        this.len = 0; // 待写入文件的数据长度
        this.needDrain = false; // 需要触发drain
        this.cache = [];
        // this.cache = new Queue;
        this.writing = false; // 是不是正在写入
        this.offset = this.start; // 偏移量

        this.open();
    }
    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            this.fd = fd;
            this.emit('open', fd);
        })
    }
    write(chunk, encoding = this.encoding, cb = () => {}) {
        // 将数据转化为buffer
        chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);

        this.len += chunk.length;

        // 是否达到预期
        let returnValue = this.len < this.highWaterMark;
        this.needDrain = !returnValue;

        // AOP增加写入成功后清空缓存的逻辑
        let userCb = cb;
        cb = () => {
            userCb();
            this.clearBuffer();
        }

        // 判断是不是正在写入,如果为false,表示是第一次写,后面的都先放到cache缓存区
        if (!this.writing) {
            // 直接写入
            this.writing = true;
            this._write(chunk, encoding, cb);
        } else {
            // 保存到缓存区
            // this.cache.offer({
            this.cache.push({
                chunk,
                encoding,
                cb
            })
        }

        return returnValue;
    }
    _write(chunk, encoding, cb) {
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._write(chunk, encoding, cb));
        }
        fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, written) => {
            this.offset += written; // 维护偏移量
            this.len -= written; // 减少缓存的个数
            cb(); // 写入成功回调
        })
    }
    // 清空缓存区
    clearBuffer() {
        let data = this.cache.shift();
        // let data = this.cache.poll();
        // 继续写入
        if (data) {
            this._write(data.chunk, data.encoding, data.cb);
        // 缓存区都清空后
        } else {
            this.writing = false;
            if (this.needDrain) {
                this.needDrain = false;
                this.emit('drain');
            }
        }
    }
}

module.exports = WriteStream;

数组的好处是可以通过索引直接拿到某个元素。但是如果删除数组中间的元素,数组后面的元素都需要做移动,这是比较耗性能的。

链表的优势是头尾操作,但如果中间插入元素,链表也需要从头遍历,找要插入的位置,因此中间插入这种情况,链表相对于数组优势不明显。

可写流这种情形,正是操作的头尾,因此用链表性能更好。

// queue.js  用链表实现的队列

const LinkedList = require('./linkList.js');

class Queue {
    constructor() {
        this.ll = new LinkedList;
    }
    poll() {
        let removeNode = this.ll.remove(0);
        return removeNode && removeNode.element;
    }
    offer(element) {
        this.ll.add(element);
    }
}

module.exports = Queue;
// linkList.js  链表本表

class Node {
    constructor(element, next) {
        this.element = element;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
        this.size = 0; // 链表长度
    }
    add(index, element) {
        // 如果只传了一个参数,统一成两个的形式
        if (arguments.length === 1) {
            element = index;
            index = this.size;
        }
        // 越界判断
        if (index < 0 || index > this.size) throw new Error('越界');

        // 第一个节点
        if (index === 0) {
            let head = this.head;
            this.head = new Node(element, head);
            // 不是第一个节点,修改next指针
        } else {
            let prevNode = this._node(index - 1); // 找前一个节点,索引是size-1
            prevNode.next = new Node(element, prevNode.next);
        }

        this.size++;
    }
    remove(index) {
        let removeNode;
        // 只有一个的情况,删除的是头
        if (index === 0) {
            removeNode = this.head;
            if (removeNode) {
                this.head = this.head.next;
                this.size--;
            }
            // 有多个,则把上一个的指针指向以上一个为基础的,下一个的下一个
        } else {
            let prevNode = this._node(index - 1);
            removeNode = prevNode.next;
            prevNode.next = prevNode.next.next;
            this.size--;
        }
        return removeNode;
    }
    // 查找节点
    _node(index) {
        let current = this.head;
        for (let i = 0; i < index; i++) {
            current = current.next;
        }
        return current;
    }
}

let link = new LinkedList();

link.add(1);
link.add(2);
link.add(3);
// link.add(1, 20); // 根据索引添加
// link.remove(1);

// console.dir(link, { depth: 100 });

module.exports = LinkedList;

六、边读边写


边读边写节约内存,利用可写流的pauseresume, 可以实现边读边写。

const rs = fs.createReadStream('./a.txt', {
    highWaterMark: 3 // 读取默认64k
});

const ws = fs.createWriteStream('./b.txt', {
    highWaterMark: 2 // 写入默认16k
})

// 读取到的数据
rs.on('data', (data) => {
    let flag = ws.write(data);
    if (!flag) {
        console.log('吃不下了,正在消化');
        rs.pause(); // 暂停往文件里写
    }
})

// 数据写入完毕
ws.on('drain', (data) => {
    console.log('消化完了,继续喂我');
    rs.resume(); // 继续往文件里写
})
const ReadStream = require('./ReadStream.js');
const WriteStream = require('./WriteStream.js');
const rs = new ReadStream('./a.txt', {
    highWaterMark: 2 // 读取默认64k
});

const ws = new WriteStream('./b.txt', {
    highWaterMark: 1 // 写入默认16k
})
rs.pipe(ws);