核心(原生)模块是指那些随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: '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.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://itbilu.com', (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
或LE
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.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 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 事件回调之后,setTimeout
和 setInterval()
之前调用。返回一个可能被clearImmediate()
用到的 immediateId
。其后为传递回调函数的参数。
clearInterval(intervalId) - 停止一个immediate
的触发