用 Node.js 实现异步 I/O


Node 是基于 C++ 的高速 JavaScript 解释器,绑定了进程、文件和网络套接字等底层 Unix API,还绑定了 HTTP 客户端和服务器 API,除了一些专门命名的同步方法外,Node 的绑定都是异步的,且 Node 程序默认绝不阻塞,这意味着它们通常具备强大的可伸缩性能力并能有效处理高负荷。由于 API 是异步的,因此 Node 依赖事件处理程序,通常使用嵌套函数和闭包来实现。

安装配置

可以到 Node 下载页下载适用于自己系统的安装文件并安装。

安装完成后,和 Rhino 一样,可以使用如下命令运行 Node 程序:

node program.js

功能特性

Node 在全局对象中实现了所有标准的 ECMAScript 5 构造函数、属性和函数,除此之外,它也支持客户端的计时器函数集 setTimeout()setInterval()clearTimeout()clearInterval()

Node 在 process 名字空间中定义了其他重要的全局属性,这里有该对象的一些属性:

  • process.version:Node版本信息
  • process.argv:命令行数组参数
  • process.env:环境变量对象
  • process.pid:进程id
  • process.getuid():返回用户id
  • process.cwd():返回当前工作目录
  • process.chdir():改变目录
  • process.exit():退出

由于 Node 的函数和方法是异步的,因此当它们等待运算完成时并不产生阻塞,非阻塞方法的返回值无法立即返回异步运算的结果,如果要获取结果,或想知道完成运算的时间,当结果准备好或完成运算时,就必须提供 Node 能调用的一个函数,在某些情况下(如定时器),只需简单地把函数作为参数传入,Node 会适时调用它,在另一些情况下,则可以利用 Node 的事件机制:Node 对象产生事件(事件触发器)定义 on() 方法来注册处理程序,当传入参数时,将事件类型作为第一参数,处理程序函数作为第二参数,不同事件类型传递给处理程序函数的参数不同,需要查阅 API 文档从而了解如何编写处理程序:

emitter.on(name, f)    // emitter 注册 f 函数来处理 name 事件
emitter.addListener(name, f) // addListener() 和 on() 方法一样
emitter.once(name, f)  // 只执行一次,然后 f 会自动删除
emitter.listeners(name)  // 返回事件处理函数组成的数组
emitter.removeListener(name, f)  // 注销事件处理程序 f
emitter.removeAllListeners(name)  // 移除name事件的所有处理程序

前面介绍的 process 对象就是一个事件触发器,下面是其部分事件处理程序示例:

// exit 事件在 Node 退出之前发送
process.on("exit", function(){ console.log("Goodbye."); });

// 如果注册了任何事件处理程序,非捕获异常都会产生事件
// 否则,异常仅会使 Node 输出错误然后退出
process.on("uncaughtException", function(){ console.log(Exception, e); });

// POSIX 中诸如 SIGINT、SIGUP 和 SIGTERM 等信号产生事件
process.on("SIGINT", function() { console.log("Ignored Ctrl-C"); });

Node 的设计目标是高性能 I/O,因此其流 API 常被用到,当数据准备好时,可读流会触发事件,下面假设 s 是可读流,我们来演示如何从文件和网络套接字中得到流对象:

s.on("data", f);    // 数据可用时,将其传入 f()
s.on("end", f);     // 不再有数据到达,在文件结束时触发 end 事件
s.on("error", f);   // 发生错误,把异常传递给 f()
s.readable          // 可读流是否依旧打开
s.pause();          // 暂停 data 事件
s.resume();         // 恢复 data 事件

// 如果想把字符串传递给 data 事件处理程序,请指定编码
s.setEncoding(enc);  // utf8、ascii 或 base64

可写流比可读流的核心事件少,使用 write() 方法发送数据,当所有方法使用完毕后使用 end() 方法结束流,write() 方法不会阻塞,下面代码中的 s 是输出流:

s.write(buffer);             // 写入二进制数据
s.write(string, encoding);   // 写入字符串数据(默认编码是 utf8)
s.end();                     // 结束流
s.end(buffer);               // 写入最后的二进制数据块并结束
s.end(str, encoding);        // 写入最后的字符串数据并结束
s.writable;                  // 可写流是否依旧打开且可写入
s.on("drain", f);            // 当内部缓存区为空,调用函数 f()

Node 文件和文件系统 API 位于 fs 模块,我们可以这样将其引入:

var fs = require("fs");   // Node 通过 require() 引入模块

这个模块提供了绝大部分方法的「同步版本」,任何以「Sync」结尾的方法都是同步(阻塞)方法,它返回一个值或抛出异常,而不以「Sync」结尾的文件系统方法都是异步(非阻塞)方法,它们会把结果或错误传递给指定的回调函数。下面的代码展示了如何使用同步方法读取文本文件和异步方法读取二进制文件:

// 同步读取文件
var text = fs.readFileSync("config.json", "utf8");

// 异步读取二进制文件
fs.readFile("image.png", function(err, buffer) {
  if (err) throw err;
  process(buffer);   // 文件内容在缓冲区中
});

Node 还定义了读写文件的流 API,下面我们通过流 API 来实现文件复制:

function fileCopy(filename1, filename2, done) {
  var input = fs.createReadStream(filename1);    // 输入流
  var output = fs.createWriteStream(filename2);  // 输出流
  input.on("data", function (d) {                // 把输入复制到输出
    output.write(d);                           
  });                                            
  input.on("error", function (err) {             // 提示错误
    throw err;
  });
  input.on("end", function () {                  // 输入结束
    output.end();                              // 关闭输出
    if (done)                                  // 通知回调函数
      done();
  });
}

fs 模块还定义了大量的方法,用于列出文件目录、查询文件属性等,下面的代码列出了一个目录的内容,并显示文件大小和修改日期:

// 加载需要的模块
var fs = require("fs"), path = require("path");
// 进入当前目录
var dir = process.cwd();
// 如果来自命令行
if (process.argv.length > 2)
  dir = process.argv[2];
// 读取目录内容
var files = fs.readdirSync();
// 输出头
process.stdout.write("Name\tSize\tDate\n");
files.forEach(function (filename) {
  // 获取文件绝对路径
  var fullname = path.join(dir, filename);
  // 获取文件属性
  var stats = fs.statSync(fullname);
  // 标记子目录
  if (stats.isDirectory())
    filename += "/";
  // 输出文件名+文件大小+修改时间
  process.stdout.write(filename + "\t" + stats.size + "\t" + stats.mtime + "\n");
});

net 模块是用于基于 TCP 网络的 API,下面是一个简单的 TCP 服务器:

// 简单的TCP回显服务器,监听2000端口上的连接并把客户端的数据回显给它
var net = require("net");
var server = net.createServer();
server.listen(2000, function () {
  console.log("Listening on port 2000");
});
server.on("connection", function (stream) {
  console.log("Accepting connection from ", stream.remoteAddress);
  stream.on("data", function (data) {
    stream.write(data);
  });
  stream.on("end", function (data) {
    console.log("Connection closed");
  })
});

除了基础的 net 模块,Node 使用 http 模块内置支持 HTTP 协议。

示例:HTTP服务器

下面是基于 Node 的简单 HTTP 服务器,能处理两种当前目录的文件,并能实现两种特殊的URL:

var http = require("http");
var fs = require("js");

// 创建新的HTTP服务器并监听8000端口
var server = http.Server();
server.listen(8000);

// 当服务器收到新请求,则运行函数处理它
server.on("request", function (request, response) {
  // 解析请求的URL
  var url = require("url").parse(request.url);

  // 此处用于模拟缓慢的网络连接
  if (url.pathname === "/test/delay") {
    // 通过查询字符串获取延迟时长,默认为2000毫秒
    var delay = parseInt(url.query) || 2000;
    // 设置响应状态码和头
    response.writeHead(200, {"Content-Type": "text/plain; charset=UTF-8"});
    // 编写响应实体
    response.write("Sleeping for " + delay + " milliseconds...");
    // 通过定时器在之后调用的另一函数中完成响应
    setTimeout(function () {
      response.write("done.");
      response.end();
    }, delay);
  } else if (url.pathname === "/test/mirror") {   // 如果请求是 /test/mirror,则原文返回它
    response.writeHead(200, {"Content-Type": "text/plain; charset=UTF-8"});
    response.write(request.method + " " + request.url + " HTTP/" + request.httpVersion + "\r\n");
    // 所有请求头
    for (var h in request.headers) {
      response.write(h + ": " + request.headers[h] + "\r\n");
    }
    // 使用额外的空白行来结束头
    response.write("\r\n");
    // 将请求数据写入响应
    request.on("data", function (chunk) {
      response.write(chunk);
    });
    // 请求结束时,响应也完成
    request.on("end", function (chunk) {
      response.end();
    })
  } else {
    // 获取本地文件名,基于其扩展名推测内容类型
    var filename = url.pathname.substring(1); // 去掉前导'/'
    var type;
    switch (filename.substring(filename.lastIndexOf(".") + 1)) {
      case "html":
      case "htm":
        type = "text/html; charset=UTF-8";
        break;
      case "js":
        type = "text/javascript; charset=UTF-8";
        break;
      case "css":
        type = "text/css; charset=UTF-8";
        break;
      case "txt":
        type = "text/plain; charset=UTF-8";
        break;
      case "manifest":
        type = "text/cache-manifest; charset=UTF-8";
        break;
      default:
        type = "application/octet-stream";
        break;
    }

    // 异步读取文件,并将内容作为单独的数据块传给回调函数
    fs.readFile(filename, function (err, content) {
      if (err) {
        response.writeHead(404, {"Content-Type": "text/plain; charset=UTF-8"});
        response.write(err.message);
        response.end();
      } else {
        response.writeHead(200, {"Content-Type": "text/plain; charset=UTF-8"});
        response.write(content);
        response.end();
      }
    });
  }
});

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 用 Rhino 脚本化 Java

>> 下一篇: 客户端 JavaScript 概述