Node.js 核心(原生)模块整理

 2016年07月26日    629     声明


核心(原生)模块是指那些随Node.js安装而安装的模块,这些模块在Node.js源代码编译时被编译成二进制执行文件。相比文件模块,核心(原生)模块的加载速度更快。核心(原生)模块提供了JavaScript语言之外处理能力,如:网络处理相关模块httpnetdgram,文件及流处理fsstream,二进制处理模块buffer,系统与进程osprocess……

  1. EventEmitter - 事件模块
  2. 网络处理相关模块
  3. 缓存、流与文件
  4. 系统与进程、集群
  5. 加密、编/解码、压缩等
  6. 辅助模块URL处理、工具类等
  7. 模块、全局对象

1. EventEmitter - 事件模块

大部分的Node.js核心API被实现为异步事件驱动架构,这些对象(“发射器”)会周期性的发射事件名,并会触发监听函数(“监听器”)的调用。

Node.js中许多对象都可以发送事件。如:net.Server对象会在每次收到新连接时发送'request'事件;fs.ReadStream对象会在打开文件时发送'open'事件;stream.Readable对象会在每次读取数据时发送'data'事件。

所有这些可以发送事件的对象,都是一个EventEmitter类实例。它们会暴露一个工eventEmitter.on()函数,在指定事件发送时调用监听函数。

EventEmitter对象发送事件时,其上绑定函数会被同步调用。但是,监听函数所有的返回值都会被忽略。

通过继承EventEmitter类后,其实例就是一个事件发射器:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
myEmitter.emit('event');

上例中通过util.inherits()方法实现了Node.js式继承。同样,也可以使用ES6 Class

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
myEmitter.emit('event');

向监听器传递参数

eventEmitter.emit()发射事件时,可以向监听函数传递参数。其第一个参数为事件名,其后参数为要传递给事件监听器的参数:

const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
  console.log(a, b, this);
    // Prints: a b {}
});
myEmitter.emit('event', 'a', 'b');

多次与单次监听

通过eventEmitter.on()注册的事件监听器会在每次触发指定时调用。

const myEmitter = new MyEmitter();
var m = 0;
myEmitter.on('event', () => {
  console.log(++m);
});
myEmitter.emit('event');
  // Prints: 1
myEmitter.emit('event');
  // Prints: 2

而通过eventEmitter.once()方法注册的事件监听器只会被调用一次:

const myEmitter = new MyEmitter();
var m = 0;
myEmitter.once('event', () => {
  console.log(++m);
});
myEmitter.emit('event');
  // Prints: 1
myEmitter.emit('event');
  // Ignored

Class: EventEmitter

EventEmitter类在events模块中定义并导出的类,该类的实例就是一个事件发射器,Node.js中所有具有事件发射功能的对象都是该类的一个实例。可以像下面这样获取该类:

const EventEmitter = require('events');

所有EventEmitter类实例中都有'newListener''removeListener'两个事件,分别会在添加新的事件临听器和移除事件临听器时触发。

EventEmitter.defaultMaxListeners

默认情况下,每个事件可以添加10个事件监听器。可以通过emitter.setMaxListeners(n) 方法修改每个实例的监听器数量。如果需要全局修改可添加事件监听器数量,可以通过类属性EventEmitter.defaultMaxListeners来设置。

emitter.setMaxListeners(emitter.getMaxListeners() + 1);
emitter.once('event', () => {
  // do stuff
  emitter.setMaxListeners(Math.max(emitter.getMaxListeners() - 1, 0));
});

EventEmitter实例中的常用方法有:

  • emitter.addListener(eventName, listener) - 添加事件监听器,emitter.on()的别名方法
  • emitter.getMaxListeners() - 最大可添加的监听器数量
  • emitter.listenerCount(eventName) - 某事件的监听器数量
  • emitter.listeners(eventName) - 返回事件所有的监听函数
  • emitter.on(eventName, listener) - 添加事件监听器
  • emitter.once(eventName, listener) - 添加仅使用一次的事件监听器
  • emitter.removeAllListeners([eventName]) - 移除所有事件监听器
  • emitter.removeListener(eventName, listener) - 移除指定事件的监听器
  • emitter.setMaxListeners(n) - 设置最大可添加监听器,此设置优先级高于全局设置


2. 网络处理相关模块

2.1 net - 实现TCP/Socket通讯

net模块提供了异步网络封装,我们可以使用这个模块实现TCP服务器或客户端(TCP Socket)。该模块是对TCP协议的封装和实现,同时它还为上层应用提供了传输功能,如:http模块就依赖该模块实现。

net模块中有net.Servernet.Socket两个类,并通过这两个类对象提供TCP服务器及TCP Socket相关功能。

Class: net.Server - TCP服务器

net.Server类用于创建一个TCP或本地服务器,该类实例由net.createServer()方法创建并返回 :

var server = net.createServer((socket) => {
  socket.end('goodbye\n');
}).on('error', (err) => {
  // 错误处理
  throw err;
});

// 监听一个随机端口
server.listen(() => {
  address = server.address();
  console.log('opened server on %j', address);
});

在上例中,我们创建了一个TCP服务器,并在一个随机端口启动了服务器监听。

net.Server是一个事件发射器是一个EventEmitter,它会发射'close''connection''error''listening'事件。除了可以创建服务器时添加连接监听函数外,还可以通过'connection'事件添加:

var server = net.createServer();

// 添加 'connection' 事件监听
server.on('connection', (socket) => {
  socket.end('goodbye\n');
})

'connection'事件的监听函数中包含一个参数::

function (socket){}

socket是一个net.Socket实例。


Class: net.Socket - TCP Socket

TCP是一种面向连接的协议,Socket套接字是对TCP协议的具体封装。在其被封将为net.Socket类,服务器与客户端之间的通讯都基于该类的实例实现。

net.Socket对象是一个抽象的TCP或本地套接。它可以由用户在客户端创建(connect());或由TCP服务器创建,并被传递至'connection'事件的回调函数参数中。

在客户端创建net.Socket实例时,可以使用构造函数new net.Socket([options]),也可以使用工厂方法net.connect()net.createConnection()。使用工厂方法创建时,除会返回套接字实例外,还会自动连接至指定的TCP服务器:

const net = require('net');
const client = net.connect({port: 8124}, () => {
  // 'connect' 监听
  console.log('connected to server!');
  client.write('world!\r\n');
});
client.on('data', (data) => {
  console.log(data.toString());
  client.end();
});
client.on('end', () => {
  console.log('disconnected from server');
});

net.Socket实现了双向流接口,它还是一个EventEmitter。Node.js TCP客户端与服务器之间的通讯是基于流的操作,如:设置编码socket.setEncoding()基本质上是设置可读流的编码,其内部会调用stream.setEncoding()方法;除了其自身添加的事件外,还有部分事件(如上例中使用的'data''end'事件)继承自stream模块。


2.2 tls - TLS (SSL)

tls模块使用 OpenSSL 来提供传输层安全协(Transport Layer Security)和安全套接字层(Secure Socket Layer)。该模块使用加密过的流通讯,可以使用这个模块构建一个安全的TCP服务器或是一个安全的Socket套接字。

使用tls模块前,应该首先创建TLS/SSL公钥、私钥

Class: SecurePair

SecurePair表示“密钥对”类,该类对象由tls.createSecurePair()创建并返回。对象中会包含两个流,一个用于读/写加密数据,一个用于读/写明文数据。

Class: tls.Server

tls.Servernet.Server的子类并与其有相同的方法。不同于原始TCP请求,tls.Server服务器会使用TLSSSL加密连接。

该类实例由tls.createServer()创建并返回:

const tls = require('tls');
const fs = require('fs');

const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  // 这个选项只用于对客户端证书进行验证时
  requestCert: true,
  // 这个选项用于客户端使用自签名证书时
  ca: [ fs.readFileSync('client-cert.pem') ]
};

var server = tls.createServer(options, (socket) => {
  console.log('server connected',
              socket.authorized ? 'authorized' : 'unauthorized');
  socket.write('welcome!\n');
  socket.setEncoding('utf8');
  socket.pipe(socket);
});
server.listen(8000, () => {
  console.log('server bound');
});

也可以使用.pfx文件创建:

const tls = require('tls');
const fs = require('fs');

const options = {
  pfx: fs.readFileSync('server.pfx'),
  requestCert: true,
};

var server = tls.createServer(options, (socket) => {
  console.log('server connected',
              socket.authorized ? 'authorized' : 'unauthorized');
  socket.write('welcome!\n');
  socket.setEncoding('utf8');
  socket.pipe(socket);
});
server.listen(8000, () => {
  console.log('server bound');
});

创建服务器后,可以使用openssl s_client命令测试服务器:

openssl s_client -connect 127.0.0.1:8000

TLS/SSL服务器不同于TCP服务器,除可以接收普通连接外,还可以接收通过'secureConnection'事件添加安全连接。上例中,创建服务器时添加的函数就会自动添加到该事件:

function (tlsSocket) {}

这个回调函数中有一个参数,该参数是一个tls.TLSSocket实例。

Class: tls.TLSSocket

tls.TLSSocket是对net.Socket的包装,使用TLS加密传输代替了透明传输。它的实例实现了双向流接口,它有流中所有事件及方法。

可以使用构造函数,从一个已存在的TCP套接字中创建一个TLSSocket对象:

new tls.TLSSocket(socket[, options])

也可以使用tls.connect()方法,连接到一个远程TCP服务器,并创建一个TLSSocket对象:

const tls = require('tls');
const fs = require('fs');

const options = {
  // 此选项仅用于需要验证客户端证书时
  key: fs.readFileSync('client-key.pem'),
  cert: fs.readFileSync('client-cert.pem'),

  // 些选项仅用于使用自签名证书时
  ca: [ fs.readFileSync('server-cert.pem') ]
};

var socket = tls.connect(8000, options, () => {
  console.log('client connected',
              socket.authorized ? 'authorized' : 'unauthorized');
  process.stdin.pipe(socket);
  process.stdin.resume();
});
socket.setEncoding('utf8');
socket.on('data', (data) => {
  console.log(data);
});
socket.on('end', () => {
  server.close();
});

或使用.pfx文件创建连接:

const tls = require('tls');
const fs = require('fs');

const options = {
  pfx: fs.readFileSync('client.pfx')
};

var socket = tls.connect(8000, options, () => {
  console.log('client connected',
              socket.authorized ? 'authorized' : 'unauthorized');
  process.stdin.pipe(socket);
  process.stdin.resume();
});
socket.setEncoding('utf8');
socket.on('data', (data) => {
  console.log(data);
});
socket.on('end', () => {
  server.close();
});


2.3 dgram - UDP / Datagram

UDP(Datagram),用户数据报协议。UDP和TCP同样属于OSI七层中传输层的协议。不同的是,TCP是面向连接的协议,其建立连接会经过三次握手,并会通过keepAlive包监听终端在线情况;而UDP是一种无连接的协议,虽然UDP在数据传递过程中会出现数据报丢失的情况,但其优势在于,极低的带宽点用量会大大降低执行时间,使传输速度得到了保证。

dgram是Node.js提供的用于实现UDP套接字功能模块。在UDP通讯中,没有服务器-客户端的概念,创建一个套接字对象并绑定通讯端口,即可实现数据广播/接收等功能。

Class: dgram.Socket

dgram.Socketdgram模块中的唯一一个类,该类实现了UDP广播、数据发送/接收等功能。创建该类的实例使用dgram.createSocket()方法,不支持new关键字创建dgram.Socket实例。

const dgram = require('dgram');
const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.log(`server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
  server.send('收到了:'+msg, 0, msg.length, rinfo.port, rinfo.address);
});

server.on('listening', () => {
  var address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

server.bind(41234);
// server listening 0.0.0.0:41234

如上,我们创建了一个UDP服务器,并在0.0.0.0:41234启动监听。但这个“服务器”并不是真正意义上服务器,只是一个套接字端点,

dgram.Socket是一个EventEmitter),它会发送'close''error''listening''message'事件。在上例中,通过'message'添加事件监听实现了消息的接收,还可以在创建对象时添加消息监听。在添加的回调函数中,包含两个参数:

function (msg, rinfo) {}

其中,msg表示收到消息;rinfo表示远程(发送消息方)套结字端点的地址信息。

接下来,再创建另一个UDP端点(客户端),实现两个端点之间的数据收发:

var dgram = require('dgram');
var client = dgram.createSocket('udp4');
var message=new Buffer('time');
client.send(message, 0, message.length, 41234, 'localhost', function (err, bytes) {
  //数据发送监听器
  if(err) {throw err;}
  console.log(bytes)
});

//监听message事件,接收数据
client.on('message', function(msg){
  console.log('收到了UDP服务端消息:', msg.toString());
})


2.4 http

HTTP是OSI七层中应用层的协议,其底层传输依赖TCP。在Node.js中,http模块也依赖于net模块。

Node.js所提供的HTTP相关API非常底层,它只做流处理和消息解析。而解析消息时,只将其解析为消息头和消息体,但并不解析具体的消息头或消息体。

http模块给我们留了足够大的操作空间,利用这个模块可以创建HTTP服务器、进行HTTP请求或响应的处理、创建HTTP客户端、或创建HTTP代理等。

在这个模块中,包含以下4个类。简单介绍如下:

Class: http.ClientRequest - 客户端请求

该对象(类实例)在http.request()http.get()方法的内部创建并返回,表示一个正在处理的HTTP请求:

var postData = querystring.stringify({
  'msg' : 'Hello World!'
});

var options = {
  hostname: 'itbilu.com',
  port: 80,
  path: '/upload',
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': postData.length
  }
};

var req = http.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding('utf8');
  res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on('end', () => {
    console.log('No more data in response.')
  })
});

req.on('error', (e) => {
  console.log(`problem with request: ${e.message}`);
});

// 向请求体中写入数据
req.write(postData);
req.end();

在上面这个HTTP客户端请求对象中,我们通过req.write()方法向请求体中写入数据,并通过req.end()方法结束请求。在http.request()回调函数中接收服务器返回数据,服务器返回的数据会被封装到一个http.IncomingMessage对象(即上面的res)中。

该对象是一个可写流,还是一个事件发射器(EventEmitter)对象。

对于上面示例来说,我们还可以基本流向请体中写入数据,并通过监听req对象的'response'来接收或处理服务器返回数据:

var req = http.request(options)
req.on('response', (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding('utf8');
  res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on('end', () => {
    console.log('No more data in response.')
  })
});

fs.createReadStream('sample.txt').pipe(req);

Class: http.Agent - 代理

http.Agent会把套接字做成资源池,用于HTTP客户端请求。在HTTP请求时,如果需要自定义一些自定义的代理参数(如:主机的套接字并发数、套接字发送TCP KeepAlive包的频率等),可以设置此对象。该对象由构选函数new Agent([options])创建返回:

const http = require('http');
var keepAliveAgent = new http.Agent({ keepAlive: true });
options.agent = keepAliveAgent;
http.request(options, onResponseCallback);

Class: http.Server - 服务器类

http.Server表示一个HTTP服务器,该类实例由http.createServer()创建并返回。该类是net.Server的子类,除父类中的方法、事件外,http.Server还添加的一些自己的方法、事件等。

创建一个HTTP服务器非常简单:

const server = http.createServer((req,res) => {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('ok');
});

server.listen(3000);

如上,我们创建了一个简单的HTTP服务器,并在3000端口监听传入连接。除从端口启动服务器外,还支持从文件描述符或UNIX Socket套接字启动,详见server.listen()

http.Servernet.Server一样是一个EventEmitter。在上例中,除了可以在创建服务器时添加监听外,还可以通过监听'request'事件添加:

const server = http.createServer();

server.on('request', (req,res) => {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('ok');
});

server.listen(3000);

在服务器对象的'request'事件监听函数中:

function (request, response) { }

这个函数会在每次有新连接进入时被调用。其中,request表示用户请求对象,它是一个http.IncomingMessage实例;response表示服务器响应对象,它是一个http.ServerResponse实例。

Class: http.IncomingMessage

IncomingMessage对象(实例)可以被http.Serverhttp.ClientRequest创建。在http.Server中会被传入'request'事件的第一个参数中,表示服务器收到的用户请求对象,可以通过该对象接收用户请求中的数据提交、文件上传等数据;在http.ClientRequest中,会被传入到'response'事件的第一个参数中,表示服务器请求对象,可以通过该对象访问来服务器的响应状态、响应头、及响应数据等。需要注意:<只存在于用户用户请求与服务器响应对象并不完全一致,如:code>message.url只存在于用户请求对象中;而message.statusCodemessage.statusMessage只存在于服务器响应对象中存在。

IncomingMessage对象是一个可读流。这意味着,我们可以基于流接收到来自用户请求或服务器响应的数据:

server.on('request', (req, res) => {
  req.setEncoding('binary'); 
  var body = '';   // 用户请求数据
  req.on('data', function(chunk){
    body += chunk;
  });

  req.on('end', function() {  
    console.log('收到用户请数据'+body.toString());
  })
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('ok');
});

Class: http.ServerResponse

ServerResponse对象(实例)会在HTTP服务器中创建,并被传递至'request'事件监听函数的第二个参数中,服务器通过该对象向用户返回响应数据。

该对象是一个可写流,也是一个EventEmitter。可以将一个可读流.pipe()转换给该对象,做为用户响应:

server.on('request', (req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html'});
  http.get('http://itbilu.com', (response) => {
    response.pipe(res);
  })
});


2.5 https

HTTPS是在TLS/SSL之上构建的HTTP协议,Node.js中做为一个单独的模块实现。

Class: https.Server

https.Servertls.Server的子类,与http.Server具有相同的事件。

https.Server对象是一个HTTPS服务器,可以通过https.createServer()方法创建服务器:

// curl -k https://localhost:8000/
const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('hello world\n');
}).listen(8000);

HTTPS服务器创建与HTTP服务器创建比较类似,只是创建时需要加入TLS/SSL密钥证书等。也可以像下面这样创建:

const https = require('https');
const fs = require('fs');

const options = {
  pfx: fs.readFileSync('server.pfx')
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('hello world\n');
}).listen(8000);

如上所示,我们创建一个HTTPS服务器,并添加了连接监听函数。和HTTP服务器一样,该函数也可以通过'request'事件添加:

function (request, response) { }

这个函数会在每次有新连接进入时被调用。其中,request表示用户请求对象,它是一个http.IncomingMessage实例;response表示服务器响应对象,它是一个http.ServerResponse实例。

https.request()

HTTPS模块还提供了用与请求HTTPS服务器的方法https.request()https.get()

https.request(options, callback)

https.request()https.get()不同,这两个方法在进行HTTP请求时可以添加TLS/SSL密钥/证书等:

const https = require('https');

var options = {
  hostname: 'encrypted.google.com',
  port: 443,
  path: '/',
  method: 'GET'
};

var req = https.request(options, (res) => {
  console.log('statusCode: ', res.statusCode);
  console.log('headers: ', res.headers);

  res.on('data', (d) => {
    process.stdout.write(d);
  });
});
req.end();

req.on('error', (e) => {
  console.error(e);
});


3. 缓存、流与文件

3.1 buffer - 缓存

在ECMAScript 2015(ES6)推出TypedArray之前,JavaScript并没有读取或操作二进制数据的能力。Buffer为Node.js带来了如TCP流操作和文件系统流操作的能力。

现在TypedArray被添加到了ES6中,而Buffer类实现了Uint8Array相关API,这样会更加优化和适合在Node.js中使用。Buffer实例与整型数非常类似,但其长度是固定的,创建后不能修改。

Buffer在Node.js中是一个全局对象,可以像下面这样使用这个类:

// 创建一个长度为 10 的缓存
const buf1 = new Buffer(10);

// 创建一个包含 [01, 02, 03] 的缓存
const buf2 = new Buffer([1,2,3]);

// 创建一个包含ASCII编码的包含 [74, 65, 73, 74] 的缓存
const buf3 = new Buffer('test');

// 创建一个utf8编码的包含 [74, c3, a9, 73, 74] 的缓存
const buf4 = new Buffer('tést', 'utf8');

缓存区编码

Buffers通常用来表示经过编码(如:UTF8UCS2Base64Hex等)的字符序列。可以通过指定编码方式,在Buffers与日常使用字符串之间进行转换:

const buf = new Buffer('hello world', 'ascii');
console.log(buf.toString('hex'));
  // prints: 68656c6c6f20776f726c64
console.log(buf.toString('base64'));
  // prints: aGVsbG8gd29ybGQ=

Node.js支持的编码方式有:

  • 'ascii' - 7位的 ASCII 数据。这种编码方式非常快,而且会剥离设置过高的bit。
  • 'utf8' - 多字节编码 Unicode 字符。大部分网页和文档使用这类编码方式。
  • 'utf16le' - 2个或4个字节,Little Endian (LE) 编码 Unicode 字符。编码范围 (U+10000 到 U+10FFFF) 。
  • 'ucs2' - 'utf16le'的别名。
  • 'base64' - Base64 字符串编码。
  • 'binary' - 将原始2进制数据编码为字符串。
  • 'hex' - 每个byte编码成2个十六进制字符。

Buffers 与 TypedArray

Buffer同样是一个Uint8Array类型数组实例。但是,它与ECMAScript 2015的TypedArray(类型数组)规范并不完全兼容。如:ArrayBuffer#slice()会创建一个分隔部分数据的拷贝,而Buffer#slice()会创建一个Buffer中拷贝数据的视图,相对来说Buffer#slice()更高效。

Buffer与类型数组可以使用相同的内存区,使用类型数组对象的.buffer属性创建缓存即可:

const arr = new Uint16Array(2);
arr[0] = 5000;
arr[1] = 4000;

const buf1 = new Buffer(arr); // buffer 复制
const buf2 = new Buffer(arr.buffer); // 与 arr 共享内存区;

console.log(buf1); // <Buffer 88 a0> 复制了 buffer 中的两个元素
console.log(buf2); // <Buffer 88 13 a0 0f>

arr[1] = 6000;
console.log(buf1); // <Buffer 88 a0>
console.log(buf2); // <Buffer 88 13 70 17>

Class: Buffer

Buffer是一个用于处理二进制数据的全局类,可以通过构造函数创建该类的实例。它有多种构建方式:

// 通过一个数组创建
new Buffer(array);

// 通过另一个缓存实例创建
new Buffer(buffer);

// 通过一个类型数组创建
new Buffer(arrayBuffer);

// 创建一个指定长度的缓存对象
new Buffer(size);

// 通过指定字符串(及编码)创建缓存对象
new Buffer(str[, encoding]);

Buffer类提供了一些类方法,这些方法可以用于比较两个缓存是否相同、连接两个缓存对象等:

  • Buffer.byteLength(string[, encoding]) - 检测字符串编码后的位长度
  • Buffer.compare(buf1, buf2) - 比较两个缓存对象是否相同
  • Buffer.concat(list[, totalLength]) - 连接多个缓存对象
  • Buffer.isBuffer(obj) - 检查是否是一个缓存对象
  • Buffer.isEncoding(encoding) - 检查指定的编码是否可用

Buffer实例中有部分方法与类方法功能相同,详见:Buffer类实例属性和方法


3.2 stream - 流处理

流(Stream)是一个抽像的接口。Node.js中流有4种形式Readable(可读流)Writable(可写流)Duplex(双向流)和Transform(转换流)。可以通过require('stream')来加载流模块,这四种流在该模块中被分别实现为一个单独的类。

stream模块中,流相关的API被实现为两类:面向消费者的API和面向实现者的API。我们更多时候是做为一个流的消费者(使用者),如:从一个可读流中读取数据、向一个可写流写入数据、或将一个可读流中的数据转接到可写流中。

Node.js中有很多对象都是基于流实现,如:HTTP中用户请求对象http.IncomingMessage就是一个可读流、服务器响应对象http.ServerResponse是一个可写流、TCP和UDP套接字Socket都被实现为一个双向流。

所有类型的流都实现了EventEmitter,可以在特定的时刻发送一些事件。

Class: stream.Readable

Readable(可读)流是对正在读取的数据的抽象,它表示一个数据的来源,可读流在接收数据前并不会发生数据。

可读流有流动暂停两种模式。我们可以使用readable.pause()方法暂停一个流动流,或者使用readable.resume()方法恢复一个暂停流。

和Linux等操作系统中的流一样,我们可以使用readable.pipe()方法将一个可读流导向到可写流,也可以使用unpipe()方法移除流导向。

可读流是一个事件发射器,如:收到数据时会触发'data'事件、数据接收完成会收到'end'事件、发生错误会触发'error'事件。

读取可读流中的数据时可以使用readable.read()方法从缓冲区中读取数据。但更推荐基于事件进行数据处理:

var readable = getReadableStreamSomehow();
readable.on('data', (chunk) => {
  console.log('got %d bytes of data', chunk.length);
});
readable.on('end', () => {
  console.log('there will be no more data.');
});

Class: stream.Writable

Writable(可写)流表示数据写入目标的一个抽象。

可写流中也存在一些事件,如:表示可写流已排空的'drain'事件、表示操作完成的'finish'事件、表示发生错误的'error'事件等。

可以使用writable.write()writable.end()方法向可写流中写入数据。Node.js中所有向可写流中写入数据的方法都是来自于Writable

http.createServer((req, res) => {
  res.write('hello, ');
  res.end('world!');
});

Class: stream.Duplex

Duplex表示双向流,它实现了ReadableWritable两者的接口。

Node.js中的双向流有:

Class: stream.Transform

Transform(转换)流是一种输出由输入计算所得的双工流。它们同时实现了ReadableWritable接口。Node.js中的转换流有:


3.3 fs - 文件处理

fs文件系统模块是对标准 POSIX 文件 I/O 操作方法集的一个简单包装。可以通过require('fs')来获取该模块。这个模块中的所有方法均有异步和同步版本。

可以像下面这样,使用异步方法删除一个文件:

const fs = require('fs');

fs.unlink('/tmp/hello', (err) => {
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});

也可以使用同步方法:

const fs = require('fs');

fs.unlinkSync('/tmp/hello');
console.log('successfully deleted /tmp/hello');

对于异步方法来说,需要一个回调函数在操作完成时进行调用。通常情况下回调函数的第一个参数为返回的错误信息,如果异步操作执行正确,则该错误参数为nullundefined

如果使用同步版本的方法,一旦出现错误,会以通常的抛出错误的形式返回错误。你可以用try…catch语句来捕获错误以保证程序的正常运行。

fs模块主要用于文件、目录的操作,以下是一些常用方法。这些方法为异步方法,它们都有对应的同步方法:

  • fs.access(path[, mode], callback) - 判断用户是否有对指定路径/模式path.mode的访问权限
  • fs.appendFile(file, data[, options], callback) - 向指定文件中追加数据
  • fs.chmod(path, mode, callback) - 修改指定文件/目录的可访问模式
  • fs.chown(path, uid, gid, callback) - 修改指定文件/目录的所有者
  • fs.link(srcpath, dstpath, callback) - 建立文件连接
  • fs.mkdir(path[, mode], callback) - 创建目录
  • fs.open(path, flags[, mode], callback) - 打开以指定的模式打开文件
  • fs.read(fd, buffer, offset, length, position, callback) - 读取文件
  • fs.readdir(path, callback) - 读取目录
  • fs.readFile(file[, options], callback) - 读取文件全部内容
  • fs.rename(oldPath, newPath, callback) - 重命名文件/目录
  • fs.rmdir(path, callback) - 移除目录
  • fs.unlink(path, callback) - 移除链接
  • fs.write(fd, buffer, offset, length[, position], callback) - 向文件中写入缓存数据
  • fs.writeFile(file, data[, options], callback) - 向文件中写入数据(Buffer或字符串)

文件操作与流

对较大的文件,或者要向文件中写入较多内容时,可以基于流进行处理。fs模块中存在以下两种流:

Class: fs.ReadStream可读流。该类对象由fs.createReadStream()方法创建:

var fs = require('fs');

var readStream = fs.createReadStream('/etc/passwd')

readStream.on('open', function(fd){
  console.log('文件已打开');
});

readStream.on('data', function(data){
  console.log('收到文件数据');
  console.log(data.toString());
});

Class: fs.WriteStream可写流。该类对象由fs.createWriteStream()方法创建:

var fs = require('fs');

var readStream = fs.createReadStream('/etc/passwd')
var writeStream = fs.createWriteStream('./myFile.txt')

readStream.on('data', function(data){
  writeStream.write(data);
});


3.4 path - 路径处理

操作文件时一般都需要处理文件路径,path模块是Node.js中用于处理文件路径的模块。它模块可以帮你规范化连接和解析路径,还可以用于绝对路径到对路径的转换、提取路径的组成部分及判断路径是否存在等。

path模块是一个用于处理和转换文件路径的工具集。几乎所有的方法只做字符串转换,不会调用文件系统检查路径是否有效。

以下是这个模块的一些用法:

使用path.basename()返回路径的最后一部分:

path.basename('/foo/bar/baz/asdf/quux.html')
// 'quux.html'

path.basename('/foo/bar/baz/asdf/quux.html', '.html')
// 'quux'

使用path.dirname()返回目录名:

path.dirname('/foo/bar/baz/asdf/quux')
// '/foo/bar/baz/asdf'

使用path.extname()返回文件扩展名:

path.extname('index.html')
// returns '.html'

path.extname('index.coffee.md')
// returns '.md'

path.extname('index.')
// returns '.'

path.extname('index')
// returns ''

path.extname('.index')
// returns ''

使用path.join()方法连接两个路径:

path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')
// returns '/foo/bar/baz/asdf'

path.join('foo', {}, 'bar')
// throws exception
TypeError: Arguments to path.join must be strings

使用path.relative()返回两个路径的相对关系:

path.relative('C:\\orandea\\test\\aaa', 'C:\\orandea\\impl\\bbb')
// returns '..\\..\\impl\\bbb'

path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')
// returns '../../impl/bbb'

使用path.resolve()方法返回绝对路径:

path.resolve('/foo/bar', './baz')
// returns '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/')
// returns '/tmp/file'


4. 系统与进程、集群

4.1 os - 系统信息查看

os模块是一个提供了操作系统查询相关API的工具模块,可以通过require('os')获取模块的引用。

os模块中有以下操作接口:

  • os.EOL - 系统行结束符常量,如:'\n'
  • os.arch() - CPU架构信息,如:'x64''arm''ia32'
  • os.cpus() - 返回CPU信息
  • os.endianness() - 返回 CPU 的字节序,可能是BELE
  • os.freemem() - 返回操作系统空闲内存量,单位字节
  • os.homedir() - 返回当前用户的主目录
  • os.hostname() - 返回操作系统主机名
  • os.loadavg() - 返回一个包含 1、5、15 分钟平均负载的数组。
  • os.networkInterfaces() - 获取一个网络接口的列表信息
  • os.platform() - 返回操作系统平台,如:'darwin', 'freebsd', 'linux', 'sunos''win32'
  • os.release() - 返回操作系统的发行版本
  • os.tmpdir() - 返回操作系统默认的监时文件目录
  • os.totalmem() - 返回操作系统内存总量
  • os.type() - 返回操作系统类型,如:'Linux''Darwin''Windows_NT'
  • os.uptime() - 返回操作系统运行的时间,单位为秒


4.2 process - 进程对象

process表示进程对象,它是一个全局对象,不需要引用即可使用。它还是一个EventEmitter实例,我们可以它的一些事件监听进程的退出、异常等。

进程中常用的事件

Event: 'beforeExit'

这一事件会在Node.js清空事件循环,且没有其它调度安提排触发。通常,Node.js退出时没有工作安排,但'beforeExit'事件监听器可以被异步调用,并导致Node.js继续运行。

'beforeExit'并不是在进程明确退出时调用的事件,如:调用process.exit()方法或发生异常时,其不能做为'exit'的替代事件,除非有其它的操作安排时。

Event: 'exit'

会在进程退出时触发的事件。注意:该事件触发后并不能阻止事件循环的退出,一旦'exit'事件监听器调用完成后,进行就会退出。

该事件只会在process.exit()调用后或隐式的结束事件循环后触发。

process.on('exit', (code) => {
  // do *NOT* do this
  setTimeout(() => {
    console.log('This will not run');
  }, 0);
  console.log('About to exit with code:', code);
});

Event: 'uncaughtException'

'uncaughtException'会异常冒泡回归到事件循环中就会触发。默认情况下,发生异常时Node.js会向stderr中打印堆栈信息并结束进程;监听了该事件后,会改变默认处理形为。

process.on('uncaughtException', (err) => {
  console.log(`Caught exception: ${err}`);
});

setTimeout(() => {
  console.log('This will still run.');
}, 500);

// 引发一个异常,但不捕获
nonexistentFunc();
console.log('This will not run.');

注意:'uncaughtException'是一个非常粗略的异常处理,仅能将做为异捕获的最后手段。试图从异常中恢复应用,可能会导致额外的不可预见的和不可预知的问题。

进程中常用属性

process中有很多实用的属性,如:可以使用process.env查看系统中的环境变量、使用process.execArgv查看当前脚本的执行参数、而process.platformos.platform()一样会返回运行平台信息。

进程中常用方法

process.exit([code])

该方法会退出当前进程,调用后会触发'exit'事件。

process.nextTick(callback)

这是非常有用的一个方法,它会事件循环的下一次循环中调用 callback回调函数。它不是setTimeout(fn, 0)函数的一个简单别名,因为它的效率高更高,能够在任何 I/O 事前之前调用我们的回调函数。但是这个函数在层次超过某个限制的时候,也会出现问题,可以通过process.maxTickDepth属性查看执行限制:

console.log('开始');
process.nextTick(function() {
  console.log('nextTick 回调');
});
console.log('已设定');
// 输出:
// 开始
// 已设定
// nextTick 回调

基于流的标准输入/输出/错误

操作系统中有stdin(标准输入)、stdout(标准输出)、stderr(标准错误)三种流,Node.js这三种流的操作API被实现到了process模块中。

process.stderr输出到stderr的可写流

process.stdout输出到stdout的可写流

process.stderrprocess.stdout不同于Node中其它的可写流,它们通常是阻塞式的。

console.log = (msg) => {
  process.stdout.write(`${msg}\n`);
};

process.stdin一个指定stdin的可读流。

process.stdin.setEncoding('utf8');

process.stdin.on('readable', () => {
  var chunk = process.stdin.read();
  if (chunk !== null) {
    process.stdout.write(`data: ${chunk}`);
  }
});

process.stdin.on('end', () => {
  process.stdout.write('end');
});


4.3 child_process - 子进程

Node.js通过child_process模块提供了类似popen(3)的三向数据流处理(stdin/stdout/stderr)的功能。 它能够以完全非阻塞的方式与子进程的stdinstdoutstderr以流式传递数据。

child_process模块非常有用,我们可以使用它来执行外部命令、实现进程间的通讯等。

如,可以使用child_process.spawn()来生成一个外部进程,并在其它执行Shell命令:

const spawn = require('child_process').spawn;
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.log(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

如上所以,可以通过生成的子进程的stdinstdoutstderr属性来向子进程输数据、监听输入及错误。

child_process.spawn()还有一个同步版本的方法child_process.spawnSync()child_process中还有一些其它的常用方法:

  • child_process.exec(): 生成一个Shell并在其中执行命令,其回调函数形式为function (error, stdout, stderr)
  • child_process.execFile(): 类似child_process.exec(),但会执行一个文件中的命令
  • child_process.fork(): 生成一个Node子进程

Class: ChildProcess

ChildProcesschild_process中唯一一个类,它是一个EventEmitter

子进程中有三个之关联的流:child.stdinchild.stdoutchild.stderr。它们可以共享父进程的stdio 流,也可以作为独立的被导流的流对象。

ChildProcess不能直接创建实例,需要通过spawn()fork()来实例化

可以通过child.send()实现进程间的通讯:

const child = require('child_process').fork('child.js');

// 打开服务器并发送处理句柄
const server = require('net').createServer();
server.on('connection', (socket) => {
  socket.end('handled by parent');
});
server.listen(1337, () => {
  child.send('server', server);
});

进程间通讯请参考:Node.js多进程的实现及进程间的通讯


4.4 cluster - 集群

Node.js是线程运行的语言,其每个实例都运行在单个线程中。这并不能发恢多核服务器的处理能力,这时我们可以借助cluster模块来启动一个进程集群来处理负载。

通过cluster模块可以很简单的创建子进程,并共享服务器的端口:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // 工作进程会共享任何 TCP 连接
  // 在本例中是一个 HTTP 服务器
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);
}

工作原理

在多进程中,有“主进程”和“工作进程”两种角色。工作进程通过child_process.fork方法派生,主从进程间可以通过 IPC(进程间通讯)实现进程间的通讯并互相传递服务器句柄。

集群模块支持两种传入连接的分配方式:

  • 第一种(除 Windows 外所有平台的缺省方式)为循环方式:主进程监听一个端口,接受新连接以轮询的方式分配给工作进程,并且其内建了一些处理机制来避免单个工作进程的超载。
  • 第二种方式是:主进程建立监听嵌套字,并将它发送给感兴趣的工作进程,由工作进程直接接受传入连接。

server.listen()方法会将大部分工作交给主进程,在这种情况下,一个普通进程和在集群中工作的进程会有以下三种情况:

  1. server.listen({fd: 7})由于消息被传递到主进程,父进程中的文件描述符7会被监听,并且句柄会被传递给工作进程,而不是监听工作进程中文件描述符7所引用的东西。
  2. server.listen(handle)明确监听一个句柄会使得工作进程使用所提供的句柄,而不是与主进程通讯
  3. server.listen(0)通常,这会让服务器监听一个随机端口。而在集群中,各工作进程每次listen(0)都会得到同样的“随机”端口。即,使用第一次随机生成的端口。

注意:工作进程间并没有状态的共享,这样你在设计程序时(如:会话、登录等),不能依赖内存对象。这时你可能需要借助Redis等外部存储,持久化存储内存中的会话信息。

Class: Worker

Worker表示由cluster.fork()派生的工作进程对象,该对象包含了工作进程的所有公开信息和方法。可通过在主进程中通过cluster.workers 或工作进程中的cluster.worker获取类实例。它是一个EventEmitter,如:会在断开连接时收到'disconnect'事件、会在工作进程退出时收到'exit'事件、而在收到主进程消息时会收到'message'事件。

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {

  // Keep track of http requests
  var numReqs = 0;
  setInterval(() => {
    console.log('numReqs =', numReqs);
  }, 1000);

  // 统计请求数
  function messageHandler(msg) {
    if (msg.cmd && msg.cmd == 'notifyRequest') {
      numReqs += 1;
    }
  }

  // 启动工作进程并启动请求监听
  const numCPUs = require('os').cpus().length;
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  Object.keys(cluster.workers).forEach((id) => {
    cluster.workers[id].on('message', messageHandler);
  });

} else {

  // 工作进程是一个 HTTP服务器
  http.Server((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');

    // 通过主进程收到请求
    process.send({ cmd: 'notifyRequest' });
  }).listen(8000);
}

Worker对象中还有一些方法,使用较多的是:

worker.send(message[, sendHandle][, callback])

该方法用于向主进程发送消息:

if (cluster.isMaster) {
  var worker = cluster.fork();
  worker.send('hi there');

} else if (cluster.isWorker) {
  process.on('message', (msg) => {
    process.send(msg);
  });
}

Express中使用集群请参考:cluster模块与Express集群


5. 加密、编/解码、压缩等

5.1 crypto - OpenSSL加密功能

crypto模块提供了加密相关的功能,它封装了OpenSSL中的hash、HMAC、cipher、decipher、sign和verify相关功能。httpstls中安全凭证相关方法就由此模块提供。

Class: Certificate - SPKAC数据处理

SPKAC是证书签名请求机制,最初由网景公司制定,现已指定为HTML5的keygen元素。

crypto模块提供了用于SPKAC数据的Certificate类,其最常使用的地方是用于HTML5<keygen>元素的输出。Node.js内部使用OpenSSL SPKAC实现。

可以通过new关键字,或直接调用crypto.Certificate()创建该类的实例:

const crypto = require('crypto');

const cert1 = new crypto.Certificate();
const cert2 = crypto.Certificate();

certificate.exportChallenge(spkac) - 导出口令

spkac的数据结构为一个公钥和一个口令。可以通过certificate.exportChallenge(spkac)方法导出口令部分;certificate.exportPublicKey(spkac)方法导出口令部分。其参数可以是一个Buffer或是一个字符串:

const cert = require('crypto').Certificate();
const spkac = getSpkacSomehow();
const challenge = cert.exportChallenge(spkac);
console.log(challenge.toString('utf8')); // 口令的 UTF8 字符串
const publicKey = cert.exportPublicKey(spkac);
console.log(publicKey); // 公钥为 <Buffer ...>

验证spkac数据是否有效使用certificate.verifySpkac(spkac)方法:

const cert = require('crypto').Certificate();
const spkac = getSpkacSomehow();
console.log(cert.verifySpkac(new Buffer(spkac)));
  // Prints true or false

Class: Cipher - 数据编码

Cipher对象用于编码数据。它有以下两种用法:

  • 做为一个可读或可写,在写入端写入未加密数据而在可读端生成加密数据
  • 使用cipher.update()cipher.final()生成加密数据

Cipher可以使用crypto.createCipher()crypto.createCipheriv()两种方式实例化,同样支持使用或不使用new关键字。

做为流使用Cipher

const crypto = require('crypto');
const cipher = crypto.createCipher('aes192', 'a password');

var encrypted = '';
cipher.on('readable', () => {
  var data = cipher.read();
  if (data)
    encrypted += data.toString('hex');
});
cipher.on('end', () => {
  console.log(encrypted);
  // Prints: ca981be48e90867604588e75d04feabb63cc007a8f8ad89b10616ed84d815504
});

cipher.write('some clear text data');
cipher.end();

使用Cipher及流转接:

const crypto = require('crypto');
const fs = require('fs');
const cipher = crypto.createCipher('aes192', 'a password');

const input = fs.createReadStream('test.js');
const output = fs.createWriteStream('test.enc');

input.pipe(cipher).pipe(output);

使用cipher.update()cipher.final()方法:

const crypto = require('crypto');
const cipher = crypto.createCipher('aes192', 'a password');

var encrypted = cipher.update('some clear text data', 'utf8', 'hex');
encrypted += cipher.final('hex');
console.log(encrypted);
  // Prints: ca981be48e90867604588e75d04feabb63cc007a8f8ad89b10616ed84d815504

Class: Decipher - 数据解码

Decipher实例用于数据解码,它有以下两种使用方式:

  • 做为一个可读或可写,在写入端写入密数据而在可读端生成未加密数据
  • 使用decipher.update()decipher.final()生成未加密数据

Decipher可以使用crypto.createDecipher()crypto.createDecipheriv()两种方式实例化,同样支持使用或不使用new关键字。

如:做为流使用Decipher

const crypto = require('crypto');
const decipher = crypto.createDecipher('aes192', 'a password');

var decrypted = '';
decipher.on('readable', () => {
  var data = decipher.read();
  if (data)
  decrypted += data.toString('utf8');
});
decipher.on('end', () => {
  console.log(decrypted);
  // Prints: some clear text data
});

var encrypted = 'ca981be48e90867604588e75d04feabb63cc007a8f8ad89b10616ed84d815504';
decipher.write(encrypted, 'hex');
decipher.end();

使用Decipher及流转接:

const crypto = require('crypto');
const fs = require('fs');
const decipher = crypto.createDecipher('aes192', 'a password');

const input = fs.createReadStream('test.enc');
const output = fs.createWriteStream('test.js');

input.pipe(decipher).pipe(output);

使用decipher.update()decipher.final()方法:

const crypto = require('crypto');
const decipher = crypto.createDecipher('aes192', 'a password');

var encrypted = 'ca981be48e90867604588e75d04feabb63cc007a8f8ad89b10616ed84d815504';
var decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
console.log(decrypted);
  // Prints: some clear text data

Class: DiffieHellman - Diffie-Hellman交换密钥

DiffieHellman是一个创建Diffie-Hellman交换密钥的工具类。

创建一个DiffieHellman实例,可以使用crypto.createDiffieHellman()方法:

const crypto = require('crypto');
const assert = require('assert');

// Generate Alice's keys...
const alice = crypto.createDiffieHellman(2048);
const alice_key = alice.generateKeys();

// Generate Bob's keys...
const bob = crypto.createDiffieHellman(alice.getPrime(), alice.getGenerator());
const bob_key = bob.generateKeys();

// Exchange and generate the secret...
const alice_secret = alice.computeSecret(bob_key);
const bob_secret = bob.computeSecret(alice_key);

// OK
assert.equal(alice_secret.toString('hex'), bob_secret.toString('hex'));

Class: ECDH

ECDH用于生成椭圆曲线Diffie-Hellman(ECDH)交换密钥。

可以使用crypto.createECDH()创建该类的实例:

const crypto = require('crypto');
const assert = require('assert');

// Generate Alice's keys...
const alice = crypto.createECDH('secp521r1');
const alice_key = alice.generateKeys();

// Generate Bob's keys...
const bob = crypto.createECDH('secp521r1');
const bob_key = bob.generateKeys();

// Exchange and generate the secret...
const alice_secret = alice.computeSecret(bob_key);
const bob_secret = bob.computeSecret(alice_key);

assert(alice_secret, bob_secret);
  // OK

Class: Hash - 哈希摘要

Hash对象是一个用于计算数据哈希摘要的工具集,它有以下两种使用方式:

  • 做为一个可读或可写,在写入端写入数据而在可读端生成摘要数据
  • 使用hash.update()hash.digest()生成摘要数据

可以使用crypto.createHash()创建该类的实例。

如,做为流对象使用:

const crypto = require('crypto');
const hash = crypto.createHash('sha256');

hash.on('readable', () => {
  var data = hash.read();
  if (data)
    console.log(data.toString('hex'));
    // Prints:
    //   6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50
});

hash.write('some data to hash');
hash.end();

在转接流中使用:

const crypto = require('crypto');
const fs = require('fs');
const hash = crypto.createHash('sha256');

const input = fs.createReadStream('test.js');
input.pipe(hash).pipe(process.stdout);

使用hash.update()hash.digest()方法:

const crypto = require('crypto');
const hash = crypto.createHash('sha256');

hash.update('some data to hash');
console.log(hash.digest('hex'));
  // Prints:
  //   6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50

Class: Hmac -

Hmac用于创建HMAC摘要,它有两种使用方式:

  • 做为一个可读或可写,在写入端写入数据而在可读端生成HMAC摘要数据
  • 使用hmac.update()hmac.digest()生成HMAC摘要数据

如,做为流使用:

const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'a secret');

hmac.on('readable', () => {
  var data = hmac.read();
  if (data)
    console.log(data.toString('hex'));
    // Prints:
    //   7fd04df92f636fd450bc841c9418e5825c17f33ad9c87c518115a45971f7f77e
});

hmac.write('some data to hash');
hmac.end();

在流转接中使用:

const crypto = require('crypto');
const fs = require('fs');
const hmac = crypto.createHmac('sha256', 'a secret');

const input = fs.createReadStream('test.js');
input.pipe(hmac).pipe(process.stdout);

使用hmac.update()hmac.digest()方法:

const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'a secret');

hmac.update('some data to hash');
console.log(hmac.digest('hex'));
  // Prints:
  //   7fd04df92f636fd450bc841c9418e5825c17f33ad9c87c518115a45971f7f77e

Class: Sign - 生成数字签名

Sign用生成数字签名,它有以下两种使用方式:

  • 做为一个可写,在写入端写入要签名的数据并且sign.sign()生成签名并返回
  • 使用sign.update()sign.sign()生成签名

如,在流对象中使用:

const crypto = require('crypto');
const sign = crypto.createSign('RSA-SHA256');

sign.write('some data to sign');
sign.end();

const private_key = getPrivateKeySomehow();
console.log(sign.sign(private_key, 'hex'));
  // Prints the calculated signature

使用sign.update()sign.sign()方法:

const crypto = require('crypto');
const sign = crypto.createSign('RSA-SHA256');

sign.update('some data to sign');

const private_key = getPrivateKeySomehow();
console.log(sign.sign(private_key, 'hex'));
  // Prints the calculated signature

Class: Verify - 验证签名

Verify用于签名验证,其有以下两种使用方式:

  • 做为一个可写,写入的数据是用来验证所提供的签名
  • 使用verify.update()verify.verify()验证签名

如,做为流使用:

const crypto = require('crypto');
const verify = crypto.createVerify('RSA-SHA256');

verify.write('some data to sign');
verify.end();

const public_key = getPublicKeySomehow();
const signature = getSignatureToVerify();
console.log(sign.verify(public_key, signature));
  // Prints true or false

使用verify.update()verify.verify()验证:

const crypto = require('crypto');
const verify = crypto.createVerify('RSA-SHA256');

verify.update('some data to sign');

const public_key = getPublicKeySomehow();
const signature = getSignatureToVerify();
console.log(verify.verify(public_key, signature));
  // Prints true or false


5.2 string_decoder - Buffer解码

string_decoder模块用于将Buffer解码为字符串,它是buffer.toString()是一个便捷接口,提供对utf8编码的支持。

Class: StringDecoder

可以通过构造函数创建StringDecoder类的实例,实例化时可以同时指定编码方式,默认编码为'utf8'

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

const cent = new Buffer([0xC2, 0xA2]);
console.log(decoder.write(cent));

const euro = new Buffer([0xE2, 0x82, 0xAC]);
console.log(decoder.write(euro));

5.3 zlib - 压缩/解压缩

zlib用于流压缩,它提供了对Gzip/Gunzip、Deflate/Inflate、和DeflateRaw/InflateRaw类的绑定,这些类具体有相同的选项,其本身也是一个

压缩/解压流时可以通过.pipe()转接实现,可以将一个fs.ReadStream转接到zlib流再转接到一个fs.WriteStream

const gzip = zlib.createGzip();
const fs = require('fs');
const inp = fs.createReadStream('input.txt');
const out = fs.createWriteStream('input.txt.gz');

inp.pipe(gzip).pipe(out);

压缩/解压数据可以通过便捷方法完成:

const input = '.................................';
zlib.deflate(input, (err, buffer) => {
  if (!err) {
    console.log(buffer.toString('base64'));
  } else {
    // handle error
  }
});

const buffer = new Buffer('eJzT0yMAAGTvBe8=', 'base64');
zlib.unzip(buffer, (err, buffer) => {
  if (!err) {
    console.log(buffer.toString());
  } else {
    // handle error
  }
});

在HTTP客户端或服务器中,可以通过accept-encoding请求头或content-encoding响应头判断数据编码,再根据编码类型进行数据的解码。

在HTTP客户端中使用:

const zlib = require('zlib');
const http = require('http');
const fs = require('fs');
const request = http.get({ host: 'izs.me',
                         path: '/',
                         port: 80,
                         headers: { 'accept-encoding': 'gzip,deflate' } });
request.on('response', (response) => {
  var output = fs.createWriteStream('izs.me_index.html');

  switch (response.headers['content-encoding']) {
    // or, just use zlib.createUnzip() to handle both cases
    case 'gzip':
      response.pipe(zlib.createGunzip()).pipe(output);
      break;
    case 'deflate':
      response.pipe(zlib.createInflate()).pipe(output);
      break;
    default:
      response.pipe(output);
      break;
  }
});

在服务器中使用:

const zlib = require('zlib');
const http = require('http');
const fs = require('fs');
http.createServer((request, response) => {
  var raw = fs.createReadStream('index.html');
  var acceptEncoding = request.headers['accept-encoding'];
  if (!acceptEncoding) {
    acceptEncoding = '';
  }

  // Note: this is not a conformant accept-encoding parser.
  // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
  if (acceptEncoding.match(/\bdeflate\b/)) {
    response.writeHead(200, { 'content-encoding': 'deflate' });
    raw.pipe(zlib.createDeflate()).pipe(response);
  } else if (acceptEncoding.match(/\bgzip\b/)) {
    response.writeHead(200, { 'content-encoding': 'gzip' });
    raw.pipe(zlib.createGzip()).pipe(response);
  } else {
    response.writeHead(200, {});
    raw.pipe(response);
  }
}).listen(1337);


6. 辅助模块URL处理、工具类等

6.1 url - URL解析

url是Node.js提供的用于URL解析的工具模块,可以将URL中的各部分解析为一个对象,或将指定的字符串参数转化为一个URL。

该模块包含以下三个方法:

  • url.parse(urlStr[, parseQueryString][, slashesDenoteHost]) - 将URL字符串解析为对象
  • url.format(urlObj) - 将对象格式化为URL字符串
  • url.resolve(from, to) - URL路径处理

详细使用请参考:Node.js URL模块的使用


6.2 util - 工具类

util模块中有一些常用的工具函数,其设计目的是满足Node内部API的使用。但其内部的一些方法,在我们日常编程中也很有用。下面是这个模块中一些常用方法:

util.format(format[, ...]) - 格式化字符串

根据第一个参数格式化字符串,类似printf格式化输出。其第一个参数中可能包含零个或多个占位符,支持的占位符有:

  • %s - 字符串
  • %d - 数字 (整型和浮点型)
  • %j - JSON.如果这个参数包含循环对象的引用,将会被替换成字符串 '[Circular]'
  • %% - 单独的('%')符号。不会消耗一个参数
util.format('%s:%s', 'foo'); // 'foo:%s'

util.inherits(constructor, superConstructor) - 实现继承

通过构造函数,继承对象上的方法。会从父类的构造函数superConstructor创建一个新对象constructor

实现继承后,可以通过constructor.super_访问父类中的方法:

const util = require('util');
const EventEmitter = require('events');

function MyStream() {
    EventEmitter.call(this);
}

util.inherits(MyStream, EventEmitter);

MyStream.prototype.write = function(data) {
    this.emit('data', data);
}

var stream = new MyStream();

console.log(stream instanceof EventEmitter); // true
console.log(MyStream.super_ === EventEmitter); // true

stream.on('data', (data) => {
  console.log(`Received data: "${data}"`);
})
stream.write('It works!'); // Received data: "It works!"

util.inspect(object[, options]) - 序列化对象

将对象以字符串的形式显示,可以通过showHidden参数设置是否显示隐藏属性、通过depth设置显示深度:

const util = require('util');

console.log(util.inspect(util, { showHidden: true, depth: null }));


7. 模块、全局对象

7.1 global - 全局对象

全局对象不需要require引用即可在所有模块中使用。Node.js中有些还有对象不是全局作用域,而是模块作用域。这些全局对象中,除了Node.js中单独定义的,还有一些是JavaScript语言本的全局对象。

Node.js中有以下全局对象:

global - 全局命令空间对象

在浏览器环境中,通过var关键字定义的变更就是全局变量。在Node中有所有不同,通过var定义的变量只属性那个模块。要添加全局变量,需要将其添加到global命名空间中。

自定义全局变量请参考:Node.js自定义Global全局对象

Node.js中的全局对象有:

  • Class: Buffer - 缓存类

    Buffer类被实现为一个全局对象,它用于处理二进制数据。详见:Buffer

  • __dirname - 脚本目录

    当前执行脚本所在目录的目录名

    如:在/Users/mjr目录下执行node example.js

    console.log(__dirname);
    // /Users/mjr
  • __filename - 脚本路径

    当前执行脚本完整路径

    如:在/Users/mjr目录下执行node example.js

    console.log(__filename);
    // /Users/mjr/example.js
  • timers - 定时器

    Node.js中的定义器函数来自于JavaScript,分别是:clearImmediate()clearInterval()clearTimeout()setImmediate()setInterval()setTimeout()。详见:timers

  • console - 控制台

    控制台对象,详见:console

  • exports - 模块导出

    exports是对module.exports的一个引用。详细说明及二者的区别请参考:module

  • require() - 模块引用

    模块引用,引用后可访问通过exports导出的功能,详见:module


7.2 module - 模块系统

Node.js有一个简单的模块加载系统。在Node.js中,文件和模块是一一对应的关系,一个文件就是一个模块。

如,在foo.js中加载同目录下的circle.js

foo.js内容:

const circle = require('./circle.js');
console.log( `半径为 4 的圆面积为 ${circle.area(4)}`);

circle.js内容:

const PI = Math.PI;

exports.area = (r) => PI * r * r;

exports.circumference = (r) => 2 * PI * r;

circle.js中通过exports导出了area()circumference两个方法,这两个方法可以其它模块中调用。

exports是对module.exports的一个简单引用。如果你需要将模块导出为一个函数(如:构造函数),或者想导出一个完整的出口对象而不是做为属性导出,这时应该使用module.exports

如,bar.js引用做为构造函数导出的square

const square = require('./square.js');
var mySquare = square(2);
console.log(`The area of my square is ${mySquare.area()}`);

square.js导出:

module.exports = (width) => {
  return {
    area: () => width * width
  };
}

更多关于模块系统的介绍请参考:Node.js Modules模块系统


7.3 console - 控制台对象

console对象实现了一个简单的调试控制台功能,它与Web浏览所提供的控制来功能类似。

这个模块提供了两种使用方式:

  • Console类方法,如:console.log()console.error()console.warn()可以写入任何Node.js流
  • 全局类实例console可以配置写入到stdoutstderr

使用全局对象console

console.log('hello world'); 
  // hello world, to stdout
console.log('hello %s', 'world');
  // hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
  // [Error: Whoops, something bad happened], to stderr

const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
  // Danger Will Robinson! Danger!, to stderr

使用Console类:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);

myConsole.log('hello world');
  // hello world, to out
myConsole.log('hello %s', 'world');
  // hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
  // [Error: Whoops, something bad happened], to err

const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
  // Danger Will Robinson! Danger!, to err

Class: Console

Console类可用于创建一个简单的日志记录器,并配置到输出流中。可以通过require('console').Consoleconsole.Console获取该类的引用。

const Console = require('console').Console;
const Console = console.Console;

该类构造函数结构如下:

new Console(stdout[, stderr])
  • stdout - 标准输出流
  • stderr - 可选,标准错误流

Console的构造函数可传入一个标准输出流参数stdout,及标准错误流参数stderr

通过自定义Console实例,可以实现不同输出流记录到不同文件的功能。如,实现一个普通(标准)输出及错误输出的日志打印功能:

const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');
// 自定义日志打印
const logger = new Console(output, errorOutput);
// 用法与全局console对象一样
var count = 5;
// 标准输出stdout会记录到 './stdout.log' 文件中
logger.log('count: %d', count);
// stdout.log: count 5


// 标准错误stderr会记录到 './stderr.log' 文件中
logger.error('a error');
// stderr.log: a error

而全局的consle对象,是一个将输出分别写入到process.stdoutprocess.stderrConsole类的实例:

new Console(process.stdout, process.stderr);

console实例中常用的方法

下面是一些常用的console实例(对象)方法。其可用于全局console对象或自定义的console对象:

  • console.assert(expression[, message][, ...]) - 断言输出,expression断言失败则抛出AssertionError异常
    console.assert(true, 'does nothing');
      // OK
    console.assert(false, 'Whoops %s', 'didn\'t work');
      // AssertionError: Whoops didn't work
  • console.dir(obj[, options]) - 调用util.inspect或自定义格式化方法,将obj打印到到标准输出(stdout)中
  • console.error([data][, ...]) - 写入到标准错误(stderr)中
    const code = 5;
    console.error('error #%d', code);
      // Prints: error #5, to stderr
    console.error('error', code);
      // Prints: error 5, to stderr
  • console.info([data][, ...]) - console.log()的别名方法
  • console.log([data][, ...]) - 写到标准输出(stdout)中
  • console.time(label) - 标记时间点
  • console.timeEnd(label) - 结束计时器,记录输出
    console.time('100-elements');
    for (var i = 0; i < 100; i++) {
      ;
    }
    console.timeEnd('100-elements');
    // prints 100-elements: 262ms
  • console.trace(label) - 打印当前位置的栈跟踪到stderr
  • console.warn([data][, ...]) - console.error()的别名方法


7.4 timers - 定时器

Node.js中的定时器与JavaScript中定时器实现类似,所有的定时器都是全局变量。

setTimeout(callback, delay[, arg][, ...]) - 超时调用

经过 delay 毫秒后,执行一次 callback。返回一个可能被clearTimeout() 用到的 timeoutId。其后为传递回调函数的参数。

注意:回调并不会在准确的delay后执行,Node.js不会保证触发时间的精确性和顺序,而是会在近可能准备的时间点调用。

clearTimeout(timeoutId) - 清除setTimeout()创建的超时调用

setInterval(callback, delay, [arg], [...]) - 定时调用

每隔 delay 毫秒,执行一次 callback。返回一个可能被clearInterval() 用到的 intervalId。其后为传递回调函数的参数。

clearInterval(intervalId) - 清除setInterval()创建的定时调用

setImmediate(callback, [arg], [...]) - I/O 事件回调之后

在所有I/O 事件回调之后,setTimeoutsetInterval()之前调用。返回一个可能被clearImmediate() 用到的 immediateId。其后为传递回调函数的参数。

clearInterval(intervalId) - 停止一个immediate的触发