核心(原生)模块是指那些随Node.js安装而安装的模块,这些模块在Node.js源代码编译时被编译成二进制执行文件。相比文件模块,核心(原生)模块的加载速度更快。核心(原生)模块提供了JavaScript语言之外处理能力,如:网络处理相关模块http、net、dgram,文件及流处理fs、stream,二进制处理模块buffer,系统与进程os、process……
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.Server和net.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.Server是net.Server的子类并与其有相同的方法。不同于原始TCP请求,tls.Server服务器会使用TLS或SSL加密连接。
该类实例由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.Socket是dgram模块中的唯一一个类,该类实现了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: 'niefengjun.cn',
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.Server与net.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.Server或http.ClientRequest创建。在http.Server中会被传入'request'事件的第一个参数中,表示服务器收到的用户请求对象,可以通过该对象接收用户请求中的数据提交、文件上传等数据;在http.ClientRequest中,会被传入到'response'事件的第一个参数中,表示服务器请求对象,可以通过该对象访问来服务器的响应状态、响应头、及响应数据等。需要注意:<只存在于用户用户请求与服务器响应对象并不完全一致,如:code>message.url只存在于用户请求对象中;而message.statusCode、message.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://niefengjun.cn', (response) => {
response.pipe(res);
})
});
2.5 https
HTTPS是在TLS/SSL之上构建的HTTP协议,Node.js中做为一个单独的模块实现。
Class: https.Server
https.Server是tls.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通常用来表示经过编码(如:UTF8、UCS2、Base64、Hex等)的字符序列。可以通过指定编码方式,在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表示双向流,它实现了Readable和Writable两者的接口。
Node.js中的双向流有:
Class: stream.Transform
Transform(转换)流是一种输出由输入计算所得的双工流。它们同时实现了Readable和Writable接口。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');
对于异步方法来说,需要一个回调函数在操作完成时进行调用。通常情况下回调函数的第一个参数为返回的错误信息,如果异步操作执行正确,则该错误参数为null或undefined。
如果使用同步版本的方法,一旦出现错误,会以通常的抛出错误的形式返回错误。你可以用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 的字节序,可能是BE或LEos.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.platform与os.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.stderr和process.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)的功能。
它能够以完全非阻塞的方式与子进程的stdin、stdout和stderr以流式传递数据。
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}`);
});
如上所以,可以通过生成的子进程的stdin、stdout和stderr属性来向子进程输数据、监听输入及错误。
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
ChildProcess是child_process中唯一一个类,它是一个EventEmitter。
子进程中有三个之关联的流:child.stdin、child.stdout 和 child.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()方法会将大部分工作交给主进程,在这种情况下,一个普通进程和在集群中工作的进程会有以下三种情况:
server.listen({fd: 7})由于消息被传递到主进程,父进程中的文件描述符7会被监听,并且句柄会被传递给工作进程,而不是监听工作进程中文件描述符7所引用的东西。server.listen(handle)明确监听一个句柄会使得工作进程使用所提供的句柄,而不是与主进程通讯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相关功能。https和tls中安全凭证相关方法就由此模块提供。
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()。详见:timersconsole- 控制台控制台对象,详见:console
exports- 模块导出exports是对module.exports的一个引用。详细说明及二者的区别请参考:modulerequire()- 模块引用模块引用,引用后可访问通过
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可以配置写入到stdout和stderr
使用全局对象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').Console或console.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.stdout和process.stderr的Console类的实例:
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 stderrconsole.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: 262msconsole.trace(label)- 打印当前位置的栈跟踪到stderrconsole.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 事件回调之后,setTimeout 和 setInterval()之前调用。返回一个可能被clearImmediate() 用到的 immediateId。其后为传递回调函数的参数。
clearInterval(intervalId) - 停止一个immediate的触发
