理解二进制数据二进制是一种在计算技术中广泛使用的数字系统。二进制数据是用0和1两位数字表示的数,它的基数是2,进位规则是“二得一”,借位规则是“借一为二”,由德国人莱布尼茨发现18世纪的数学哲学家。——百度百科二进制数据就像上图一样,用0和1来存储数据。将普通十进制数转换成二进制数,采用“除以2取余,倒序排列”的方法。将十进制整数除以2得到商和余数;然后将商除以2得到商和余数,以此类推,直到商小于1,然后先得到的余数作为二进制数的低位有效位,后面得到的余数用作二进制数的高位有效位,它们按顺序排列。例如,数字10转换成二进制就是1010,那么数字10在计算机中就是以1010的形式存储的。字母和一些符号需要用ASCII码来对应。例如字母a对应的ACSII编码为97,二进制表示为01100001。在JavaScript中,可以使用charCodeAt方法获取字符对应的ASCII:除了ASCII,还有一些其他的编码映射不同字符的方法。比如我们使用的汉字可以通过JavaScript的charCodeAt方法获取其UTF-16编码。节点处理二进制数据。JavaScript在诞生初期主要是用来处理表单信息的,所以JavaScript自然擅长处理字符串。可以看到String的原型提供了很多方便的字符串操作方法。但是仅仅在服务器端对字符进行操作是远远不够的,尤其是对于网络和文件的一些IO操作,还需要支持对二进制数据流的操作,Node.js的Buffer的存在就是为了支持这些。幸运的是,ES6发布后,引入了类型化数组(TypedArray)的概念,逐渐加入了处理二进制数据的能力。现在在Node.js中也可以直接使用,但是在Node.js中,Buffer更适合二进制数据处理,性能更好,当然Buffer也可以直接看成TypedArray中的Uint8Array。除了Buffer,Node.js还提供了stream接口,主要用于处理大文件的IO操作,相对于批量、分片处理文件。理解BufferBuffer直译成中文就是“缓冲区”的意思。顾名思义,Node.js中实例化的Buffer也是专门用来存储二进制数据的缓冲区。Buffer可以理解为一块开辟的内存区域,Buffer的大小就是开辟的内存区域的大小。我们先看一下Buffer的基本用法。API介绍早期的Buffer是通过构造函数创建的,不同的Buffer是通过不同的参数来分配的。newBuffer(size)创建一个大小为size(number)的缓冲区。newBuffer(5)//newBuffer(array)使用八位数组array分配一个新的Buffer。constbuf=newBuffer([0x74,0x65,0x73,0x74])////对应ASCII码,这些十六进制数对应test//将Buffer实例转为字符串Get以下结果buf.toString()//'test'newBuffer(buffer)将缓冲区数据复制到新创建的Buffer实例中。constbuf1=newBuffer('test')constbuf2=newBuffer(buf1)newBuffer(string[,encoding])创建一个内容为string的Buffer,指定编码方式为encoding。constbuf=newBuffer('test')////可以看到结果和newBuffer([0x74,0x65,0x73,0x74])一致buf.toString()//'test'更安全的Buffer由于Buffer实例根据第一个参数的类型执行不同的结果,如果开发者不对参数进行验证,很容易造成一些安全问题。比如我想创建一个内容为字符串“20”的Buffer,但是传入了错误的数字20,结果创建了一个长度为20的Buffer实例。从上图可以看出,在Node.js8之前,为了高性能,Buffer开辟的内存空间并没有释放已有的数据,直接返回这个Buffer可能会导致敏感信息的泄露。因此Buffer类在Node.js8前后进行了较大的调整,不再推荐使用Buffer构造器实例Buffer,而是使用Buffer.from()、Buffer.alloc()和Buffer.allocUnsafe()而不是newBuffer()。Buffer.from()这个方法用来替换newBuffer(string),newBuffer(array),newBuffer(buffer)。Buffer.alloc(size[,fill[,encoding]])该方法用于替换newBuffer(size),其创建的Buffer实例默认会用0填充内存,即覆盖所有内存之前的数据,比如之前的newBuffer(size)比较安全,因为它需要覆盖之前的内存空间,这也意味着性能较低。此外,如果size参数不是数字,则会抛出TypeError。Buffer.allocUnsafe(size)这个方法和之前的newBuffer(size)是一致的。这种方式虽然不安全,但是相对于alloc有明显的性能优势。Buffer的编码前面介绍过,二进制数据和字符的对应需要指定一种编码。类似地,将字符串转换为Buffer或将Buffer转换为字符串需要指定编码。Node.js目前支持以下编码方式:hex:将每个字节编码成两个十六进制字符。ascii:仅适用于7位ASCII数据。这种编码速度很快,如果设置的话会去掉高位。utf8:Unicode字符的多字节编码。许多网页和其他文档格式都使用UTF-8。utf16le:2或4个字节,little-endian编码的Unicode字符。ucs2:utf16le的别名。base64:Base64编码。latin1:一种将Buffer编码为单字节编码字符串的方法。binary:latin1的别名。比较常用的有UTF-8、UTF-16、ASCII。前面提到,JavaScript的charCodeAt使用的是UTF-16编码,或者说JavaScript中的字符串都是用UTF-16存储的,而Buffer默认的编码是UTF-8。可以看出在UTF-8下一个汉字需要占用3个字节,而UTF-16下只需要2个字节。主要原因是UTF-8是变长字符编码。大多数字符使用1个字节以节省空间,一些超过1个字节的字符需要使用2个或3个字节。这意味着大多数汉字在UTF-8中需要使用3个字节来表示。UTF-16使用2个字节来表示所有字符。后面超过2个字节的字符,需要4个字节来表示。2个字节表示的UTF-16编码和Unicode是完全一样的,通过汉字Unicode编码表可以找到大部分汉字对应的Unicode编码。前面提到的“汉”用Unicode表示为6C49。这里所说的Unicode编码也称为Unicode、Unicode、Unicode。它为每种语言设置了统一唯一的二进制编码,上面提到的UTF-8和UTF-16都是他的。一种方法。关于编码的更多细节不再赘述,也不是本文的重点。如果想了解更多,可以自行搜索。乱码的原因我们经常会遇到一些乱码,这是由于字符串和Buffers在转换过程中使用了不同的编码造成的。我们先新建一个文本文件,然后用utf16编码保存,然后通过Node.js读取修改后的文件。constfs=require('fs')constbuffer=fs.readFileSync('./1.txt')console.log(buffer.toString())因为Buffer在调用toString方法时默认使用utf8编码,所以乱码都是输出,这里我们把toString的编码方式改成utf16就可以正常输出了。constfs=require('fs')constbuffer=fs.readFileSync('./1.txt')console.log(buffer.toString('utf16le'))识别Stream之前我们说过,可以在Node中使用.js使用Buffer来存储一段二进制数据,但是如果数据量非常大,使用Buffer会消耗相当大的内存。这时候就需要在Node.js中使用Stream了。要了解流,您必须了解管道的概念。在类Unix操作系统(以及其他一些借鉴这种设计的操作系统,例如Windows)中,管道是一系列连接标准输入和输出的进程,其中每个进程的输出直接用作下一个进程的输入过程。该概念由DouglasMcIlroy为Unix命令行发明,因其与物理管道的相似性而得名。--摘自维基百科我们经常在Linux命令行上使用管道将一个命令的结果传输到另一个命令,例如搜索文件。ls|grepcode这里用ls列出当前目录下的文件,然后让grep查找包含code关键字的文件。前端构建工具gulp也用到了pipeline的概念,因为用pipeline来构建,大大简化了工作流程,使用人数一下子超过了grunt。//用gulp编译scssconstgulp=require('gulp')constsass=require('gulp-sass')constcsso=require('gulp-csso')gulp.task('sass',function(){returngulp.src('./**/*.scss').pipe(sass())//将scss转换为css.pipe(csso())//压缩css.pipe(gulp.dest('./css'))})前面说了那么多pipeline,pipeline和flow应该怎么直连。流可以理解为水流。水流向何处由管道决定。如果没有管道,水就不能形成水流,所以水流必须依附在管道上。Node.js中所有的IO操作都可以通过流来完成,因为IO操作的本质就是从一个地方流到另一个地方。例如,网络请求是将数据从服务器流向客户端。constfs=require('fs')consthttp=require('http')constserver=http.createServer((request,response)=>{//创建数据流conststream=fs.createReadStream('./data.json')//将数据流管道化到响应流stream.pipe(response)})server.listen(8100)//data.json{"name":"data"}将在使用Stream时读取数据。json是将数据写入到responsestream中,而不是像Buffer一样将整个data.json读入内存,然后一次性输出到response中,所以在使用Stream的时候,会更加节省内存。实际上,Stream内部仍然对Buffer进行操作。如果我们把一段二进制数据比作一桶水,那么通过Buffer传输文件就是将一桶水直接倒入另一个桶中,而使用Stream就是通过管道一点一点地提取桶中的水。Stream和Buffer的内存消耗对比如果只是口头说说可能不是很明显。现在分别通过Stream和Buffer复制一个2G的文件,看看node进程的内存消耗情况。流复制文件//流复制文件constfs=require('fs');constfile='./file.mp4';fs.createReadStream(file).pipe(fs.createWriteStream('./file.copy.mp4')).on('finish',()=>{console.log('文件复制成功');})缓冲复制文件//缓冲复制文件constfs=require('fs');constfile='./file.mp4';//fs.readFile直接输出文件Bufferfs.readFile(file,(err,buffer)=>{fs.writeFile('./file.copy.mp4',buffer,(err)=>{console.log('文件复制成功');});});从上图的结果可以看出,通过Stream复制时,只占用了我电脑内存的0.6%,而使用Buffer时,占用了15.3%的内存。API介绍在Node.js中,Steam分为五种类型。可读流(Readable),可以读取数据的流;可写流(Writable),可以写入数据的流;双工流(Duplex),可读可写的流;转换流(Transform),在读写过程中,可以对数据进行任意修改和转换的流(也是可以读写的流);所有流都可以通过.pipe进行消费,即管道(类似于linux中的|)。此外,事件也可用于监控数据流。无论是读写文件,还是http请求和响应,都会在内部自动创建一个Stream。读取文件时,会创建可读流,输出文件时,会创建可写流。可读流(Readable)虽然称为可读流,但可读流也是可写的,但是写操作一般在内部进行,外部只需要读。可读流一般分为两种模式:流模式:表示正在读取数据,流中的数据一般是通过事件监听获取的。暂停模式:此时不会消费流中的数据。如果需要在暂停模式下读取可读流的数据,需要显式调用stram.read()。创建可读流时,默认情况下它处于暂停模式。一旦.pipe被调用或者数据事件被监听,它会自动切换到流模式。const{Readable}=require('stream')//创建一个可读流constreadable=newReadable()//绑定数据事件,改变模式为流模式readable.on('data',chunk=>{console.log('chunk:',chunk.toString())//输出chunk})//写5个字母for(leti=97;i<102;i++){conststr=String.fromCharCode(i);readable.push(str)}//推送`null`表明流已经结束readable.push(null)const{Readable}=require('stream')//创建一个可读流constreadable=newReadable()//写5个字母for(leti=97;i<102;i++){conststr=String.fromCharCode(i);readable.push(str)}//推送`null`表示流已结束可读。push('\n')readable.push(null)//输出流数据,通过管道向控制台写入数据。前面说过,Node.js中数据的写入是在内部实现的。下面的例子读取通过读取文件的fs创建的可读流:constfs=require('fs')//创建data.json文件可读流constread=fs.createReadStream('./data.json')//监听数据事件,此时变成流模式read.on('data',json=>{console.log('json:',json.toString())})####可写流(Writable)相对于readablestream,writablestream真的只是可写的,属于只能进出的类型,类似于貔貅。创建可写流时,必须手动实现一个_write()方法,因为前缀有下划线的表示这是一个内部方法,用户一般不会直接实现,所以在Node.js内部定义了这个方法。例如,文件可以是Writestream,在此方法中将传入的Buffer写入指定的文本。如果写入结束,一般需要调用可写流的.end()方法来表示本次写入结束,此时也会调用finish事件。const{Writable}=require('stream')//创建一个可写流constwritable=newWritable()//绑定_write方法将写入的数据输出到控制台writable._write=function(chunk){console.log(chunk.toString())}//写入数据writable.write('abc')//结束写入writable.end()_write方法也可以在实例可写时传入对象的write属性来实现。const{Writable}=require('stream')//创建一个可写流constwritable=newWritable({//同上,bind_write方法write(chunk){console.log(chunk.toString())}})//写入数据writable.write('abc')//写入结束writable.end()我们来看看Node.js内部通过fs创建的可写流。constfs=require('fs')//创建可写流constwritable=fs.createWriteStream('./data.json')//写入数据,与你手动创建的可写流一致writable.write(`{"name":"data"}`)//结束写入到writable.end()可以理解为Node.js响应http时,需要调用.end()方法结束响应。其实内部就是一个可写流。现在通过Stream复制文件的代码就更容易理解了。constfs=require('fs');常量文件='./file.mp4';fs.createReadStream(file).pipe(fs.createWriteStream('./file.copy.mp4')).on('finish',()=>{console.log('filesuccessfulcopy');})双工stream(Duplex)Duplexstream同时实现了Readable和Writable,具体用法可以参考readablestream和writablestream,这里不占用文章篇幅。PipelineConcatenation前面介绍过,一个桶中的数据可以通过管道(.pipe())传输到另一个桶中,但是当有多个桶时,我们需要多次调用.pipe()。比如我们有一个文件需要gzip压缩后重新导出。constfs=require('fs')constzlib=require('zlib')constgzip=zlib.createGzip()//gzip是双工流,可读可写constinput=fs.createReadStream('./data.json')constoutput=fs.createWriteStream('./data.json.gz')input.pipe(gzip)//文件压缩gzip.pipe(output)//压缩输出就面临这种情况,Node.js提供了pipeline()api,可以一次完成多个pipeline操作,还支持错误处理。const{pipeline}=require('stream')constfs=require('fs')constzlib=require('zlib')constgzip=zlib.createGzip()constinput=fs.createReadStream('./data.json')constoutput=fs.createWriteStream('./data.json.gz')pipeline(input,//输入gzip,//压缩输出,//output//最后一个参数是错误捕获的回调函数(err)=>{if(err){console.error('压缩失败',err)}else{console.log('压缩成功')}})参考字符编码注释Buffer|Node.jsAPI流|Node.jsAPIstream手册