爱妃科技nodejs 进程 | | 爱妃科技
正在加载
请稍等

菜单

红楼飞雪 梦

15526773247

文章

Home nodejs nodejs 进程
Home nodejs nodejs 进程

nodejs 进程

nodejs by

虽然Node 把许多东西从操作系统中抽象出来,但你依然在操作系统里运行,而且可能想要更直接地与它交互。Node 中可以使用系统中已经存在的进程,或者创建新的子进程来做各种工作。虽然Node 本身是一个“胖”线程,带有单独一个事件循环,但你可以任意地开启其他进程(线程)在事件循环外工作。

1、process模块

可以使用process 模块从当前的Node 进程中获得信息,并可以修改配置。和其他大部分模块不同,process 模块是全局的,并且可以一直通过变量process 获得。

(1)process 事件

process 是EventEmitter 的实例,所以它提供了基于对Node 进程的系统调用的事件。exit 事件提供了在Node 进程退出前的最终响应时机。重要的是,事件循环在exit 事件之后就不会再运行了,因此只有那些不需要回调函数的代码才会被执行。

在Node 退出前调用代码

process.on('exit', function () {
  setTimeout(function () {
    console.log('This will not run');
  }, 100);
  console.log('Bye.');
});

因为事件循环不会再运行,因 此setTimeout() 里的代码永远不会执行。process 提供的一个非常有用的事件是uncaughtException。用过Node 一段时间后,你会发现那些在事件主循环里碰到的异常会导致Node 进程退出。在许多应用场景下,特别是对那些希望永不当机的服务器程序来说,这都是不可接受的。uncaughtException 事件会提供一个极其暴力的方法来捕获这些异常。它确实是最后一道防线了,但对解决此问题非常有效。

通过uncaughtException 事件捕获异常

process.on('uncaughtException', function (err) {
  console.log('Caught exception: ' + err);
});
setTimeout(function () {
  console.log('This will still run.');
}, 500);
// 故意导致异常,并且不捕获它。
nonexistentFunc();
console.log('This will not run.');

让我们来分解一下整个操作过 程。首先,我们为uncaughtException 创建了一个事件监听器。它并非一个智能处理程序,只是简单地把异常输出到标准输出。如果这个Node 脚本是作为服务器运行的话,可以很方便地把标准输出保存到一个文件中,记录下这些错误。但是,因为它捕获的是一个不存在的函数触发的事件,所以虽然 Node 程序不会退出,但是标准的执行流程会被打断。我们知道所有的JavaScript 都会执行一遍,然后任何回调函数都可能在其对应监听的事件触发时被调用到。但在这个例子中,因为nonexistentFunc() 抛出了异常,所以在它之后的代码都不会执行下去。然而,在此之前已经运行的代码会继续下去,也就是说,setTimeout() 依然会被调用。这在你开发服务器程序的时候很重要。让我们再多看些这方面的例子

捕获异常的回调函数的作用

var http = require('http');
var server = http.createServer(function (req, res) {
  res.writeHead(200, {
  });
  res.end('response');
  badLoggingCall('sent response');
  console.log('sent response');
});
process.on('uncaughtException', function (e) {
  console.log(e);
});
server.listen(8080);

这段代码创建了一个简单的HTTP 服务器,然后在进程层次上监听所有的未捕获异常。在HTTP 服务器中,回调函数在发送了HTTP 响应后,故意调用了一个错误的函数。下例展示了这个脚本的终端输出内容。

上述代码段的输出(为了看起来能清晰一些,所以这里对输出内容进行了格式化)

node ex-test.js
{
  stack: [
    Getter / Setter
  ],
  arguments: [
    'badLoggingCall'
  ],
  type: 'not_defined',
  message: [
    Getter / Setter
  ]
}
{
  stack: [
    Getter / Setter
  ],
  arguments: [
    'badLoggingCall'
  ],
  type: 'not_defined',
  message: [
    Getter / Setter
  ]
}
{
  stack: [
    Getter / Setter
  ],
  arguments: [
    'badLoggingCall'
  ],
  type: 'not_defined',
  message: [
    Getter / Setter
  ]
}
{
  stack: [
    Getter / Setter
  ],
  arguments: [
    'badLoggingCall'
  ],
  type: 'not_defined',
  message: [
    Getter / Setter
  ]
}

当启动这个例子的脚本时,服 务器已经准备就绪,然后我们对它发起了几次HTTP请求。注意,服务器并没有关闭退出。相反,错误被绑定在uncaughtException事件上的函 数记录了下来,而且我们依然能够提供完整的HTTP 请求处理服务。这是为什么呢? Node 故意阻止了正在运行的回调函数继续处理和调用下面的console.log()。该错误只会影响我们派生出来的进程,而服务器能够继续运行,所以该异常被 封装在一个特定的代码路径上,其他代码并不会受影响。还要重点理解的是Node 中的监听器是如何实现的,让我们看看下面这个例子:

EventEmitter 的简略监听代码

EventEmitter.prototype.emit = function (type) {
  ...var handler = this._events[type];
  ...
} else if (isArray(handler)) {
  var args = Array.prototype.slice.call(arguments, 1);
  var listeners = handler.slice();
  for (var i = 0, l = listeners.length; i < l; i++) {
    listeners[i].apply(this, args);
  }
  return true;
  ...
};

在事件触发后,运行时处理程 序中的一项检查是看看是否存在事件监听器的数组。如果有几个监听器,运行执行器会按数组顺序把里面的监听器一一调用。意思是说,第一个绑定的监听器会首先 用apply() 方法调用,然后是第二个,以此类推。这里需要重点注意的是,同一个事件的所有监听器是在同一个代码路径上的。所以如果其中一个回调函数出现了异常未被捕 获,将导致该事件的其他回调函数终止执行。但是一个事件实例中的未捕获异常不会影响其他事件。
我们还能利用process 来访问一些系统事件。当进程得到一个信号的时候,它会通过process 触发的事件通知Node 程序。例如操作系统会产生许多POSIX 系统事件,如sigaction(2) 帮助文档中介绍的那些。最常见的有SIGINT、中断信号量。通常,当用户对运行在终端的程序按下Ctrl-C 的时候,SIGINT 就会发生。除非你通过process 来处理信号事件,否则Node 会采取默认方法进行处理。比如说SIGINT 的情况,默认操作就是立刻杀死进程。你可以通过process.on() 方法来修改这些默认行为,除了一些永远无法捕获的信号之外

捕捉Node 进程的信号量

// 开始从标准输入读取内容,所以程序不会退出
process.stdin.resume();
process.on('SIGINT', function () {
  console.log('Got SIGINT. Press Control-D to exit.');
});

为了确保Node 程序不会主动退出,我们从标准输入读取内容,这样Node 进程就会继续运行了。如果你在程序运行的时候按下Ctrl-C,操作系统会发送SIGINT 信号给Node 程序,这会被SIGINT 事件处理器所捕获。在本例中,我们采用把信息记录在终端的方式来代替原本的退出程序操作。

(2)、与当前Node 进程交互

process 包含了有关Node 进程的许多元信息。当你希望在进程内管理Node 运行环境时,这会很有用。这里面包含了关于Node 进程的若干不可改变(只读)的信息,例如:

  • process.version
  • 包含了正在运行的 Node 的版本号:
  • process.installPrefix
  • 也包含了安装时指定的安装目录 (/usr/local、~/local 等 ):
  • process.platform
  • 会列出正在运行的平台名称。输出内容会指明内核(linux2、darwin 等),而
    不是Redhat ES3、Windows 7、OSX 10.7 这一类名称。
  • process.uptime()
  • 还会列出当前进程运行了多少秒。

此 外,你还可以从Node 进程得到或设置一些属性。当进程运行时,它是按某个特定的用户及用户组启动的。你可以调用process.getgid()、 process.setgid()、process.getuid() 和process.setuid() 来获得或修改这些属性。这样做可以有
效 地确保Node 程序运行在一个安全的环境中。还需要注意的是,set 方法除了可以接受用户名/ 用户组所对应的数字ID 外,还可以直接使用用户组/ 用户名本身。但是,如果你传入的是用户组或用户名,该方法会采取堵塞的方式来把这个信息翻译成ID,这样会花费些时间。正在运行的Node 实例的进程ID,或称为PID,可以通过process.pid 属性得到。

你还能修改process.title 属性来设置Node 显示在系统的标题名称,该属性修改后的内容会在ps 命令调用时显示出来。当你在生产环境中需要运行多个Node 进程时,这会很有用。你可以为每个进程修改容易辨别的名称,而不是一堆进程都叫做node(或者node app.js)。当一个进程占用了大量的CPU 或RAM 时,也可以很快地知道具体是谁干的。其他可用的信息包括process.execPath,它显示的是当前执行的node 程序所在的路径,比如/usr/local/bin/node。当前的工作目录(所有打开文件的相对路径)可以用process.cwd() 获取。工作目录是Node 启动的目录。你可以调用process.chdir() 来修改(如果修改的目录不可读或者不存在,将会抛出异常),还可以使用process.memoryUsage() 来得到当前进程的内存使用情况,这会返回一个对象来说明内存使用的各种情况:rss 是RAM 的使用量,而vsize 是内存使用总量,包括了RAM 和swap。你还可以获知V8 的一些状态:heapTotal 和heapUsed 分别表示V8 分配了多少内存,已经有多少内存正在使用。

(3). 操作系统的输入/ 输出

通 过process,还有若干方法可以与操作系统交互(除了修改正在运行的Node 进程以外)。其中一个主要功能就是可以访问操作系统的标准I/O 流,stdin 是进程的默认输入流,stdout 是进程的输出流,stderr 是其错误输出流。它们对应暴露的接口是process.stdin、process.stdout 和process.stderr, 其中process.stdin 是可读的数据流,而process.stdout 和process.stderr 是可写的数据流。

<1>、 process.stdin stdin 在进程间通信时是非常有用的,它能够为命令行下采用管道通信提供便利。当我们输入cat file.txt | node program.js 时,标准输入流会接收到cat 命令输出的数据。因为任何时候都能使用process,所以process.stdin 也会为所有的Node 进程初始化。但它一开始是处于暂停状态,这时候Node 可以对它进行写入操作,但是你不能从它读取内容。在尝试从stdin 读数据之前,需要先调用它的resume() 方法。Node 会为此数据流填入供读取的缓存,并等待你的处理,这样可以
避免数据丢失。

把标准输入写到标准输出

process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function (chunk) {
  process.stdout.write('data: ' + chunk);
});
process.stdin.on('end', function () {
  process.stdout.write('end');
});

我们请求 process.stdin 进行resume() 操作,并把编码设置为UTF-8,然后设置了监听器把接收的数据推送到process.stdout 上去。当process.stdin 发起end 事件时,我们把它也传输给process.stdout 流。因为stdin 和stdout 都是真正的数据流,所以我们也可以采用更简便的方法,那就是使用数据流的pipe() 方法,

通过管道把标准输入转到标准输出

process.stdin.resume();
process.stdin.pipe(process.stdout);

这是连接两个数据流的最漂亮的方式。

<2>、 process.stderr stderr 用来输出异常和程序运行过程中遇到的问题。在POSIX 系统里,因为它是另外一个独立的流,所以输出的日志和错误的日志很容易被记录到不同的目标位置。这也许是可取的,但Node 有自己的一套处理特性。当写入stderr时,Node 将保证该次写入的会被完成。但是,和其他普通的流不一样,这会以堵塞的方式执行。通常情况下,调用Stream.write() 会返回一个布尔值,用来表示Node 是否能够写到内核缓存中去。对于process.stderr 来说这个返回值永远是真,但它可能不会像一般的write() 那样立刻返回,而是需要等待一会儿。一般来说,这是非常快的,但内核缓存有的时候可能满了,这就会导致你的程序挂起等待。因此,在一个生产系统中,我们应 该避免对stderr 写入过多的内容,因为它会堵塞真正需要的工作。还需要注意的是,process.stderr 永远是UTF-8 编码的数据流。不需要设置编码格式,你写入process.stderr 的所有数据都会被当做UTF-8 来处理。而且,你不能更改编码格式。另外,Node 程序员要从操作系统读取的内容还包括了程序启动时的参数。argv 是包含命令行参数的数组,以node 命令为第一个参数

输出argv 的简单脚本

console.log(process.argv);

运行上例所示脚本(为方便查看,已将输出代码格式化)

node argv.js -t 3 -c "abc def" -erf foo.js
[
  'node',
  '/Users/croucher/argv.js',
  '-t',
  '3',
  '-c',
  'abc def',
  '-erf',
  'foo.js'
]
Enki:~ $

这里需要注 意几个问题。第一,process.argv 数组只是简单地把命令行内容以空格作分割得到的。如果两个参数之间包含多个空格,也只会被切分一次。检查空格的方法可以用正则表达式(regex)的写 法\s+,但这不包括引号内的空格,引号可以用来把多个词组合在一起。而且,还要注意第一个文件参数是如何被展开成全路径的。这意味着你可以传给命令行一 个相对路径的文件名作为参数,它会在argv中显示成绝对路径。这对一些特殊字符也同样生效,比如用~ 来表示home 目录,只有第一个参数会被这样展开。argv 在编写命令行脚本的时候相当有用,但又有点太原始了。有几个社区项目对它进行了扩展,可以帮助你轻松编写命令行程序,包括功能自动启用、编写程序内帮助文 档及其他高级功能。

(4). 事件循环和计数器

如 果你之前在浏览器里使用过JavaScript 编程,就应该对setTimeout() 很熟悉了。在Node 里,我们有更加直接的方法来访问事件循环,并且可以推延工作,这些非常有用。process.nextTick() 创建了一个回调函数,它会在下一个tick 或者事件循环下一次迭代时被调用。因为实现是使用队列的,所以它会取代其他事件。让我们在下例中进一步查看。

用process.nextTick() 往事件循环队列里插入回调函数

> var http = require('http');
> var s = http.createServer(function(req, res) {
... res.writeHead(200, {});
... res.end('foo');
... console.log('http response');
... process.nextTick(function(){console.log('tick')});
... });
> s.listen(8000);
>
> http response
tick
http response
tick

这个例子创建了一个 HTTP 服务器。服务端监听请求事件的函数调用process.nextTick() 创建了一个回调函数。无论我们向HTTP 服务器发起多少次请求,tick每次都会出现在事件循环的下一个轮回中。nextTick() 回调函数不像其他回调函数那样是一个单独的事件,因此也不像一般回调函数那样异常脆弱

在其他代码异常之后,nextTick() 继续工作

process.on('uncaughtException', function (e) {
  console.log(e);
});
process.nextTick(function () {
  console.log('tick');
});
process.nextTick(function () {
  iAmAMistake();
  console.log('tock');
});
process.nextTick(function () {
  console.log('tick tock');
});
console.log('End of 1st loop');

上述代码运行结果

Enki:~ $ node process-next-tick.js
End of 1st loop
tick
{
  stack: [
    Getter / Setter
  ],
  arguments: [
    'iAmAMistake'
  ],
  type: 'not_defined',
  message: [
    Getter / Setter
  ]
}
 
tick tock
Enki:~ $

尽管故意制 造了错误,但与其他在单个事件内的回调函数不同,tick 中的每一个函数都被隔离开了。让我们来看一下代码。首先,我们设置了异常监听器来捕获所有的异常。其次,调用process.nextTick() 设置了几个回调函数。每一个回调函数都会输出到终端。但是,第二个函数有一个故意的错误。最后,我们在终端记录了一条消息。当Node 运行这个程序的时候,它先处理了所有的代码,并且包括了输出’End of 1st loop’。然后它按顺序调用了nextTice() 中的回调函数。第一个’tick’ 输出后,我们抛出了异常,因为遇到了下一个tick 中故意安放的错误。这个错误导致进程触发了一个uncaughtException 事件,并使得我们的函数把错误输出到终端上。因为抛出了异常,’tock’ 并没有在终端打印出来,但’ticktock’ 依然打印了,这是因为每次调用nextTick() 的时候,回调函数都是在隔离中创建的。你可能会想到将要被触发的事件是在事件循环当前遍历的内部执行的。

而与其他事件相比,nextTick() 则是在事件循环的遍历开始前被调用的。最后,其他事件在事件循环内按顺序执行。

2、子进程

你 可以使用child_process 模块来为Node 主进程创建子进程。因为Node 的单进程只有一个事件循环,所以有时候创建子进程是很有用的。比如,你可能需要用此方法来更好地利用CPU 的多核,而单个Node 进程只能使用其中一个核。或者说,你可以用child_process 来启动其他程序,然后与其交互。特别是当你在编写命令行脚本的时候,这会非常有用。child_process 有两个主要的方法。spawn() 会创建一个子进程,并且有独立的stdin、stdout 和stderr 文件描述符。exec() 会创建子进程,并会在进程结束的时候以回调函数的方式返回结果。创建子进程的方法有很多种,其中一种依然是非阻塞的方式,而且不需要你写额外的代码来推动 运行。所有的子进程都有一些公共的属性。它们每个都包含了stdin、stdout 和stderr 的特性, 正如我们在上一小节所讨论的那样。此外它们还有一个pid 属性,它包含了该
子进程的OS 进程ID。子进程在退出的时候会触发exit 事件。其他data 事件可以通过child_process.stdinchild_process.stdout 和child_process.stderr 的流方法获得。

(1)、child_process.exec()

让我们用最直观的使用情景来介绍exec() 吧。使用exec(),你可以创建一个子进程来运行其他程序(也可以是另外一个Node 程序),然后在回调函数中返回执行的结果

用exec() 调用ls

var cp = require('child_process');
cp.exec('ls -l', function (e, stdout, stderr) {
  if (!e) {
    console.log(stdout);
    console.log(stderr);
  }
});

当调用exec() 时,可以输入一个命令行指令让新创建的进程去执行。注意整个命令是一个字符串。如果你需要给命令传入参数,也需要将其包含在字符串里。在这个例子中,我们 传给ls 命令-l 参数,用来指定输出格式为详细格式。你还可以使用复杂的命令行功能,比如“|”来实现管道命令。Node 会返回管道中最后一个命令的结果。回调函数接收3 个参数:一个error 对象、stdout 的结果和stderr 的结果。注意调用的ls 命令会运行在Node 程序当前所在的工作目录中,你可以调用process.cwd() 获得这个目录。重要的是要了解第一个和第三个参数的区别。如果子进程返回了错误的状态码或者是有其他异常发生,error 对象就不会是null。当子进程退出时,它会把状态传回给父进程。比如,在Unix 中,0 是表示成功,大于0 的8 位数字则用来表示错误。error 对象也可以用来表示被调用的命令不满足Node 对它的限制。当错误代码从子进程返回时,error 对象会包含错误代码和stderr。但是,若一个子进程运行是成功的,stderr 中依然可以有数据。exec() 的第二个参数可以是一个可选的配置对象。默认情况下,这个对象包含了如下例所示的属性。

child_process.exec() 的默认配置对象

var options = {
  encoding: 'utf8',
  timeout: 0,
  maxBuffer: 200 * 1024,
  killSignal: 'SIGTERM',
  setsid: false,
  cwd: null,
  env: null
};

这些属性如下。

  • encoding
    I/O 流输入字符的编码格式。
  • timeout
    进程运行的时间,以毫秒为单位。
  • killSignal
    当时间或Buffer 大小超过限制时,用来终止进程的信号。
  • maxBuffer
    stdout 或stderr 允许最大的大小,以千字节为单位。
  • setsid
    是否创建Node 子进程的新会话。
  • cwd为子进程初始化工作目录(null 表示使用当前的进程工作目录)。
  • env 进程的环境变量。所有的环境变量都可以从父进程继承。

让我们设置一些选项来给子进程一些限制吧。首先,我们限制响应数据的Buffer大小,

限制child_process.exec() 调用的Buffer 大小

> var child = cp.exec('ls', {maxBuffer:1}, function(e, stdout, stderr) {
... console.log(e);
... }
... );
>{
  stack: [
    Getter / Setter
  ],
  arguments: undefined,
  type: undefined,
  message: 'maxBuffer exceeded.'
}

在本例中,你可以看见我们设 置了一个很小的maxBuffer(只有1kb),所以运行ls 命令很快就耗尽所有的可用空间并且抛出错误。因此检查错误很重要,这让你能够用合理的方法来处理它们。因为你已经限制在child_process 里,所以你不会希望由于访问了不存在的资源而导致真正的异常发生。如果child_process 返回了一个错误,它的stdin 和stdout 属性就不可用了,因此如果再去访问它们将会抛出异常。我们也可以在子进程运行超过一定时间后,把它终止掉,

process.exec() 调用时设置超时

> var child = cp.exec('for i in {1..100000};do echo $i;done',
... {timeout:500, killSignal:'SIGKILL'},
... function(e, stdout, stderr) {
... console.log(e);
... });
> { stack: [Getter/Setter], arguments: undefined, type: undefined, message:
... }

这个例子定义了一个故意长时间运行的进程(在shell 脚本中从1 数到100 000),但我们又设置了一个很短的超时。注意,我们还指定了killSignal。默认的终止信号是SIGTERM,但我们使用SIGKILL 来展示这个功能。

SIGKILL 可以在命令行用kill -9 产生。

得到错误的返回时,注意一下其中的killed 属性,它会告诉我们Node 主动终止了该进程,并且它没有自行退出。这对于前一个例子也同样成立。因为它不是自己主动退出的,所以也没有code 属性及其他关于系统错误的属性。

(2). child_process.spawn()

spawn() 和exec() 很像,但它是一个更加通用的方法。它要求你自己处理流和它们的回调函数。这让它的功能更加强大和灵活,但这也意味着需要编写更多的代码才能达到 exec() 那些一下子就能完成的功能。所以spawn() 最常见的用途是用来在服务器开发中创建服务器程序的子模块,它也是人们使Node 运行在一台机器的多个CPU 核上的最常见方式。虽然其功能和exec() 一样, 但spawn() 的API 还是有些差异的。第一个参数依然是让进程去开始运行的命令,但与exec() 不同,它不再是一个命令字符串,而只是可执行程序。进程的参数以数组的形式作为第二个(可选的)参数传给spawn()。这和process.argv 的反向操作类似:不是把命令按空格分隔开,而是提供一个数组来以空格连接起来(join())。最后,spawn() 还可以接受一个选项数组作为最后一个参数。配置的部分属性与exec() 的相同,我们马上会进一步介绍。

用spawn() 启动子进程

var cp = require('child_process');
var cat = cp.spawn('cat');
cat.stdout.on('data', function (d) {
  console.log(d.toString());
});
cat.on('exit', function () {
  console.log('kthxbai');
});
cat.stdin.write('meow');
cat.stdin.end();

上例的的运行结果

Enki:~ $ node cat.js
meow
kthxbai
Enki:~ $

在上面的例子中,我们使用 了Unix 程序的cat 命令,它会把所有输入的内容都复制一遍并打印出来。你会看到,与exec() 不同,我们没有直接给spawn() 指定回调函数,因为期待使用子进程类提供的流事件来读取并发送数据。我们把子进程的实例命名为“cat”变量,然后就可以通过cat.stdout 来设置子进程stdout 流的事件监听器了。我们为cat.stdout 设置了监听器来监控所有的data 事件,并且对子进程本身设置了exit 事件的监听器。通过其child.stdin 流,就可以接着往子进程的stdin 中发送数据。这只是一个普通的可写数据流,但是,由于cat 程序的行为特点,当我们关闭stdin 的时候,子进程就会退出。这并非对所有程序都有效,但对于cat 程序来说是有效的,因为它的存在只是为了把数据回显。传给spawn() 的配置内容并非和exec() 完全一样,这是因为你需要对spawn()进行更多的手工操作。env、setsid 和cwd 属性都是spawn() 的可选项。还有uid 和gid,分别用来设置用户ID 和组ID。与process 类似,设置uid 或gid来修改用户名或用户组的名称会因为查找用户或用户组而短暂堵塞。spawn() 还比exec() 多一个配置项,你可以设置自定义的文件描述符来传给新建立的子进程。让我们多花点时间在这个话题上吧,毕竟它有点复杂。Unix 系统中的文件描述符是用来记录跟踪该程序正在对哪些文件进行操作的方法。因为Unix 允许多个程序同时运行,所以需要有方法来确保这些程序在修改文件系统时不会不小心把别人的修改覆盖。文件描述符表是用来记录一个进程想要访问的所有文件信 息的,内核可能会为了防止两个程序同时修改一个文件而把某个特定的文件锁住,当然还有其他管理功能。进程会从文件描述符表中查找某个文件对应的文件描述 符,然后传给内核去访问该文件。文件描述符其实只是用一个整数来表示。重要的一点是,文件描述符这个名字有点虚幻,因为它并不是单纯地表示文件。网络或其 他socket 一类的东西也是分配成文件描述符。Unix 的跨进程通信(IPC)socket 可以用来让进程间互相发消息, 我们称它们为stdin、stdout 和stderr。当spawn() 允许我们在创建新的子进程时指定文件描述符时,情况变得有趣起来。这
意 味着,不必由操作系统指派一个新的文件描述符,我们可以要求子进程与父进程一起共享一个已经存在的文件描述符。该文件描述符可以是一个连接在互联网的网络 socket,或者只是父进程的stdin。但重点是,我们有了一个功能强大的方法来把工作分配给子进程了。这是如何做到的呢? 当传递options 对象给spawn() 时, 我们可以指定customFds来把自己拥有的3 个文件描述符传递给子进程,这样进程就不需要创立新的stdin、stdout 和stderr 文件描述符了

把stdin、stdout 和stderr 传给子进程

var cp = require('child_process');
var child = cp.spawn('cat', [
], {
  customFds: [
    0,
    1,
    2
  ]
});

运行上例并通过stdin 用管道传入数据

Enki:~ $ echo "foo"
foo
Enki:~ $ echo "foo" | node
readline.js:80
    tty.setRawMode(true);
^
Error: ENOTTY, Inappropriate ioctl for device
    at new Interface (readline.js:80:9)
    at Object.createInterface (readline.js:38:10)
    at new REPLServer (repl.js:102:16)
    at Object.start (repl.js:218:10)
    at Function.runRepl (node.js:365:26)
    at startup (node.js:61:13)
    at node.js:443:3
Enki:~ $ echo "foo" | cat
foo
Enki:~ $ echo "foo" | node fds.js
foo
Enki:~ $

文件描述符0、1、2 分别代表了stdin、stdout 和stderr。在例子中, 我们创建了一个子进程,并从父进程给它传递stdin、stdout 和stderr。可以在命令行里进行连接测试。echo 命令可以打印出字符串foo。如果直接把它用管道传给node 程序(stdout 到stdin),结果是出错。但是,我们可以把它传递给cat 命令,它会把内容回显出来。同样,如果把内容通过管道传给运行脚本的Node 程序,它也会把内容重复出来。这是因为我们将Node 进程的stdin、stdout 和stderr 都与子进程中的cat 程序绑定在一起了。当Node 主进程从stdin 得到数据的时候,它会传给cat 子进程,并由cat 程序把内容回传给共享的stdout。要注意的一点是,一旦你把Node程序以这种方式连接起来,子进程就丢失了它的child.stdin、 child.stdout 和child.stderr 的文件描述符引用。这是因为一旦把文件描述符传递给子进程,它们就会被复制,并且由内核来处理数据传递。因此,Node 并不是在进程与文件描述符之间(FD),所以你无法对这些数据流添加事件监听器

当传递自定义文件描述符后,尝试访问这些文件描述符流失败

var cp = require('child_process');
var child = cp.spawn('cat', [
], {
  customFds: [
    0,
    1,
    2
  ]
});
child.stdout.on('data', function (d) {
  console.log('data out');
});

测试结果

Enki:~ $ echo "foo" | node fds.js
node.js:134
    throw e; // process.nextTick error, or 'error' event on first tick
foo
    ^
TypeError: Cannot call method 'on' of null
    at Object.<anonymous> (/Users/croucher/fds.js:3:14)
    at Module._compile (module.js:404:26)
    at Object..js (module.js:410:10)
    at Module.load (module.js:336:31)
    at Function._load (module.js:297:12)
    at Array.<anonymous> (module.js:423:10)
    at EventEmitter._tickCallback (node.js:126:26)
Enki:~ $

当指定了自定义的文件描述 符,这些流就被显式地设置为null,并且完全不能从父进程访问了。但在许多情况下这是有价值的,因为比起用Node 的stream.pipe()把数据流连接起来,通过内核来分发要快很多。而且,stdin、stdout 和stderr 并非仅有的几个值得用来连接子进程的文件描述符。一个常见的使用情境是把网络socket 和一组子进程相连接,来利用多核的性能。假设我们在创建一个网站或游戏服务器,或者任何需要处理大量流量的应用。我们有着强大的服务器,上面有一堆处理 器,每个又有2 个或4 个核。假如只是简单地启动Node 进程来运行代码,就只能用上一个核。虽然CPU 通常不是Node 程序的核心因素,但我们还是想尽量接近CPU 的极限。此时我们可以把Node 程序启动
到不同的端口上,然后利用Nginx 或Apache 来进行负载均衡。但是,这样做并不优雅,而且要使用更多的软件。我们也可以让Node 进程启动许多子进程,然后把请求分发给它们。这离理想解决方案已经很接近了,但是这个方法会出现一个单点故障,因为只有一个Node 进程来分发所有的数据,这还不够理想。现在就是传递custom FD 大显身手之时了。用传递主进程stdin、stdout 和stderr 同样的方法,我们可以创建其他socket 并且把它们传给子进程。但因为我们传递的是文件描述符而不是消息,所以内核会负责处理分发。这意味着,即使依然需要有一个主Node 进程,但是它不再需要承载所有的流量负荷了。

4、用assert来测试

assert 是为测试代码提供基础功能的核心库。Node 的断言功能与其他开发语言及环境所提供的功能很类似:允许你为对象或函数调用提出要求,并且在破坏断言的时候发出信息。这些方法都很容易使用,并能为代码 单元测试提供许多便利。Node自己的测试也是用assert 编写的。assert 的许多方法都是成对出现的,一个方法提供了正面的测试,另一个就提供反面的功能。比如例5-37 演示的equal() 和notEqual()。这些方法接受两个参数,第一个是期待的值,第二个是实际的值。

assert 的基本功能

> var assert = require('assert');
> assert.equal(1, true, 'Truthy');
> assert.notEqual(1, true, 'Truthy');
AssertionError: Truthy
    at [object Context]:1:8
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
    at ReadStream._emitKey (tty_posix.js:307:10)
    at ReadStream.onData (tty_posix.js:70:12)
>

这里最明显的就是assert 方法不通过时,会抛出异常。这是测试套件的基本原则。当一个测试套件运行时,它应该只是运行,不会抛出异常。在这种情况下,测试会被认为是成功的。只有几 个断言函数, 如equal() 和notEqual(), 会检查相等(==) 和不相等(!=)操作。这意味着其他的测试只会弱化地检查真值和假值(truthy 和falsy,这是Crockford 给它们起的名称)。简单而言,当测试作为一个布尔值时,假值包含了false、0、空字符串(如””)、null、undefined 和NaN,所有其他值都为真值。一个像”false” 这样的字符串是真值,一个包含”0″ 的字符串也是真值。而equal() 和notEqual() 可以用来比较两个简单对象的值(如字符串、数字)。但你需要仔细检查布尔值,以确保得到想要的结果。strictEqual() 和notStrictEqual() 方法检测两个数值是否相等时会采用===和!==,这样可以确保测试时的true 和false 可分别被作为真和假来对待。下例中的ok() 方法是用来测试一个对象是否为真值的简便方法,它会使用== 来对比测试对象和true 是否一样。

用assert.ok() 测试某个对象是否为真值

> assert.ok('This is a string', 'Strings that are not empty are truthy');
> assert.ok(0, 'Zero is not truthy');
AssertionError: Zero is not truthy
    at [object Context]:1:8
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
    at ReadStream._emitKey (tty_posix.js:307:10)
    at ReadStream.onData (tty_posix.js:70:12)
>

但通常你想要比较的内容并不是简单值,而 是对象。JavaScript 并没有提供某种方法来让对象为自己定义相等运算符。即使它允许这样做,人们通常也不会定义运算符。所以deepEqual() 和notDeepEqual() 方法提供了深入比较两个对象值的方法。这些方法会进行若干测试,而无需太多的细节。如果任何一个检查失败了,测试就会抛出异常。首先检查的是若用简单 的=== 操作来比较,两个值的结果是否相等。接着,检查一下它们的类型是否为Buffer,如果是,则检查它们的长度,然后按字节对比。如果对象的类型按== 运算符不匹配,它们就不可能相等。最后,如果比较的参数是对象类型,会进行更加严格的测试,如比较两个对象的原型、属性数量,然后对每个属性执行 deepEqual() 以进行递归比较。这里需要重点指出,deepEqual() 和notDeepEqual() 是非常有用的,但是代价可能很大。你应该只在需要的时候才使用它们。虽然这些方法都尝试先做最快速的测试,但可能需要花费较长的时间才能找到不一致的地 方。如果你提供的对象更加精确,如用对象的某个属性来代替整个对象,就可以显著提高测试的性能。接下来要介绍的assert 方法是throws() 和doesNotThrow()。这些方法会检查指定的代码块是否会抛出异常。你可以检测指定的异常,或者是任意的异常是否抛出。这些方法都很直观,但有 几个选项需要研究一下。大家很容易忽略这些测试,但处理异常是编写健壮JavaScript 代码的重要组成部分,所以你该使用这些测试来确保写出的代码在正确的地方抛出异常。

要把代码块传给throws() 和doesNotThrow(),需要把它们包含在一个没有参数的函数里。 待测试的异常是可选的,如果没有传入,throws() 会检查是否有异常发生,而doesNotThrow() 会确保不抛出异常。如果指定了错误类型,throws() 会检查该指定的异常,并且只会抛出该类型的异常。如果任意其他的异常抛出来,或者指定的异常没有抛出,测试都不会通过。对于 doesNotThrow(),当指定了一个错误,如果有指定异常之外的任何异常抛出时,它都会继续运行。而如果指定的异常出现了,测试就会中断。

用assert.throws() 和assert.doesNotThrow() 检查异常处理

> assert.throws(
... function() {
... throw new Error("Seven Fingers. Ten is too mainstream.");
... });
> assert.doesNotThrow(
... function() {
... throw new Error("I lived in the ocean way before Nemo");
... });
AssertionError: "Got unwanted exception (Error).."
    at Object._throws (assert.js:281:5)
    at Object.doesNotThrow (assert.js:299:11)
    at [object Context]:1:8
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
>

有4 种方法可用来指定要查看或避免的错误类型,我们可以传入以下类型。

  • 比较函数
    该函数只接收一个参数,即异常错误对象。在函数里比较传入的异常是否与你想
    查找的类型匹配,如果匹配则返回真,否则返回假。
  • 正则表达式
    函数库会根据正则表达式来比较错误消息是否匹配你的要求, 采用的是
    JavaScript 的regex.test() 方法。
  • 字符串
    函数库会直接比较错误消息与指定的字符串。
  • 对象构造类型
    函数库会用typeof 来对异常进行操作并测试。如果该测试在调用typeof 时抛
    出错误,则认为异常类型匹配,这可以用来使throws() 和doesNotThrow() 变
    得更为灵活。

5.虚拟机

虚 拟机(vm)模块让你可以运行任意一块代码,并得到运行结果。它提供了一些功能,可以修改指定代码运行的上下文。这很有用,比如可以用来作为人造沙箱。但 是代码还是运行在同一个Node 进程里,所以你依然需要小心行事。vm 和eval()类似,但提供了更多功能和更好的API 来管理代码。然而,它不像eval() 那样能提供与本地作用域互动的能力。用vm 运行代码有两种方法。第一种与使用eval() 的方法类似,把代码内嵌运行;第二种是先把代码预编译成vm.Script 对象。我们看一下下例 ,它演示了如何用vm 内嵌运行代码

用vm 来运行代码

> var vm = require('vm');
> vm.runInThisContext("1+1");
2

到目前为止,vm 看起来都很像eval()。我们给它传入一段代码,它就返回了结果。但是,vm 并不会像eval() 那样改变本地作用域的内容。用eval() 执行的代码会像真的嵌入在当前位置运行一样,并且替换掉eval() 函数调用,而调用vm 方法就不能这样作用于本地作用域了。所以说,eval() 会修改周围的上下文,但vm 不会

使用vm 和eval() 在访问本地作用域时的区别

> var vm = require('vm'),
... e = 0,
... v = 0;
> eval(e=e+1);
1
> e
1
> vm.runInThisContext('v=v+1');
ReferenceError: v is not defined
    at evalmachine.<anonymous>:1:1
    at [object Context]:1:4
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
    at ReadStream._emitKey (tty_posix.js:307:10)
>
> vm.runInThisContext('v=0');
0
> vm.runInThisContext('v=v+1');
1
>
0

我们创建了e 和v 两个变量。在eval() 中使用变量e 时,代码执行结束后,该结果会影响正文内容。但是当对变量v 用vm.runInThisContext() 尝试做同样的操作时,得到的结果是出现异常,因为对变量v 的引用是在等号右边,而该变量还没有定义。eval() 是运行在当前作用域的,而vm 不是。vm 实际上会在每一个实例的内部,维护一套独立的本地上下文,并且能够保持状态。因此,当我们在vm 的作用域内创建了变量v,该变量就能够在同一个vm 的后续操作中有效,并且保持上一次调用时的状态。但是vm 内的变量v 并不会影响运行在主事件循环中的本地作用域。此外,也可以传给vm 一个已经存在的上下文内容。该上下文会作为默认的上下文使用。下例 使用了vm.runInNewContext(),并以第二个参数作为上下文对象。该对象的作用域就成了我们用vm 运行代码的上下文。如果我们继续把它传给不同的调
用,此上下文就会被修改。而且,这个上下文能够被全局作用域使用。

传给vm 上下文

> var vm = require('vm');
> var context = { alphabet:"" };
> vm.runInNewContext("alphabet+='a'", context);
'a'
> vm.runInNewContext("alphabet+='b'", context);
'ab'
> context
{ alphabet: 'ab' }
>

你也可以把代码编译成vm.Script 对象。这样就可以重复运行同一段代码,从而节省一些代码量。在运行的时候,你可以选择用哪个上下文来执行,这样就可以很方便地对不同的上下文执行同一段代码了。

用vm 把代码编译成脚本对象

> var vm = require('vm');
> var fs = require('fs');
>
> var code = fs.readFileSync('example.js');
> code.toString();
'console.log(output);\n'
>
> var script = vm.createScript(code);
> script.runInNewContext({output:"Kick Ass"});
ReferenceError: console is not defined
    at undefined:1:1
    at [object Context]:1:8
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
    at ReadStream._emitKey (tty_posix.js:307:10)
> script.runInNewContext({"console":console,"output":"Kick Ass"});
Kick Ass

这个例子从一个 JavaScript 文件中读取代码,里面包含一句简单的命令console.log(output);。我们先把它编译成一个script 对象,这样就能对此脚本执行script.runInNewContext(),并且传入一个上下文。为了演示,我们故意触发一个错误。当运行 vm.runInNewContext() 时, 你需要传入所引用的对象( 如console 对象),否则,即使是最基础的全局函数也不能使用。还需要注意的是,抛出异常的位置是undefined:1:1。所有的vm 运行命令都可以把文件名作为可选的最后一个参数。它不会改变其功能,但是允许你设置出现错误时在消息里想要显示的文件名字。如果你从磁盘加载并运行了许多文件,这个功能就很有帮助,因为它能够告诉你哪个代码出现了错误。该参数是完全随意的,因此你可以采用任何有助于调试的字符串作为参数。

 

26 2015-06

 

我要 分享

 

 

本文 作者

 

相关 文章