node.js中的buffer和stream
node.js中处理数据有两种模式:buffer模式、stream模式
buffer模式:取完数据一次性操作,比如fs.readFile,readFileSync读取文件返回的就是一个buffer对象。stream模式:边取数据边操作,常见的就是http请求流、响应流、输入io流和输出io流。fs.createReadStream、fs.createWriteStream返回的就是一个stream对象。
Node 中的 Buffer
Buffer 类是直接处理二进制数据的全局类型。 它可以使用多种方式构建。buffer对象,代表着一组二进制数据的缓存。
常用API
Buffer.alloc(size[, fill[, encoding]]) 创建一个 buffer 空间,可以填充制定元素,也可以指定编码类型
Buffer.from() 以 buffer 方式存储内容
Buffer.from(array)创建一个 buffer 数组 buf,buf.values()返回一个可以迭代的对象Buffer.from(string[, encoding])创建一个 buffer 字符串,可以指定编码类型
Buffer.from(buffer)拷贝一个 buffer 对象
import { Buffer } from 'buffer';
// 创建长度为 10 的以零填充的缓冲区。
const buf1 = Buffer.alloc(10);
console.dir(buf1) //Buffer(10) [Uint8Array] [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// 创建长度为 10 的缓冲区,
// 使用值为 `1` 的字节填充。
const buf2 = Buffer.alloc(10, 1);
console.dir(buf2)//Buffer(10) [Uint8Array] [ 1, 1, 1, 1, 1,1, 1, 1, 1, 1]
// 创建包含字节 [1, 2, 3] 的缓冲区。
const buf3 = Buffer.from([1, 2, 3]);
console.dir(buf3) //Buffer(3) [Uint8Array] [ 1, 2, 3 ]
// 创建包含字节 [1, 1, 1, 1] 的缓冲区,
// 所有条目都使用 `(value & 255)` 截断以符合范围 0–255。
const buf4 = Buffer.from([257, 257.5, -255, '1']);
console.dir(buf4)//Buffer(4) [Uint8Array] [ 1, 1, 1, 1 ]
// 创建包含字符串 'tést' 的 UTF-8 编码字节的缓冲区:
// [0x74, 0xc3, 0xa9, 0x73, 0x74](十六进制)
// [116, 195, 169, 115, 116](十进制)
const buf5 = Buffer.from('tést');
console.dir(buf5)//Buffer(5) [Uint8Array] [ 116, 195, 169, 115, 116 ]
// 创建包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的缓冲区。
const buf6 = Buffer.from('tést', 'latin1');
console.dir(buf6)//Buffer(4) [Uint8Array] [ 116, 233, 115, 116 ]
stream 流
流是用于在 Node.js 中处理流数据的抽象接口。 stream 模块提供了用于实现流接口的 API。
stream会在移动中触发各种事件,
const fs = require('fs');
//创建一个文件可读流
let rs = fs.createReadStream('./test.txt');
//文件被打开时触发
rs.on('open', function () {
console.log('文件打开');
});
//监听data事件,会让当前流切换到流动模式
//当流中将数据传给消费者后触发
//由于我们在上面配置了 highWaterMark 为 3字节,所以下面会打印多次。
rs.on('data', function (data) {
console.log(data);
});
//流中没有数据可供消费者时触发
rs.on('end', function () {
console.log('数据读取完毕');
});
//读取数据出错时触发
rs.on('error', function () {
console.log('读取错误');
});
//当文件被关闭时触发
rs.on('close', function () {
console.log('文件关闭');
});
流的移动:
pipe:stream的移动是通过pipe(管道)来实现的。copy 文件可以将Readable 流移动到Writable 目标上,达到的copy目的。
const fs = require('fs');
//创建一个可读流
let rs = fs.createReadStream('./1.txt');
//创建一个可写流
let ws = fs.createWriteStream('./2.txt');
//绑定可写流到可读流,自动将可读流切换到流动模式,将可读流的所有数据推送到可写流。
rs.pipe(ws);
如果 Readable 流在处理过程中触发错误,则 Writable 目标不会自动关闭。 如果发生错误,则需要手动关闭每个流以防止内存泄漏。
所以要注意,使用.pipe()从一端到另一端获取数据源,如果无法正确接收数据块,则Readable源或 gzip流将不会被销毁。pipe需要手动监听异常或者关闭事件,如果使用source.pipe(dest)方法,如果dest发出'close'或'error',pipe并不会销毁source。
所以,官方推荐我们使用pump库,pump与pipe不同,如果有某个流发生错误或者关闭,pump会自动销毁相关所有的流。
类似stream.pipe(),pump()返回传入的最后一个流。
var pump = require('pump')
var fs = require('fs')
var source = fs.createReadStream('/dev/random')
var dest = fs.createWriteStream('/dev/null')
pump(source, dest, function(err) {
console.log('pipe finished', err)
})
Stream和Buffer互转
//Stream转Buffer
function streamToBuffer(stream) {
return new Promise((resolve, reject) => {
let buffers = [];
stream.on('error', reject);
stream.on('data', (data) => buffers.push(data))
stream.on('end', () => resolve(Buffer.concat(buffers))
});
}
//Buffer转Stream
let Duplex = require('stream').Duplex;
function bufferToStream(buffer) {
let stream = new Duplex();
stream.push(buffer);
stream.push(null);
return stream;
}
Duplex是双工流,继承了Readable和Writable,一个Duplex对象既可当成可读流来使用,也可以当做可写流来使用。
文件压缩
node.js 有原生压缩api,zlib,其压缩和解压缩是围绕 Node.js stream构建的。
const gzip = require('zlib').createGzip();
const fs = require('fs');
const inp = fs.createReadStream('The.Matrix.1080p.mkv');
const out = fs.createWriteStream('The.Matrix.1080p.mkv.gz');
inp.pipe(gzip).pipe(out);
不过我们可以用写好的npm包,compressing,它可以promise调用和流式调用。
//压缩文件夹,开启gzip压缩
import compressing, { tgz } from 'compressing'
import pump from 'pump'
// 流式调用
async function compress(source: string){
const tarStream = new compressing.tar.Stream()
tarStream.addEntry(source)
const destStream = pump(tarStream, new compressing.gzip.FileStream())
return destStream
}
//promise调用
compressing.gzip.compressFile('xxx.zip')
.then(() => {
console.log('success');
})
.catch(err => {
console.error(err);
});
koa中的流处理
//koa中响应文件流
async download(ctx) {
const id = ctx.params.id
const result = await fileService.download(ctx, id)
const filename = basename(result.filePath)
//设置请求头
ctx.set('Content-Disposition', `attachment;fileName=${filename}`)
//创建可读流,并响应
ctx.body = fs.createReadStream(result.filePath)
}
koa中的响应流也做了处理,如果ctx.body 等于一个流对象,则会把这个流对象移动到http响应流
if (body instanceof Stream) return body.pipe(res);
//koa中的application.js
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' === ctx.method) {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' === typeof body) return res.end(body);
//这里对流处理
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}