正在加载
请稍等

菜单

红楼飞雪 梦

15526773247

文章

Home nodejs I/O 操作
Home nodejs I/O 操作

nodejs I/O 操作

未分类 by

I/O 是Node 有别于其他框架的核心模块之一。今天将探索Node 中提供非阻塞I/O的API。

1、数据流(stream)

Node 中的许多组件提供了连续输出或可连续处理输入的功能。 为了让这些组件行为一致,stream API 提供了一个抽象的接口。该API 提供了常用的方法,以及数据流具体实现时需要使用的属性。数据流分为可读、可写和可读写。所有的流都是EventEmitter 的实例,也就是说可以主动触发事件。

可读的数据流

可 读的数据流API 是一组方法和事件,提供了数据源在发送时访问数据块的功能。基本上,可读数据流是与触发data 事件相关的。这些事件流就代表了数据的流形式。为了更加可控,数据流还提供了一些功能让你可以配置返回数据的大小和速度。最基本的流如下面这个例子所示, 它从一个文件里把数据分块读取。每当一个新的数据块准备好的时候,它会把数据以变量data 的形式传给回调函数。在这个例子里,我们只是简单地把数据记录到终端。但在实际使用场景中,你可以把数据以流的形式发送到其他地方,或者把它积攒起来,然 后再一并处理。其实,data 事件只提供了访问数据的方法,你需要想出如何处理每次返回的数据块。

创建可读文件流

var fs = require('fs');
var filehandle = fs.readFile('data.txt', function (err, data) {
  console.log(data)
});

让我们进一步看看一种处理数据流的常用模 式。有时候我们需要等待完整的数据都可用后再进行操作,在这种情况下就会用到数据池模式(spooling pattern)。我们知道重点是不要让Node 的事件循环阻塞,所以即使不想在接收到所有数据之前进行下一步处理,也不希望堵塞事件循环。在这种情况下,我们使用数据流来读取数据,但只有在接收到足够 的内容后才使用这些数据。通常“足够”的意思是指数据流已经结束,当然也可能是其他条件。

使用缓冲池模型来读取完整的流数据

//stream 是个抽象的数据流
var spool = '';
stream.on('data', function (data) {
  spool += data;
});
stream.on('end', function () {
  console.log(spool);
});

2.文件系统

文 件系统模块显然非常有用,因为你需要它来访问磁盘上的文件。它几乎模仿了文件I/O 的POSIX 风格。这个独特的模块为它所有的功能都提供了异步和同步的方法。但是,我们强烈建议你使用异步的方法,除非你是用Node 来创建命令行脚本。即使这样,通常使用异步版本也会更好,虽然会增加一点点代码,但你可以并行访问多个文件,并缩减脚本运行的时间。人们在处理异步调用时 遇到的主要问题是执行次序具有不确定性,特别是处理文件I/O 时。人们常常会想要同时进行一些操作,如文件移动、重命名、复制、读写等,但是其中一些操作依赖于另一些操作,所以当执行完成的次序不能确定时,可能会出 现问题。这意味着代码中先执行的操作,有可能会在第二个操作之后才完成。有些模式能够很容易地解决次序问题,考虑一下下面例中的情况,读取文件然后把它删 除掉。如果删除(unlink)发生在读取之前,就不可能读取到文件的内容了。

异步读取并删除文件——但这是错误的

var fs = require('fs');
fs.readFile('warandpeace.txt', function (e, data) {
  console.log('War and Peace: ' + data);
});
fs.unlink('warandpeace.txt');

需要注意的是,我们使用了异步方法,并且还 创建了回调函数,但是并没有写任何代码来指定这些函数调用的次序。这对于不熟悉使用事件循环来编程的程序员来说,通常会导致一些问题。这个代码表面上看起 来没问题,但运行起来有时候能正常工作,有时候却不能。需要使用一种模式,让我们可以指定想要运行的次序。有几种方法可实现这一点,其中一种常用的方法是 采用回调函数嵌套。在下例中,删除文件的异步调用是嵌入在异步读取文件的回调函数中的。

通过嵌入回调函数完成异步读取并删除文件

var fs = require('fs');
fs.readFile('warandpeace.txt', function (e, data) {
  console.log('War and Peace: ' + data);
  fs.unlink('warandpeace.txt');
});

这个方法通常能够有效地把一组操作分离开。我们的例子中只有两个操作,很容易就能读懂理解。但这个模式有时候也可能会失去控制。

3、Buffer

虽 然Node 也是使用JavaScript,但它是在JavaScript 通常使用的环境外运行的。比如,浏览器需要JavaScript 来进行许多操作,但并不包括处理二进制数据。虽然说JavaScript 支持字节位操作,但它并没有二进制数据的原生表现形式。当你考虑到JavaScript 里数字类型系统的限制时,更是会头疼不已,最后会变成只好采用二进制形式。Node 带来了Buffer 类,为你操作二进制数据弥补了短板。Buffer 是V8 引擎上的扩展,这意味着它有其固有的一些限制。Buffer 实际上是对内存的直接分配,这意味着这多少受制于你在低级计算机语言方面的经验。JavaScript 的其他数据类型都把存储数据的复杂性进行了抽象,而Buffer 与它们不同,它提供的是内存的直接操作。创建了一个Buffer 后,它的大小就固定了。如果你需要添加更多的数据,就必须把老的Buffer 复制到一个更大的Buffer 中。虽然有些特性看起来让人沮丧,但它们让Buffer 能够在服务器上快速地处理大量的数据操作。这是一个特意的设计选择,为了性能而牺牲了一些程序员的开发便利。

(1). 二进制的快速入门
我 们觉得有必要在这里插入使用二进制数据的快速入门内容,一方面是为了那些并没有太多二进制数据处理经验的读者,另一方面,对于那些很久没处理二进制数据的 读者来说,正好也可以回顾一下(就像我们开始使用Node 时的情况一样)。大多数人都知道,计算机的工作原理是操作“开”和“关”状态。因为只有这样两种状态,所以我们称此为二元状态。计算机的所有东西都建立在 此基础上,这就说明了为什么在计算机上操作时,直接操作二进制通常是最快的方法。要做更复杂的事情时,我们把比特(bit,每一位表示一个二元状态)集合 成8 个一组,称之为8 位字节(octet),也就是通常所说的字节(byte)。3 这样我们就能表示除了0、1 外的其他数字了。利用8 位字节,我们就可以表示从0 到255 间的所有数字了。最右边的位表示1,然后每向左移动一位,把它的表示值乘以2。要计算某个字节表示的数字,只要简单地把对应位置上的数加起来就知道了

并没有“标准”的字节大小, 但实际上现在人们使用的通常是8 位字节。因此8 位字节(octet)和字
节(byte)是等价的,我们会采用更常见的术语字节(byte)来指代特定的8 位字节。

在一个字节里表示0 到255

128 64 32 16 8 4 2 1
--- -- -- -- - - - -
0 0 0 0 0 0 0 0 = 0
128 64 32 16 8 4 2 1
--- -- -- -- - - - -
1 1 1 1 1 1 1 1 = 255
128 64 32 16 8 4 2 1
--- -- -- -- - - - -
1 0 0 1 0 1 0 1 = 149

你 还将接触到许多采用十六进制表示法(hex)的地方。因为字节需要用简单的方式来描述,但8 个0 和1 组成的字符串并不够方便, 所以十六进制表示法便流行起来。二进制表示法的基数是2,因此每个数字(0 或1)只有两种状态。十六进制使用的基数是16,每一位能够表示0 到F 种状态,其中字母A 到F(或对应的小写字母)对应代表10 到15。十六进制非常方便的地方在于,我们只需要2 个字母就能表示整个字节了。最右的位表示1,往左一位就表示16。如果我们要表示数字149,就等价于(16×9)+(5×1),也就是十六进制的95。

用十六进制表示0 到255

十六进制转换到十进制:
0 1 2 3 4 5 6 7 8 9 A B C D E F
- - - - - - - - - - -- -- -- -- -- --
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
十六进制法计数:
16 1
-- -
0 0 = 0
16 1
-- -
F F = 255
16 1
-- -
9 5 = 149

在JavaScript 中,用一个十六进制值表示数字时,需要在其十六进制数值前添加0x 标记。比如,0x95 表示十进制数字149。在Node 里,你会常常看见console.log() 输出,或是在Node 命令行中,Buffer 值是采用十六进制表示的。下例演示了如何用Buffer 保存3 个字符(比如RGB 颜色值)。

用8 位字节数组创建一个3 个字节的Buffer

> new Buffer([255,0,149]);
<Buffer ff 00 95>
>

那么二进制如何表示其他类型的数据 呢?前面我们已经看见如何用二进制来表示数字了。在网络协议中,通常会指定一些字符来传达信息,比如用固定位置上的比特来表示特殊的含义。举个例子,在 DNS 请求中,头两个字节表示的数字是事务ID,下一个字节的每个比特都是独立使用的,每一位表示了在这个请求中是否使用DNS的某个功能。二进制另外一种非常 常用的情况是用来表示字符串。其中使用最多的字符串编码方式是ASCII 和UTF(通常是UTF-8)。这些编码方式定义了如何把比特转换成字符。我们不会在此深入地展开讨论,但基本上,编码的工作原理就是采用一个查找表把字 符映射到对应的数字上。要把编码后的内容转换回来,计算机只要通过查找转换表,就能把数字变成字符。ASCII 字符(其中包含非可见字符,如回车)一定是正好每个都是7 位大小的,因此能表示0 到127 之间的数值。字符中的第8 位比特通常用来扩展字符集,表示各种国际化的字符(如ȳ 或ȱ)。UTF 则要复杂一些。它的字符集包含了非常多的字符,包括许多国际化字符。每个UTF-8 字符需要至少1 个字节,最多的时候需要4 个字节才能表示。实质上,头128个值是传统的ASCII,其他的值被推到了映射表的更远的地方,通过更大的数字来表示。当一个罕见的字符被引用时,通过 第一个字节表示的数字,会告诉计算机利用下一个字节从映射表的第二页中查找字符的实际址。如果该字符不在映射表的第二页,第二个字节会告诉计算机去查找第 三页,以此类推。这意味着在UTF-8 中,字符串对
应的长度并不是与字节数的长度一样。而ASCII 中,这两个长度是永远一致的。
(2). 二进制与字符串

需 要重点记住的是,一旦你把内容复制到一个Buffer 后,它就会以二进制的形式存储起来。当然,你可以随时把Buffer 中的二进制内容转换成其他形式,比如说字符串。所以Buffer 只由它的大小来定义,而非通过编码或者其他任何指示含义的方式。既然Buffer 是不透明的,那么它需要多大才能把输入的特定字符串保存起来呢?正如前面我们介绍的,一个UTF 字符可能会占用最多4 个字节。因此为安全起见,需要定义一个Buffer 的大小为可能输入的UFT 字符最大值的4 倍大小。你可以采取一些方法来减轻这个负担,比如限制输入只能为欧洲语言,这样就能确定每个字符最大为2 个字节了。

(3). Buffer 的使用

创建Buffer 可以使用3 种参数:指定Buffer 的字节长度,需要拷贝到Buffer 里的字节数组,或是需要拷贝到Buffer 里的字符串。第一和最后一种方法是目前最常使用的。在一些不常见的情况下,你会需要用一个JavaScript 的字节数组。

其中一个原因是这样非常浪费内存。比如,若把每个字节作为一个数字保存,你需要使用64 位大小
的内存空间来表示一个8 位大小的内容。

创建特定大小的Buffer 是很常见的情况,而且容易处理。你只需在创建Buffer的时候指定需要的字节大小作为参数就可以了

指定字节长度创建Buffer

> new Buffer(10);
<Buffer e1 43 17 05 01 00 00 00 41 90>
>

正如你在前一个例子中所见,创建 Buffer 后,得到了一个对应长度的字节组。但是,因为Buffer 是从内存直接分配的,它并不会对原有的内容进行初始化,所以得到的内容就是原本占用的东西。这与原生的JavaScript 类型不同,它们会把所有的内存初始化,无论你是创建一个新的原生变量还是对象,它都不会把原本内存空间的垃圾数据返回给你。你可以用下面的情景来帮助理 解。假设你到了一家繁忙的咖啡店,想找一张桌子时,最快的方法就是一旦有人离开了就立马坐下来。虽然这样很快,但是你会面对之前客人留下的脏盘子和剩菜。 你也许希望等待服务员清理桌子后再坐下。这与Buffer 和原生类型的工作方式很像。Buffer 并不会为了让你更方便而做额外的工作,但它们能让你直接快速地操作内存。如果你想要一组漂亮的全是0 的比特组,就需要自己动手(或是找找其他工具库)。当你在处理网络传输协议之类的工作时,因为它们有着定义好的格式,所以创建指定字节长度的Buffer 就很常用了。当你准确地知道数据的大小(或者是知道最大会是多少),并为了性能原因想分配并重用Buffer 时,这就是很好的选择。也许创建Buffer 最常用的方法就是使用ASCII 或UTF-8 字符串了。虽然Buffer可以存储任何数据,但在处理I/O 的字符数据时Buffer 特别有用,因为Buffer 本身的一些限制使得它的操作比一般的字符串操作要快很多。所以当你在创建高度可
扩展的应用时,通常值得采用Buffer 来保存字符串,特别是当你只是在应用间分流字符串,而不会修改它们的时候。因此,即使JavaScript 原生存在了字符串类型,在Node 程序中还是会经常使用Buffer 来保存字符串。如下面所示的例子,我们用字符串来创建Buffer,它默认是UTF-8 编码的。如果你没有指定编码格式,它就会认为是UTF-8 字符串。这并不意味着Buffer 会把字符串补全成能够存下任意Unicode 字符的大小(盲目地为每个字符分配4 个字节),而是说明它不会截断字符内容。在这个例子中,我们看到当输入的字符串是小写字母时,无论采用的是哪种编码方式,Buffer 都使用同样的字节结构,因为每个字母都落在同样的区间里。但是,当我们输入“é”字符时,无论是默认的UTF-8还是我们显式指定为UTF-8,它都被编 码成2 个字节大小。但是当我们指定编码为ASCII 时,字符被截断成单个字节。

用字符串创建Buffer

> new Buffer('foobarbaz');
<Buffer 66 6f 6f 62 61 72 62 61 7a>
> new Buffer('foobarbaz', 'ascii');
<Buffer 66 6f 6f 62 61 72 62 61 7a>
> new Buffer('foobarbaz', 'utf8');
<Buffer 66 6f 6f 62 61 72 62 61 7a>
> new Buffer('é');
<Buffer c3 a9>
> new Buffer('é', 'utf8');
<Buffer c3 a9>
> new Buffer('é', 'ascii');
<Buffer e9>
>

4. 字符串的使用

Node 提供了一些操作来简化字符串和Buffer 操作。首先, 你不需要在创建Buffer 前提前计算字符串的长度,只要把字符串作为参数传给创建Buffer 的函数就可以了。或者,你也可以使用Buffer.byteLength() 方法来获得字符串在编码
上 的字节长度,而不是String.length 返回的字符个数。你还可以往已经存在的Buffer 上写入字符串。Buffer.write() 会把字符串写到Buffer 指定的位置上。如果从Buffer 指定位置开始有足够空间的话,整个字符串都会被写入。否则,字符串的尾部会被截断,好让其大小能放入Buffer。在这两种情况 下,Buffer.write() 都会返回一个数字,表示有多少字节被成功写入。对于UTF-8 字符串来说,如果一个完整字符无法写入到Buffer 的话,就不会单独写入该字符的某个字节。如在下例中,因为Buffer 太小了,以至于无法写入一个非ASCII 字符,所以它就是空的。

Buffer.write() 及部分字符

> var b = new Buffer(1);
> b
<Buffer 00>
> b.write('a');
1
> b
<Buffer 61>
> b.write('é');
0
> b
<Buffer 61>
>

在只有一个字节的Buffer 中,它可以写入一个“a”字符,所以操作返回了1,表示写入了1 个字节。但是,尝试写入一个“é”字符的时候,它需要2 个字节,因此操作返回的是0,因为没有写入任何东西。使用Buffer.write() 还有些复杂的情况。如果条件允许, 当写入UTF-8 时,Buffer.write() 写入的字符串会以一个NULL 字符结尾5。这在一个较大的Buffer中写入时能够看得更明显。

通常这只是意味着是一个二进制的0。

在下例中,创建了一个5 个字节长的Buffer(这是通过传入字符串直接完成
的)。 我们用字母f 把Buffer 整个写满,f 的字符编码是0x66(十进制是102)。这是为了让我们能够看清楚,在Buffer 位移为1 的地方写入字符“ab”会有什么效果。第0 位的字符依然是f。在位置1 和2 上,字符被改为了61 和62。然后Buffer.write() 插入了一个结束符,正如例子中的一个空字符0x00。

写入Buffer 的字符串包含了结束符

> var b = new Buffer(5);
> b.write('fffff');
5
> b
<Buffer 66 66 66 66 66>
> b.write('ab', 1);
2
> b
<Buffer 66 61 62 00 66>
>

4、console.log

这个简单的console.log 命令借用了Firefox 中Firebug 调试器的概念,让你可以轻松把输出打印到标准输(stdout),而不需要借助任何模块。它还提供了美化打印格式的功能来帮助遍历对象

用console.log 输出

> foo = {};
{}
> foo.bar = function() {1+1};
[Function]
> console.log(foo);
{ bar: [Function] }
>

 

25 2015-06

 

我要 分享

 

 

本文 作者

 

相关 文章