node.js中的buffer和stream

1,528 阅读5分钟

node.js中的buffer和stream

node.js中处理数据有两种模式:buffer模式、stream模式

  • buffer模式:取完数据一次性操作,比如fs.readFile,readFileSync读取文件返回的就是一个buffer对象。
  • stream模式:边取数据边操作,常见的就是http请求流、响应流、输入io流和输出io流。fs.createReadStreamfs.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('文件关闭');
});

流的移动:

pipestream的移动是通过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是双工流,继承了ReadableWritable,一个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);
}

参考

我妈都看得懂的 Buffer 基础

Node 流(stream) (可读流、可写流、双工流、转换流)

Backpressuring in Streams