什么是二进制数据?计算机以二进制形式存储和表示数据,二进制形式是0和1的集合。例如:0100、1010。例如,计算机要存储数字13,需要将数字转换为1101。二进制中的0和1称为位。虽然它们看起来代表一个数值,但它们代表的是符号。0代表假,1代表真。位操作实际上是对真值和假值的操作。为了存储数据,计算机包含大量电路,每个电路都能够存储一个位。这种位存储器称为“主存储器”。计算机将其主内存组织成存储单元。一个典型的存储单元容量是8位,一个8位的字符串称为一个字节(byte)。然而,数字并不是我们需要存储和处理的唯一数据,我们还需要处理字符串、图像和视频。以字符串为例,计算机是如何存储字符串的?例如,我们要存储字符串“S”,计算机会先将“S”转换为数字“S”。charCodeAt()===83,那么计算机怎么知道83是“S”的意思呢?什么是字符集?字符集是定义明确的规则,每个字符都有精确的数字表示。字符集由不同的规则定义,例如:“Unicode”、“ASCII”。浏览器使用“Unicode”字符集。它是为“S”定义83的“Unicode”字符集。那么接下来,计算机会不会直接把83转成二进制呢?不,我们还需要使用“字符编码”。什么是字符编码?字符集定义了用特定的数字来表示字符(汉字也一样)。字符编码定义了如何将数字转换为特定长度的二进制数据。常见的utf-8字符编码规定字符最多由4个字节编码(一个字节用8、0或1表示)。hello<==Unicode==>104101108108111<==ut??f-8==>11010001100101110110011011001101111对于视频、音频、图片,计算机也有特定的规则将它们转换成二进制数据。计算机将所有数据类型存储为二进制文件。这些二进制文件是二进制数据。什么是缓冲区?数据流是指数据从一个位置移动到另一个位置(通常将大文件分解成块)。如果数据流动的速度快于进程处理它的速度,多余的数据就会在某处等待。如果数据流动的速度小于进程处理数据的速度,那么数据就会在某处积累到一定数量,然后被进程处理。(我们无法控制流的速度)等待数据的地方,积累数据,然后正好出去。它是缓冲区。缓冲区通常位于计算机的RAM(内存)中。我们可以把缓冲区想象成一个公共汽车站。较早到达车站的乘客将等待公共汽车。当公交车满员离开时,剩下的乘客将等待下一班车的到来。等车的地方就是缓冲区。举一个常见的缓冲区的例子,我们在看在线视频的时候,如果你的网速很快,缓冲区总是会立即被填满,然后发送出去,然后马上缓冲下一个视频。在观看过程中,不会有任何延迟。如果网速很慢,你会看到loading,这意味着缓冲区正在被填充。填写完成后,将数据发送出去看这个视频。缓冲类什么是TypedArray?在TypedArray出现之前,js没有读取或操作二进制数据流的机制。TypedArray并不是一个具体的全局对象,而是很多全局对象的总称。//TypedArray指的是以下之一Int8Array//8位二进制补码有符号整数数组Uint8Array//8位无符号整数数组(0>&<256(8位无符号整数))等。Uint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64Array什么是Buffer类?Buffer类是Nodejs中实现的Uint8ArrayAPI(Uint8Array是TypedArray的一种)。用于与八字节二进制数据交互。通用的APIBuffer.fromBuffer.from接受各种形式的参数。下面介绍几种常用的以8字节为参数的数组。//helloconsole.log(buf.toString())使用字符串作为参数constbuf=Buffer.from('HelloWorld!');//HelloWorldconsole.log(buf.toString())Buffer.alloc创建指定大小,以及初始化的Buffer(默认填充0)//创建一个大小为10字节的Buffer,并填充0constbuf=Buffer.alloc(10)//console.log(buf)//创建一个12字节大小的Buffer,并用汉字填充constbuf=Buffer.alloc(12,'big')//大大打印出来,因为汉字是3个字节的关系,所以填充了4个汉字notinitializethefilledBuffer//CreateaBufferwithsizeof10bytes,Notinitializedconstbuf=Buffer.allocUnsafe(10)为什么Buffer.allocUnsafe不安全?Buffer是内存的抽象,尝试运行console.log(Buffer.allocUnsafe(10000).toString()),我们应该可以从console看到打印内存中的一些东西buffer.writewritesastringtoBuffer,如果Buffer空间不够,则不会写入多余的字符串constbuf=Buffer.alloc(5)//Hello长度为6字节buf.write('Hello')//你的console.log(buf.toString())buffer.toJSON将缓冲区中的数据转换为Unicode编码constbuf=Buffer.from('hello')//{//type:'Buffer',//data:[104,101,108,108,111]hello//}console.log(buf.toJSON())buffer.toString将缓冲区解码成字符串constbuf=Buffer.from([0b1101000,0b1100101,0b1101100,0b1101100,0b1101111])//helloconsole.log(buf.toString())StringDecoder考虑以下In本例中,由于两个汉字的字节长度为6,字节长度为5的缓冲区放不下,所以打印出来的字符串是不完整的。constbuf=Buffer.alloc(5,'Hello')//如果你console.log(buf.toString()),有没有办法输出Buffer中不完整的字符串?我们可以使用字符串解码器const{StringDecoder}=require('string_decoder')constdecoder=newStringDecoder('utf8')//Buffer.from('good')conststr1=decoder.write(Buffer.alloc(5,'Hello'))//你的console.log(str1)conststr2=decoder.end(Buffer.from([0xbd]))//好的console.log(str2)StringDecoder实例接受写入实例ofBuffer使用内部缓冲区来确保解码后的字符串不包含不完整的字节,并将不完整的字节保存到下一次使用write或end。decoder.write返回解码后的字符串,字符串不包含不完整的字节,incompletebytes。不完整的字节将存储在解码器内部的缓冲区中。const{StringDecoder}=require('string_decoder')constdecoder=newStringDecoder('utf8')//helloconststr=decoder.write(Buffer.from([0xe5,0x93,0x88,0xe5,0x96]))//哈,0xe5,0x96由于不完整不会返回,而是存放在解码器的内部缓冲区中。console.log(str)decoder.end会将解码器内部缓冲区中剩余的缓冲区保存一次返回。const{StringDecoder}=require('string_decoder')constdecoder=newStringDecoder('utf8')//hellodecoder.write(Buffer.from([0xe5,0x93,0x88,0xe5,0x96]))conststr=decoder.end()//?,decoder内部buffer中剩余字节不完整console.log(str)什么是stream?流是数据的集合。与字符串或数组不同,流是立即可用的,并且流并不都存在于内存中。在处理大量数据时,流非常有用。在Nodejs中,很多模块都实现了流模式。下图是一个实现流模式的内置模块(图片来自SamerBuna的在线课程)。流类型Writable,可以写入流,是对writetarget的抽象。一个常见的例子:fs.createWriteStreamReadable,可以读取流,是一个数据源的抽象。常见例子:fs.createReadStreamDuplex,可读可写流(duplexstream)Transform,也是可读可写流,但是读写时可以修改转换后的数据。所以也可以称为转换流。例如:zlib.createGzip压缩数据流管道constfs=require('fs')//可读流作为数据源constreadable=fs.createReadStream('./datasource.json')//可写流作为目标constwritable=fs.createWriteStream('./target.json')//将数据源连接到目标readable.pipe(writable)在这些简单的代码行中我们将读取流的输出(作为数据源可读),连接管道到可写流的输入(可写为目标)。源必须是可读流,目标必须是可写流。constfs=require('fs')constzlib=require('zlib')constreadable=fs.createReadStream('./datasource.json')//gzip是双工流constgzip=zlib.createGzip()constwritable=fs.createWriteStream('./target.gz')//数据源连接到转换流(gzip),转换流处理完数据后连接到目标可读.pipe(gzip).pipe(可写)我们也可以将可读流通过管道传输到双工流(转换流)。总结一下pipe方法的用法。pipe可以返回一个目标流,它可以连接到一个双工流,可写流。可读流.pipe(duplexstream).pipe(duplexstream).pipe(writablestream)使用pipe是最简单的流消费方式,它会自动管理一些操作,比如错误处理,比如如果是可读流的情况当没有数据可供消费时。当然我们也可以通过事件来消费流,但是最好避免两者混用。流事件如果需要对流实现更多的自定义控制,可以使用事件消费流。下面的代码等价于前面的管道代码。constfs=require('fs')constreadable=fs.createReadStream('./datasource.json')constwritable=fs.createWriteStream('./target.json')//当可读流绑定数据事件当readable.on('data',(chunk)=>{writable.write(chunk);})readable.on('end',()=>{writable.end()})下图是一个列表可读流和可写流的事件和方法(图片来自SamerBuna的在线课程)关于上面例子中的一些问题,上面的流事件例子存在隐患。具体问题原因可以查看我的这篇文章简单了解背压(backpressure)机制//其实这段代码其实是有问题的readable.on('data',(chunk)=>{writable.write(chunk);})可读流的暂停和流动模式forstreams可读流在默认情况下是暂停的,但它们可以在需要时切换到流动模式并返回到暂停模式。有时,模式切换会自动发生。在暂停模式下,我们可以使用read方法从流中读取数据。constfs=require('fs')constreadable=fs.createReadStream('./datasource.json')constwritable=fs.createWriteStream('./target.json')//当可读流可以被读取时获取时间是发生更改或到达流末尾的时间。readable可以被触发readable.on('readable',()=>{letchunkwhile(chunk=readable.read(1)){writable.write(chunk)}})在streaming模式下,数据会一直流,我们必须添加事件并使用它。如果您无法处理数据流,数据就会丢失。我们可以添加数据事件来处理数据。添加数据事件会自动将可读流的模式从暂停模式切换到流模式。如果需要在两种模式之间手动切换,可以使用resume()(从暂停模式恢复)和pause()(进入暂停模式)方法。(如果监听到可读流的可读事件,则resume()方法无效。)在下面的例子中,每通过可写流写入一位数据,可读流就会暂停1秒,并且写入将在1秒后继续。constfs=require('fs')constreadable=fs.createReadStream('./datasource.json')constwritable=fs.createWriteStream('./target.json')//自动切换到流模式readable.on('data',(chunk)=>{console.log('write')writable.write(chunk)readable.pause()console.log('pause')//暂停1s后,再次切换到flow模式setTimeout(()=>{readable.resume()},1000)})下图是两种模式的切换(图片来自SamerBuna的在线课程)。添加数据事件时(单独添加数据事件,没有可读事件时),模式会自动切换。Streamimplementation创建一个自定义的可写流要创建一个自定义的可写流,我们需要继承stream.Writable类。并在子类中实现_write方法。const{Writable}=require('stream')classCustomWritableextendsWritable{/***@paramchunk待写入数据*@paramencoding编码格式*@paramnext处理完成回调*/_write(chunk,encoding,next){try{//Justdoaprintconsole.log(`Justdoaprint:${chunk.toString()}`)next()}catch(error){next(error)}}}流本身不很有道理。这个自定义的可写流只会打印可读流输入的数据。我们可以链接到process.stdin可读流并将输入打印到终端。constcustomWritable=newCustomWritable()process.stdin.pipe(customWritable)创建自定义可读流要创建可读流,我们需要继承stream.Readable类。并在子类中实现_read方法。const{Readable}=require('stream');classCustomReadableextendsReadable{_read(){}}流本身也没有多大意义。我们可以使用push方法将数据添加到流的内部队列中,供流消费。我们可以结合之前自定义的可写流打印出推送数据。constcustomReadable=newCustomReadable()constcustomWritable=newCustomWritable()customReadable.push('我喜欢西尔莎罗南')//通知流不会有任何数据customReadable.push(null)//可读流将数据传递给writablestream//可写流打印数据customReadable.pipe(customWritable)创建自定义转换流要创建可读流,我们需要继承stream.Transform类。并在子类中实现_transform方法。const{Transform}=require('stream')classCustomTransformextendsTransform{_transform(chunk,encoding,next){//我们将可读流的内容转换为大写chunk=chunk.toString().toUpperCase()next(null,chunk)}}此自定义流会将所有可读和流式传输的字符串转换为大写。constcustomReadable=newCustomReadable()constcustomWritable=newCustomWritable()constcustomTransform=newCustomTransform()customReadable.push('abcdefg')customReadable.push(null)customReadable.pipe(customTransform).pipe(customWritable)对象模式节点streams默认通过Buffer或string传输,我们可以开启objectMode开关。启用流传输js的对象。const{Readable,Writable}=require('stream')classCustomReadableextendsReadable{_read(){}}classCustomWritableextendsWritable{_write(chunk,_,next){try{//这里可以打印js对象//chunk可以让js对象console.log(chunk)next()}catch(error){next(error)}}}constcustomReadable=newCustomReadable({objectMode:true//开启对象模式})constcustomWritable=newCustomWritable({objectMode:true//开启对象模式})//我们可以传输对象customReadable.push(['a','b','c','d'])customReadable.push(null)customReadable.pipe(customWritable)参考计算机科学概论(第11版)?Node.jsStreams:你需要知道的一切?你想更好地理解Node.js中的Buffer吗?查看此Node.jsv13.8.0文档