什么是二进制数据?

计算机是以二进制模式存储和示意数据,二进制是 01 的汇合。例如:01001010。比方,要存储数字 13 计算机须要将数字转换为 1101

二进制中的 01 被称为位(bit)。只管它们很像示意一个数值,然而其实它们示意是符号。0 代表FALSE, 1代表TRUE。位的运算其实是对虚实值的操作。为了存储数据,计算机蕴含了大量的电路,每一个电路能存储独自的一个。这种位存储器,被称为"主存储器"。计算机通过存储单元组织治理主存储器。一个典型的存储单元容量是8位,一个8位的串称为一个字节(byte)。

然而,数字不是咱们惟一须要存储解决的数据,咱们还须要解决字符串,图片,视频。

以字符串为例,那么计算机怎么存储字符串呢?例如,咱们要存储字符串 "S" ,计算机首先会将 "S", 转换为数字'S'.charCodeAt() === 83, 那么计算机是如何晓得83示意"S"的?

什么是字符集?

字符集是曾经定义好的规定,每一个字符都有一个确切的数字示意。字符集有不同规定的定义,例如:"Unicode", "ASCII"。浏览器中应用"Unicode"字符集。正是"Unicode"字符集,定义了83示意"S"。

那么接下来,计算机会间接将83转为二进制吗?并不是,咱们还须要应用"字符编码"。

什么是字符编码?

字符集定义了应用特定的数字示意字符(汉字也是同样的)。而字符编码定义了,如何将数字转换为特定长度的二进制数据。常见的utf-8字符编码,规定了字符最多由4个字节进行编码(一个字节由8个,0或者1示意)。

h e l l o <==Unicode==> 104 101 108 108 111 <==utf-8==> 1101000 1100101 1101100 1101100 1101111 

对于视频,音频,图片,计算机也有特定规定,将其转换为二进制数据。计算机将所有数据类型,存储为二进制文件。这些二进制文件就是二进制数据。

什么是缓冲区?

数据流指数据从一个地位到另一个地位的挪动(通常大文件会被拆解为块的模式)。

如果数据流动的速度,大于过程解决数据的速度,多余的数据会在某个中央期待。如果数据流动的速度,小于过程解决数据的速度,那么数据会在某个中央累计到肯定的数量,而后在由过程进行解决。(咱们无法控制流的速度)

那个期待数据,累计数据,而后产生进来的中央。就是缓冲区。缓冲区通常位于电脑的RAM(内存)中。

咱们能够把缓冲区设想成一个公交车站,较早达到车站的乘客的会期待公交车,当公交车以及装满来到后,剩下的乘客会期待下一班的公交车到来。那个期待公交车的中央,就是缓冲区。

举一个常见的缓冲区的例子,咱们在观看在线视频的时候,如果你的网速很快,缓冲区总是会被立刻填充,而后发送进来,而后立刻缓冲下一段视频。观看的过程中,不会有卡顿。如果网速很慢,则会看到loading,示意缓冲区正在被填充,当填充实现后数据被发送进来,能力看到这段视频。

Buffer类

什么是TypedArray?

TypedArray呈现前,js没有读取或操作二进制数据流的机制。TypedArray并不是一个特定的全局对象,而是许多全局对象的统称。

// TypedArray 指的是上面其中之一Int8Array // 8位二进制补码有符号整数的数组Uint8Array // 8位无符号整型数组(0 > & < 256(8位无符号整形)),等等Uint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64Array

什么是Buffer类?

Buffer类,是以Nodejs形式实现的Uint8ArrayAPI(Uint8Array属于TypedArray的一种)。用于与八字节的二进制数据进行交互。

常见的API

Buffer.from

Buffer.from, 承受多种形式的参数,上面介绍几种常见的

应用8字节的数组,作为参数

const buf = Buffer.from([0b1101000, 0b1100101, 0b1101100, 0b1101100, 0b1101111])// helloconsole.log(buf.toString())

应用字符串作为参数

const buf = Buffer.from('Hello World!');// HelloWorldconsole.log(buf.toString())

Buffer.alloc

创立一个指定大小,并且曾经初始化的Buffer(默认被0填充)

// 创立一个大小为10个字节的Buffer,并应用0进行填充const buf = Buffer.alloc(10)// <Buffer 00 00 00 00 00 00 00 00 00 00>console.log(buf)// 创立一个大小为12个字节的Buffer,并应用汉字进行填充const buf = Buffer.alloc(12, '大')// 打印出 大大大大,因为汉字是3个字节的关系,所以填充了4个汉字console.log(buf.toString())

Buffer.allocUnsafe

创立一个指定大小,然而没有初始化填充的Buffer

// 创立一个大小为10个字节的Buffer,没有被初始化const buf = Buffer.allocUnsafe(10)
为什么说 Buffer.allocUnsafe 是不平安的?Buffer是内存的形象,尝试运行console.log(Buffer.allocUnsafe(10000).toString()), 咱们应该能够从控制台看到打印出了内存里的一些货色

buffer.write

向Buffer中写入字符串,如果Buffer空间不够,多余的字符串不会被写入

const buf = Buffer.alloc(5)// 您好的长度是6个字节buf.write('您好')// 您console.log(buf.toString())

buffer.toJSON

将buffer中的数据转换为Unicode码

const buf = Buffer.from('hello')// {//     type: 'Buffer',//     data: [ 104, 101, 108, 108, 111 ] h e l l o 的 Unicode码// }console.log(buf.toJSON())

buffer.toString

将buffer解码成字符串

const buf = Buffer.from([0b1101000, 0b1100101, 0b1101100, 0b1101100, 0b1101111 ])// helloconsole.log(buf.toString())

String Decoder

思考上面这种状况,因为两个汉字的字节长度是6,所以字节长度等于5的buffer是放不下的,所以打印进去的字符串是不残缺的。

const buf = Buffer.alloc(5, '您好')// 您�console.log(buf.toString())

那么有什么方法能够将Buffer中不残缺的字符串输入进去呢?咱们能够应用String Decoder

const { StringDecoder } = require('string_decoder')const decoder = new StringDecoder('utf8')// Buffer.from('好') <Buffer e5 a5 bd>const str1 = decoder.write(Buffer.alloc(5, '您好'))// 您console.log(str1)const str2 = decoder.end(Buffer.from([0xbd]))// 好console.log(str2)

StringDecoder的实例承受写入Buffer的实例,应用外部缓冲区确保解码的字符串不蕴含不实现的字节,并且将不残缺的字节,保存起来,直到下一次应用write或者end。

decoder.write

返回已解码的字符串,字符串不蕴含不残缺的字节,不残缺的字节。不残缺的字节会保留到decoder外部的缓冲区中。

const { StringDecoder } = require('string_decoder')const decoder = new StringDecoder('utf8')// 哈喽 <Buffer e5 93 88 e5 96 bd>const str = decoder.write(Buffer.from([0xe5, 0x93, 0x88, 0xe5, 0x96]))// 哈,0xe5, 0x96因为不残缺不会被返回,而是保留在decoder的外部缓冲区console.log(str)

decoder.end

会将decoder外部缓存区残余的buffer一次性返回。

const { StringDecoder } = require('string_decoder')const decoder = new StringDecoder('utf8')// 哈喽 <Buffer e5 93 88 e5 96 bd>decoder.write(Buffer.from([0xe5, 0x93, 0x88, 0xe5, 0x96]))const str = decoder.end()// �,decoder外部缓冲区残余的字节是不残缺的console.log(str)

什么是流?

流是数据的汇合,流不像字符串或者数组一样是立刻可用的,流不会全副存在内存中。解决大量数据时,流十分有用。

Nodejs中,许多模块都实现了流模式。下图是实现了流模式的内置模块(图片来自于Samer Buna的在线课程)

流的类型

  1. Writable,可写入流,是写入指标的形象,常见的例子:fs.createWriteStream
  2. Readable,可读取流,是数据源的形象。常见的例子:fs.createReadStream
  3. Duplex,可读可写流(双工流)
  4. Transform,也是一种可读可写流,然而能够读取写入的时候批改转换数据。所以也能够叫做转换流。例如:zlib.createGzip压缩数据流

流的管道

const fs = require('fs')// 可读流作为数据源const readable = fs.createReadStream('./数据源.json')// 可写流作为指标const writable = fs.createWriteStream('./指标.json')// 将数据源通过管道连贯到指标readable.pipe(writable)

在这几行简略的代码中咱们将可读流的输入(readable作为数据源),连贯管道至可写流的输出(writable作为指标)。源必须是可读流,指标必须是可写流。

const fs = require('fs')const zlib = require('zlib')const readable = fs.createReadStream('./数据源.json')// gzip是一个双工流const gzip = zlib.createGzip()const writable = fs.createWriteStream('./指标.gz')// 数据源连贯到转换流(gzip),转换流解决数据后,连贯到指标上readable    .pipe(gzip)    .pipe(writable)

咱们也能够将可读流的管道连贯到双工流(转换流)上。总结一下pipe办法的用法。pipe能够返回一个指标流,指标流能够连贯到双工流,可写流上。

可读流    .pipe(双工流)    .pipe(双工流)    .pipe(可写流)

应用pipe是生产流最简略的办法,它会主动治理一些操作,比方错误处理,比方如果可读流没有数据可供生产时的状况。当然咱们也能够通过事件生产流,然而最好防止两者混合应用。

流的事件

如果须要对流实现,更自定义的管制,能够应用事件生产流。上面的这段代码和之前的pipe的代码是等效的。

const fs = require('fs')const readable = fs.createReadStream('./数据源.json')const writable = fs.createWriteStream('./指标.json')// 当可读流绑定data事件时,会将流切换到流动模式readable.on('data', (chunk) => {    writable.write(chunk);})readable.on('end', () => {    writable.end()})

下图是可读流,可写流的事件与办法的列表(图片来自于Samer Buna的在线课程)

对于下面的例子中存在的一些问题

下面对于流事件的示例中,是存在隐患的。具体的问题起因,能够查看我的这篇文章简略了解 backpressure(背压)机制

// 其实这段代码是其实有问题的readable.on('data', (chunk) => {    writable.write(chunk);})

流的可读流的暂停和流动模式

默认状况下,可读流是处于暂停状态的,然而它们能够被切换到流动模式,并在须要时切换回暂停模式。有时,模式会被主动产生切换。

在暂停模式时,咱们能够应用read办法从流中读取数据。

const fs = require('fs')const readable = fs.createReadStream('./数据源.json')const writable = fs.createWriteStream('./指标.json')// 当可读流是能够被读取时或者会发生变化时或者达到流的止境时。readable能够被触发readable.on('readable', () => {    let chunk    while (chunk = readable.read(1)) {        writable.write(chunk)    }})

在流动模式时,数据会继续流动,咱们必须增加事件并生产它。如果不能解决流动的数据,数据是会失落的。咱们能够增加data事件处理数据。增加data事件,会主动将可读流的模式,由暂停模式切换到流动模式。

如果须要实现两种模式的手动切换能够应用resume()(从暂停模式中复原)和pause()(进入暂停模式)办法。(如果监听了可读流的readable的事件,resume()办法有效)

上面的例子中,每一次通过可写流写入一点数据,都会暂停一秒可读流1s,1s后持续写入。

const fs = require('fs')const readable = fs.createReadStream('./数据源.json')const writable = fs.createWriteStream('./指标.json')// 主动切换到流动模式readable.on('data', (chunk) => {    console.log('写入')    writable.write(chunk)    readable.pause()    console.log('暂停')    // 暂停1s后,从新切换到流动模式    setTimeout(() => {        readable.resume()    }, 1000)})

下图是两种模式之前的切换(图片来自于Samer Buna的在线课程)。增加data事件时(独自增加data事件,不存在readable事件时),模式会主动切换。

流的实现

创立自定义可写流

创立一个自定义的可写流,咱们须要继承 stream.Writable 类。并在子类中实现 _write 办法。

const { Writable } = require('stream')class CustomWritable extends Writable {    /**     * @param chunk 须要写入的数据     * @param encoding 编码格局     * @param next 解决实现的回调     */    _write(chunk, encoding, next) {        try {            // 仅仅是做了一个打印            console.log(`仅仅做了一个打印:${chunk.toString()}`)            next()        } catch (error) {            next(error)        }    }}

这个流自身没有多大的意义。这个自定义可写流,只会对可读流输出的数据,进行一个打印。咱们能够链接到 process.stdin 可读流上,对终端的输出,进行一个打印。

const customWritable = new CustomWritable()process.stdin.pipe(customWritable)

创立自定义可读流

创立一个可读流,咱们须要继承 stream.Readable 类。并在子类中实现 _read 办法。

const { Readable } = require('stream'); class CustomReadable extends Readable {    _read () {    }}

这个流自身也没有很大的意义。咱们能够应用 push 办法,将数据增加到流的外部队列中,以供流进行生产。咱们能够联合前一个自定义的可写流,将 push 的数据打印进去。

const customReadable = new CustomReadable()const customWritable = new CustomWritable()customReadable.push('我喜爱西尔莎罗南')// 告诉流不会有任何数据了customReadable.push(null)// 可读流将数据传递给可写流// 可写流将数据打印进去customReadable.pipe(customWritable)

创立自定义转换流

创立一个可读流,咱们须要继承 stream.Transform 类。并在子类中实现 _transform 办法。

const { Transform } = require('stream')class CustomTransform extends Transform {    _transform (chunk, encoding, next) {        // 咱们把可读流的内容,全副转换为大写        chunk = chunk.toString().toUpperCase()        next(null, chunk)    }}

这个自定义流,会将可读流传过去的字符串,全副转换为大写。

const customReadable = new CustomReadable()const customWritable = new CustomWritable()const customTransform = new CustomTransform()customReadable.push('abcdefg')customReadable.push(null)customReadable    .pipe(customTransform)    .pipe(customWritable)

对象模式

Node中的流,默认都是应用Buffer 或者 字符串进行传输,咱们能够开启 objectMode 开关。使流能够传输js的对象。

const { Readable, Writable } = require('stream')class CustomReadable extends Readable {    _read () {    }}class CustomWritable extends Writable {    _write(chunk, _, next) {        try {            // 这里能够打印js对象            // chunk能够使js对象            console.log(chunk)            next()        } catch (error) {            next(error)        }    }}const customReadable = new CustomReadable({    objectMode: true // 开启对象模式})const customWritable = new CustomWritable({    objectMode: true // 开启对象模式})// 咱们能够传输对象了customReadable.push(['a', 'b', 'c', 'd'])customReadable.push(null)customReadable.pipe(customWritable)

参考

  • 计算机科学概论(第11版)
  • ✨Node.js Streams: Everything you need to know
  • ✨Do you want a better understanding of Buffer in Node.js? Check this out
  • Node.js v13.8.0 Documentation