NodeJS 秘籍(二)
四、构建 Web 服务器
Web 服务器是用 Node.js 构建的典型应用。这是由于 Node.js 的主要目标。Node.js 非常适合构建高度可伸缩的、事件驱动的、网络化的应用——web 服务器。
在本章中,你将学习和理解如何用 Node.js 构建一个 web 服务器。你将看到从简单的 web 服务器到在你的服务器上处理静态文件的主题。这些主题只是使 web 服务器正常工作的一部分。为了全面了解 web 服务器,因为它可以通过 Node.js 实现,您还将学习以下内容:
- 使用 HTTPS 创建安全套接字层(SSL)服务器
- 配置标题
- 管理 HTTP 状态代码
- 处理 HTTP 请求和响应
- 使用 HTTP 事件管理您的 web 服务器
4-1.设置 HTTP 服务器
问题
您需要创建一个简单的 web 服务器来通过 HTTP 提供内容。
解决办法
在 Node.js 中,web 服务器通常使用 HTTP 模块来设置。这提供了一个与 HTTP 协议交互的层。
假设您正在编写一个 web 服务器,当您连接到 web 服务器时,它将向客户端发送一条状态消息。在这个解决方案中,清单 4-1 ,这已经被简化为简单地写响应‘hello ’,然后结束响应。
清单 4-1 。简单 HTTP Web 服务器
/**
* Setting up an HTTP server
*/
var http = require('http');
var server = http.createServer(function(req, res) {
res.write('hello');
res.end();
});
server.listen(8080);
它是如何工作的
这个 web 服务器过于简化,因此您可以研究 HTTP 模块如何创建服务器。在这个解决方案中,您自然会从需要 http 模块开始。这个模块公开了一个函数http.createServer,它是服务器实际创建的地方。http.createServer方法实例化一个新的服务器对象。服务器对象接受一个requestListener回调函数。这将把响应和请求参数发送给 web 服务器的回调。
新的 web 服务器是一个 HTTP 服务器,它是从你在《??》第二章中看到的net.Server对象派生而来的。服务器还为事件、connection、request和clientError提供事件监听器。
清单 4-2 。由 createServer 实例化的服务器源
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.addListener('request', requestListener);
}
// Similar option to this. Too lazy to write my own docs.
//http://www.squid-cache.org/Doc/config/half_closed_clients/
//http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false;
this.addListener('connection', connectionListener);
this.addListener('clientError', function(err, conn) {
conn.destroy(err);
});
this.timeout = 2 * 60 * 1000;
}
util.inherits(Server, net.Server);
Server.prototype.setTimeout = function(msecs, callback) {
this.timeout = msecs;
if (callback)
this.on('timeout', callback);
};
exports.Server = Server;
您已经创建了您的 web 服务器。接下来,告诉服务器您想在哪里监听请求。这是通过server.listen完成的。server.listen函数接受一个端口以及一个可选的主机名、backlog 和一个回调。server.listen方法的回调函数将监听“listening’事件。提供主机名将告诉服务器您将在哪里监听给定端口的请求。
注
server.listen还有另外两个签名。一种替代方法是只提供一个 UNIX 路径和一个回调。这将在路径上开始一个套接字服务器。另一种方法是提供一个句柄——一个套接字或一个服务器——它将成为新的服务器。
一旦您的服务器在监听,您就可以从服务器提供您的响应。在您提供给http.createServer方法的请求监听器回调中有两个参数。这些参数表示所提供的 HTTP 请求和 HTTP 响应。在该解决方案中,您希望创建一个 web 服务器来发送对“hello”连接的响应。这是通过流式传输一个res.write(‘hello’)函数来完成的。一旦response.end()函数被调用,这将在客户端呈现。
Response.write将响应体的块作为第一个参数发送。可选的第二个参数用于设置这个块的字符编码。您可能认为响应只需要一个response.write,但这种想法是不正确的。事实上,对于每个响应,您都需要调用response.end()函数。
4-2.使用 SSL 构建 HTTPS 服务器
问题
您创建了一个 web 服务器,但是您想通过使用 SSL 加密的连接通过 HTTPS 提供内容来增加额外的安全级别。
解决办法
为了构建一个 SSL 服务器,在开始之前,您需要准备好一些东西。首先,您的客户端和服务器必须执行传输层安全性(TLS)握手。为此,您需要生成一个证书和密钥来验证您的 HTTPS 会话。这些密钥在客户端和服务器之间交换。一旦交换了密钥,验证和确认会话的过程就开始了。一旦密钥被认为是有效的,会话就像普通的 HTTP 连接一样通过 HTTPS 继续进行,只是增加了一层安全性。
从那里,您可以使用 Node.js 中的 HTTPS 模块。该模块的行为类似于 HTTP 模块,但是连接是通过 TLS/SSL 加密的。然后通过 Node.js 创建一个 HTTPS 服务器,如清单 4-3 所示。
清单 4-3 。HTTPS 服务器
/**
* HTTPS server
*/
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('privatekey.pem'),
cert: fs.readFileSync('certificate.pem')
};
https.createServer(options, function (req, res) {
res.writeHead(200);
res.write("https!\n");
res.end();
}).listen(8080);
它是如何工作的
创建 HTTPS 连接从 TLS/SSL 开始。该协议确保客户端和服务器之间的安全通信。发生这种情况是因为客户端和服务器之间存在握手,在握手过程中,服务器向客户端公开其证书和公钥。然后,当客户端发送响应时,用服务器的公钥对响应进行加密,并进行验证。如果所有数据都被评估为有效,则会话将在 HTTPS 上继续。
但是如何获得这些证书和密钥呢?在 Node.js 中,SSL/TLS 实现利用了 OpenSSL。OpenSSL 是 SSL/TLS 的开源实现。这是一个能让你轻松实现密钥和证书的协议。为了生成这样一个密钥,你需要打开你的终端并输入如清单 4-4 所示的命令。
清单 4-4 。创建 TLS/SSL 密钥和证书
$ openssl genrsa -out privatekey.pem 1024
$ openssl req -new -key privatekey.pem -out certrequest.csr
$ openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem
在 Windows 上这略有不同,因为默认情况下 Windows 不包含 OpenSSL 实现。您应该首先从http://openssl.org/related/binaries.html下载一个二进制发行版。默认情况下,这将安装到您计算机上的 C:\OpenSSL-Win32。在那里,您可以打开 PowerShell 并从 C:\OpenSSL-Win32\bin 目录运行以下内容。
PS C:\OpenSSL-Win32\bin> .\openssl.exe genrsa –out privatekey.pem 1024
PS C:\OpenSSL-Win32\bin> .\openssl.exe req –new –key .\privatekey.pem –out certrequest.csr
PS C:\OpenSSL-Win32\bin> .\openssl.exe x509 –req –in .\certrequest.csr –signkey .\privatekey.pem –out certificate.pem
一旦创建了证书和密钥,现在就可以创建安全的服务器了。这从https.createServer方法开始。这个函数类似于http.createServer方法,除了创建安全连接。这是通过一个选项对象完成的。本例中使用的选项为创建tls.Server?? 设置证书和密钥。你会在第六章中看到更多关于 SSL 和 TLS 的细节。为了实际读取密钥和证书文件的值,你使用文件系统读取它们,如第三章中所讨论的。一旦这些被读取,您就可以创建您的服务器。
清单 4-5 。HTTPS 服务器继承了 tls。计算机 Web 服务器
function Server(opts, requestListener) {
if (!(this instanceof Server)) return new Server(opts, requestListener);
if (process.features.tls_npn && !opts.NPNProtocols) {
opts.NPNProtocols = ['http/1.1', 'http/1.0'];
}
tls.Server.call(this, opts, http._connectionListener);
this.httpAllowHalfOpen = false;
if (requestListener) {
this.addListener('request', requestListener);
}
this.addListener('clientError', function(err, conn) {
conn.destroy(err);
});
this.timeout = 2 * 60 * 1000;
}
inherits(Server, tls.Server);
一旦创建了服务器,您应该能够通过 SSL 连接访问它。要测试这一点,只需旋转服务器地址,您应该会看到响应“https!”写入您的控制台。另一方面,如果您不尝试访问服务器的 HTTPS 版本,您将无法从服务器获得预期的结果。
清单 4-6 。使用 cURL 查看您的安全连接
$ curl –khttps://localhost:8080 # works
https!
$ curl http://localhost:8080 # nope
curl: (52) Empty response from the server
4-3.在您的服务器上处理请求
问题
你有一个 HTTP 或 HTTPS 服务器。该服务器需要处理传入的请求。
解决办法
当您构建 web 服务器时,您需要处理请求。请求的形式多种多样,包含的内容很快就会变成大量的数据。在处理请求时,您需要能够有效地筛选传入的数据,以便处理头、方法和 URL 参数。
在此解决方案中,您将创建一个处理请求的 web 服务器。它可能看起来与您熟悉的许多 web 服务器相似。该服务器将处理请求头,并按照您认为合适的方式处理它们。例如,如果请求标头包含“不要跟踪”指令,则不发送跟踪 cookie。
在正确处理了头之后,您可能想要解析请求 URL。这将通过处理传入路径来帮助您处理 404 和一般应用路由。除了路径之外,您还可能对随请求一起发送的查询字符串参数感兴趣。
最后,您将需要检查启动请求的请求方法。这就是 HTTP 方法,在你创建任何应用,或者一个具象状态转移(REST)应用 编程接口(API)的时候都会很有用。
清单 4-7 。处理请求
/**
* Processing Requests
*/
var http = require('http'),
url = require('url');
var server = http.createServer(function(req, res) {
//Handle headers
if (req.headers.dnt == 1) {
console.log('Do Not Track');
}
//Parse the URL
var url_parsed = url.parse(req.url, true);
//What type of request is this
if (req.method === 'GET') {
handleGetRequest(res, url_parsed);
} else if (['POST', 'PUT', 'DELETE'].indexOf(req.method) > -1) {
handleApiRequest(res, url_parsed, req.method);
} else {
res.end('Method not supported');
}
});
handleGetRequest = function(res, url_parsed) {
console.log('search: ' + url_parsed.search);
console.log('query: ' + JSON.stringify(url_parsed.query));
console.log('pathname: ' + url_parsed.pathname);
console.log('path: ' + url_parsed.path);
console.log('href: ' + url_parsed.href);
res.end('get\n');
};
handleApiRequest = function(res, url_parsed, method) {
if (url_parsed.path !== '/api') {
res.statusCode = 404;
res.end('404\n');
}
res.end(method);
};
server.listen(8080);
它是如何工作的
该解决方案中的 web 服务器是为处理请求而构建的。它通过检查服务器收到的请求周围的细节来做到这一点。这个请求实际上是一个名为http.IncomingMessage的对象。
http.IncomingMessage 继承了可读流接口。在此基础上,它构建了一些对 HTTP 消息有用的对象,如清单 4-8 所示。
清单 4-8 。http。传入消息
function IncomingMessage(socket) {
Stream.Readable.call(this);
this.socket = socket;
this.connection = socket;
this.httpVersion = null;
this.complete = false;
this.headers = {};
this.trailers = {};
this.readable = true;
this._pendings = [];
this._pendingIndex = 0;
// request (server) only
this.url = '';
this.method = null;
// response (client) only
this.statusCode = null;
this.client = this.socket;
this._consuming = false;
this._dumped = false;
}
util.inherits(IncomingMessage, Stream.Readable);
正如您从源代码中看到的,http.IncomingMessage带来了几个对您的解决方案很重要的对象或设置。首先,它带来了标题。请求头是直接反映随请求一起发送的键值对的对象。当我试图从我的 web 浏览器向这个服务器发送一个请求时,标题看起来如清单 4-9 所示。
清单 4-9 。典型的请求头
{ host: 'localhost:8080',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:20.0) Gecko/20100101 Firefox/20.0',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'en-us,en;q=0.5',
'accept-encoding': 'gzip, deflate',
dnt: '1',
connection: 'keep-alive' }
其次,在您的 web 服务器中,您需要处理request.url。这包含通过请求 URL 发送的所有信息。最简单的解析方法是利用 URL 模块。您可以告诉 URL 模块解析包含查询字符串的 request.url。
请求的第三部分对您的服务器有价值的是request.method . request.method将为您提供开始请求的 HTTP 方法。在该解决方案中,web 服务器被设置为模仿 web API。在这种情况下,API 方法的路由不完全由 URL 的路径决定,还由request.method决定。这些方法只是 HTTP 方法的字符串名称。您的解决方案以两种不同的方式处理这些不同的方法。首先,您服务一个 HTTP GET 请求;使用它将记录请求的一些细节,并响应该方法确实是一个 GET。第二,用其他方法模拟 API 路由方案。这些由一个单独的函数处理,该函数将检查以确保您请求的不仅是正确的方法,还有路径。正如你在解决方案中看到的,这些都是以这样的方式处理的,你可以卷曲每种类型来查看不同的结果,如你在清单 4-10 中看到的。
清单 4-10 。不同结果的卷曲
$ curl -X PUT http://localhost:8080/api
put
$ curl -X PUT http://localhost:8080/apis
404
$ curl -X DELETE http://localhost:8080/api
delete
$ curl -X TRACE http://localhost:8080/api
Method not supported
通过理解 Node.js 中伴随着http.request的信息,您能够利用它来构建您的 web 服务器来处理这些请求。接下来,您将看到如何从您的服务器发送响应。
4-4.从您的服务器发送响应
问题
您已经有了 web 服务器,但是现在您需要能够以响应的形式从服务器发送信息。
解决办法
服务器响应是作为请求事件的一部分发出的 Node.js EventEmitter对象。在这个解决方案中,您将利用响应对象直接将内容写入请求者。首先,您想要发送一个 HTML 文档。您可以通过创建一个响应并直接发送 HTML 内容来做到这一点。
清单 4-11 。HTML 的响应.写入
/**
* Sending a response from your server
*/
var http = require('http');
var server = http.createServer(function(req, res) {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200, 'woot');
res.write('<!doctype html>');
res.write('<html>');
res.write('<head><meta charset="utf-8"></head>');
res.write('<body>');
res.write('<h2>Hello World</h2>');
res.write('</body></html>');
res.end();
});
server.listen(8080);
现在,您可以在回复中直接提供 HTML 内容。您可能需要能够发送其他类型的内容,以便为您的应用提供可靠的解决方案。在这种情况下,您选择发送一个 JavaScript Object Notation (JSON)编码的对象,以便客户机可以从您的服务器检索信息。这在实现上是相似的,只是有一些小的变化,您将在它的工作原理一节中看到细节。
清单 4-12 。A JSON 服务器负责人
var http = require('http');
var server = http.createServer(function(req, res) {
res.setHeader('Content-Type', 'application/json');
res.writeHead(200, 'json content');
res.write('{ "wizard": "mithrandir" }');
res.end();
});
server.listen(8080);
它是如何工作的
您现在通过使用serverResponse对象从您的 web 服务器提供内容。在本解决方案中使用的这个对象带有一些有价值的功能。requestListener回调函数中的第一行是response.setHeader.``setHeader函数顾名思义就是这样做的;它设置响应的标头。这些被设置在一个名称和值对中,res.setHeader(‘Name’, ‘Value’);.
在解决方案中,您设置 Content-Type 头来定义随请求一起发送的内容的类型。您还可以设置 cookies、自定义标头参数或来自服务器的请求标头附带的任何内容。
本解决方案中使用的另一种设置响应头的方法是response.writeHead方法。这种方法不会将您的头创建限制为单个名称和值对。此方法最多需要三个参数。第一个参数是必需的,它为响应设置 HTTP 状态代码。然后,您可以选择设置与状态代码描述相对应的自定义描述或原因短语。这可以是您希望的任何原因短语,与 HTTP 标准描述不同。
清单 4-13 。Node.js 中的 HTTP 原因短语覆盖
if (typeof arguments[1] == 'string') {
reasonPhrase = arguments[1];
headerIndex = 2;
} else {
reasonPhrase = STATUS_CODES[statusCode] || 'unknown';
headerIndex = 1;
}
第三个参数实际上是一个 header 对象,它将接受名称和值对,而不仅仅是单个的名称-值对,作为一个完整的对象。为了重构上面 JSON serverResponse的解决方案,您可以简单地调用一次response.writeHead来获得相同的结果。
清单 4-14 。在 response.writeHead 调用中组合 HTTP 状态代码、原因短语和标头
res.writeHead(200, ‘json content’, {
‘Content-Type’: ‘application/json’});
然后,使用response.write向客户机发送响应的主体。这个函数将接受一个表示响应体块的字符串。response.write的第二个参数是设置响应的编码,默认为 utf8。response.write不要求您已经通过前面提到的方法设置了标题。如果没有显式设置这个头,那么response.write方法将隐式定义一个状态码为 200 的头。然后,write 方法确保标头已经发送。如果标头尚未发送,则它们将与数据的初始写入一起发送到客户端。
清单 4-15 。如果头还没有发送,就和第一个块一起发送
if (!this._headerSent) {
if (typeof data === 'string') {
data = this._header + data;
} else {
this.output.unshift(this._header);
this.outputEncodings.unshift('ascii');
}
this._headerSent = true;
}
return this._writeRaw(data, encoding);
现在,您已经看到并执行了从 web 服务器发送响应的方法。这些选项只是 HTTP 响应的一部分。可用物品的完整列表如表 4-1 所示。
表 4-1 。HTTP serverResponse 方法
| 方法 | 描述 |
|---|---|
| response.addTrailers(标题) | 向响应中添加 HTTP 尾随标头(标头,但在消息的末尾)。仅当分块编码用于响应时,才会发出尾部;如果不是(例如,如果请求是 HTTP/1.0),它们将被无声地丢弃。 |
| response . end([数据],[编码]) | 向服务器发出信号,表明所有响应标头和正文都已发送;服务器应该认为消息是完整的。必须对每个响应调用方法 response.end(),。 |
| response.getHeader(名称) | 读出一个已经排队但没有发送到客户端的头。请注意,该名称不区分大小写。这只能在头被隐式刷新之前调用。 |
| response.headersSent | 布尔值(只读)。如果发送了头,则为真,否则为假。 |
| response.removeHeader(名称) | 移除排队等待隐式发送的标头。 |
| 响应.发送日期 | 如果为真,将自动生成日期标题,如果标题中没有日期标题,则在响应中发送。默认为真。 |
| response.setHeader(名称,值) | 为隐式标头设置单个标头值。如果该标题已经存在于待发送标题中,其值将被替换。如果需要发送多个同名的头,请在这里使用字符串数组。 |
| response.setTimeout(毫秒,回调) | 将套接字的超时值设置为毫秒。如果提供了回调,那么它将被添加为响应对象上的“超时”事件的侦听器。 |
| response.statusCode 代码 | 当使用隐式标头(不显式调用 response.writeHead()时),此属性控制标头刷新时将发送到客户端的状态代码。 |
| response.write(块,[编码]) | 发送一大块响应正文。可以多次调用此方法来提供身体的连续部分。 |
| writeContinue() | 向客户端发送 HTTP/1.1 100 Continue 消息,指示应该发送请求正文。 |
| writeHead(状态代码,[原因短语],[标题]) | 向请求发送响应标头。 |
4-5.处理标题和状态代码
问题
在构建 Node.js web 应用时,您需要能够正确地传递和处理头信息和 HTTP 状态代码。
解决办法
在为这个解决方案创建的场景中,您可以想象这样一种情况,您需要为您的 web 应用提供特定类型的文件。当您正在构建一个希望发布到托管 web 应用商店或市场(如 Chrome 或 Firefox OS 应用)的 web 应用时,可能会出现这种情况。
在这种情况下,您可以提供一个应用清单文件。这通常是 JSON 文件的形式,它设置应用的细节,以便使它可以安装在托管平台上。这需要特定的头类型,以便主机平台将该文件识别为清单。因此,在这个解决方案中,您将操作标题来适当地表示内容类型,并为您的应用处理正确的状态代码。
清单 4-16 。处理标题和状态代码
/**
* Headers and status codes
*/
var http = require('http');
url = require('url');
var server = http.createServer(function(req, res) {
if (req.headers) {
console.log('request headers', req.headers);
}
var parsedUrl = url.parse(req.url);
if (parsedUrl.path === '/manifest.webapp' && req.method === ‘GET’) {
// serving an application manifest file type
res.writeHead(200, { 'Content-Type' : 'application/x-web-app-manifest+json' });
res.write('{ "name" : "App" }');
res.write( '"description": "My elevator pitch goes here",');
res.write('"launch_path": "/",');
res.write('"icons": {');
res.write('"128": "/img/icon-128.png" },');
res.write('"developer": {');
res.write(' "name": "Your name or organization",');
res.write(' "url": "http://your-homepage-here.org" },');
res.write('"default_locale": "en" }');
res.end();
} else if (parsedUrl.path !== '/') {
res.statusCode = 404;
res.end(http.STATUS_CODES[res.statusCode]);
} else {
res.writeHead(200, { 'Content-Type': 'text/html'});
res.end('<h2>normalContent</h2>');
}
});
server.listen(8080);
它是如何工作的
该解决方案旨在做两件事。首先,它被设计为从应用的根提供静态 HTML,url path = '/'。第二,它被设计成服务于 webapp.manifest 文件,或者您将编写来打包您的应用以在应用市场上托管的内容。为了正确地做到这一点,您需要控制标题和状态代码。
状态代码很重要,因为它们提供了有关您对客户端的响应状态的信息。状态代码属于五个类别中的一个,这五个类别由每个以 100 开始的整数块分隔。100 范围内的状态代码是信息代码;200 范围内的代码是代表成功的代码;300 个范围代码表示重定向。对于客户端错误,错误由 400 范围内的状态代码表示,对于服务器错误,错误由 500 范围内的状态代码表示。
在此解决方案中,您的应用被设计为仅提供来自 web 应用根的内容,或者清单文件本身。服务器请求的其他路径将导致 404 状态代码。此状态代码是一个客户端错误,指示找不到路径。
清单 4-17 。设置 404 未找到状态码
if (parsedUrl.path !== '/') {
res.statusCode = 404;
res.end(http.STATUS_CODES[res.statusCode]);
}
这个响应是用通过response.end方法传递的数据编写的。这利用了http.STATUS_CODES对象,该对象将为传递的response.statusCode找到相应的状态代码原因描述。
您的目标 URL 都将返回 200 或“OK”状态代码。第一个是 web 应用的根。除了状态代码,您还希望将这个根目录作为 HTML 文档提供。这不仅由您提供的内容控制,也由标题控制。
当从任何类型的 web 服务器提供内容时,控制头是很重要的,因为头指示客户端如何处理内容,或者一旦内容被处理后如何处理。这方面的例子有内容类型头,指示请求如何提供内容;Cache-Control 头,它告诉客户端如何处理内容的缓存;和指示请求长度的 Content-Length。这些只是可以在 Node.js 的请求中设置的三个标准和非标准头名称。
在此解决方案中,当您发送应用清单文件时,您发送了一个自定义的非标准头:{ ' Content-Type ':' application/x-we b-app-manifest+JSON ' }。这个头表明内容属于应用清单类型,应该是一个 JSON 文件。如果您在应用的根目录下,响应会提供一个“text/html”的内容类型头,您可能会认为这是一个 html 文档。当然,您可以根据需要向这些响应添加任何额外的头,但是知道某些响应的内容类型(比如清单文件)需要精确是很重要的。
4-6.创建 HTTP 客户端
问题
您希望创建一个 Node.js 应用作为 HTTP 客户端。
解决办法
从 Node.js 应用中创建 HTTP 客户机就像创建 HTTP 服务器一样简单。在此解决方案中,您将从为客户端设置选项开始。这些选项告诉您的应用将请求发送到哪里,以及通过什么方式获取请求。您这样做是为了能够与在 4-3 节中为您的应用创建的 REST API 进行通信。这将解析一组参数,确定发送给 API 的方法和路径,然后处理http.request。
清单 4-18 。HTTP 客户端
/*
* Creating an HTTP client
*/
var http = require('http'),
args = process.argv.slice(2);
//Set defaults
var clientOptions = {
host: 'localhost',
// hostname:'nodejs.org',
port: '8080',
path: '/',
method: 'GET'
};
args.forEach(function(arg) {
switch(arg) {
case 'GET':
clientOptions.method = 'GET';
break;
case 'SUBMIT':
case 'POST':
clientOptions.method = 'POST';
clientOptions.path = '/api';
break;
case 'UPDATE':
case 'PUT':
clientOptions.path = '/api';
clientOptions.method = 'PUT';
break;
case 'REMOVE':
case 'DELETE':
clientOptions.method = 'DELETE';
clientOptions.path = '/api';
break;
default:
clientOptions.method = 'GET';
clientOptions.path = '/';
}
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode);
switch(res.statusCode) {
case 200:
res.setEncoding('utf8');
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
}
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
});
它是如何工作的
这通过利用 Node.js HTTP 模块来实现。该模块提供了一个界面,可以方便地创建一个客户端请求,http.request。在这个解决方案中,您首先利用process.argv去除启动您的应用的任何相关命令行参数。在本例中,您只需实例化传递您希望提供的 HTTP 方法的应用,应用将遍历这些方法,为每个方法创建一个请求。
$ node 4-6-1.js GET POST PUT DELETE NOTHING
如果您的目标是在第 4-3 节中创建的服务器,您可以看到与下面类似的结果,显示您成功地访问了客户端请求的 API 端点。
status code 200
data get
status code 200
data get
status code 200
data post
status code 200
data put
status code 200
data delete
以上介绍了实现的工作原理,但现在您将看到 Node.js 如何处理一个http.request。http.request有两个参数,一个选项对象和一个接收响应的回调函数。
当您调用http.request时,您初始化了一个ClientRequest对象。ClientRequest对象继承自 Node.js OutgoingMessage对象。ClientRequest对象有一整套缺省值,这些缺省值是根据传递给 options 参数的内容进行处理的。当您浏览ClientResponse对象时,您将看到这些默认设置正在被配置。
表 4-2。客户端请求对象选项
| [计]选项 | 功能 |
|---|---|
| 代理人 | 控制代理行为。当使用代理时,请求将默认为 Connection: keep-alive。 |
| 作家(author 的简写) | 基本认证(即“用户:密码”)。 |
| 头球 | 包含请求标头的对象。 |
| 宿主 | 向其发出请求的服务器的域名或 IP 地址(默认为“localhost”)。 |
| 主机名 | 为了支持 url.parse(),主机名优于主机。 |
| 本地地址 | 要为网络连接绑定的本地接口。 |
| 方法 | 指定 HTTP 请求方法的字符串(默认为 GET)。 |
| 小路 | 请求路径(默认为“/”)。应包含查询字符串(如果有)。 |
| 港口 | 远程服务器的端口(默认为 80)。 |
| 套接字路径 | Unix 域套接字(使用 host:port 或 socketPath 之一)。 |
只有当响应返回时,传递给http.request函数的回调函数才会从ClientRequest对象中调用。您还为 error 事件设置了一个事件侦听器,以便捕获请求过程中可能发生的任何错误。一旦返回了响应,您就可以处理该响应。在这个例子中,您检查statusCodes并相应地记录。您将在下一节看到更多关于处理响应的内容。
需要注意的是,为了让clientResponse工作,你必须调用request.end()函数。不管通过请求体发送的数据量有多少,这都是必要的,因为您必须表示请求的结束。
4-7.处理客户端响应
问题
您已经创建了一个 HTTP 客户端;您现在需要理解如何处理客户端响应。
解决办法
正确处理您在 HTTP 客户机上收到的响应非常重要。您需要响应诸如状态代码或应用所依赖的特定标题之类的东西。
对于这个解决方案,您可以想象一个场景,其中您的 HTTP 客户端需要查找由服务器设置的自定义头,x-ample,如果设置为适当的值,它将提醒客户端执行一个特殊的操作。然后,您将检查状态代码,以确保在执行您的操作之前有一个良好的响应。
清单 4-19 。处理响应
/**
* Processing client responses
*/
var http = require('http');
var clientOptions = {
host: 'localhost',
port: '8080',
path: '/',
method: 'GET'
};
var clientReq = http.request(clientOptions, function(res) {
//Handle custom header for something special
if (res.headers['x-ample'] === 'trigger') {
console.log('x-ample header trigger');
//work with status codes
switch(res.statusCode) {
case 200:
res.setEncoding('utf8'); // unless you can read buffer chunks
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
default:
console.log(res.statusCode + ': ' + http.STATUS_CODES[res.statusCode]);
break;
}
} else {
console.log('required header not present');
}
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.setHeader('Cache-Control', 'no-cache');
clientReq.end();
它是如何工作的
http.request函数上的回调函数是“response”事件的事件处理程序。这个事件监听器是从服务器响应接收数据的唯一方式。如果您省略了“响应”侦听器或回调,您的客户端请求将永远不会从服务器接收任何数据。
一旦您使用适当的监听器为'response'事件设置了客户端请求,您就能够从响应中获取数据。响应是一个可读的流,所以您可以通过为'data'事件添加一个侦听器,或者在流变成“readable”时调用response.read()来处理数据
在本例中,您避免直接从响应中读取数据,直到您检查了响应中的某个值。其中一个值是检查从响应发送的头。因为响应是包含 headers 对象的可读流,所以只需检查想要解析的头;将其值与应用中所需的值进行比较。
清单 4-20 。响应标题
if (res.headers['x-ample'] === 'trigger') {
console.log('x-ample header trigger');
/* . . . */
}
然后,您继续处理响应。在这个解决方案中,下一步是检查响应状态代码。如果状态代码不是 200 OK,您将无法从响应中读取数据。当然,如果一切正常,您将阅读响应正文。
清单 4-21 。响应状态代码
switch(res.statusCode) {
case 200:
res.setEncoding('utf8');
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
default:
console.log(res.statusCode + ': ' + http.STATUS_CODES[res.statusCode]);
break;
}
读取响应分两步完成。首先,为了使响应可读,将编码设置为 UTF-8。
清单 4-22 。响应默认编码
data <Buffer 67 65 74 0a>
清单 4-23 。响应 UTF-8 编码
data get
通过有策略地检查返回到 HTTP 请求回调的响应对象,可以处理特定于 Node.js 解决方案的各种参数和任务。
4-8.处理客户端请求
问题
您已经创建了一个 HTTP 客户端,并学习了如何处理来自它的响应。现在,您需要更详细地控制您的客户端请求。
解决办法
首先构建一个 HTTP GET 请求。GET 请求可以有两种形式。首先,如果您不需要控制自定义头或何时发送request.end()事件,您可以通过使用http.get() .使用 Node.js 快速实现 HTTP GET 请求
清单 4-24 。使用 http.get()
var http = require('http');
var getReq = http.get('http://localhost:8080', function(res) {
console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});
getReq.on('error', function(err) {
console.log(err);
});
或者,如果您需要能够控制您的头的某些方面,但是您仍然只需要处理一个 HTTP GET 请求,您将希望使用完整的http.request方法来代替。
清单 4-25 。HTTP GET 使用 http.request
var http = require('http');
var clientOptions = {
host: 'localhost',
port: '8080',
path: '/',
method: 'GET',
headers: { 'Connection': 'keep-alive',
'Content-Length': 0 }
};
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});
clientReq.on('continue', function(res) {
console.log('continue event due to 100-continue');
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
在构建 Node.js 应用时,您可能会遇到需要处理数据上传的情况。在 Node.js 中,你可以用 POST 方法处理这个http.request。这个请求然后用request.write函数写上传。
清单 4-26 。用 HTTP POST 上传
var http = require('http');
var opt = {
host : 'localhost',
port : 8080,
path : '/upload',
method : 'POST'
};
var upload = http.request(opt, function(res) {
console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});
upload.on('error', function(err) {
console.log(err);
});
upload.write('my upload stuff');
upload.end();
它是如何工作的
通过客户机请求检索内容的第一种解决方案是使用http.get()。HTTP GET 是对http.request的抽象。事实上,http.get()函数调用默认的http.request,允许设置所有的默认选项;然后它立即调用request.end()方法并完成请求。
清单 4-27 。Node.js http 模块,获取方法
exports.get = function(options, cb) {
var req = exports.request(options, cb);
req.end();
return req;
};
接下来,通过将方法选项设置为“GET”的http.request方法检索内容。这是一个标准的 GET 请求,但是您也传递了两个特定的头。值设置为“keep-alive”的连接头将告诉 Node.js 保持到服务器的连接打开,直到下一个请求。本解决方案中的另一个标题集是Content-Length标题。这个头一旦设置,将阻止 Node.js 使用默认的分块编码。在http.request选项中,有另外两个值得注意的头文件没有在这个解决方案中使用。
其中一个标题是Expect标题。设置这个头将立即发送请求头,以便考虑潜在的Expect: 100-continue头,我们将在 4-9 节处理事件时看到更多细节。
最后一个值得注意的头是当你发送一个授权头时。当配置http.request的设置时,该标题将取代利用auth选项的需要。
处理 HTTP 客户端请求的解决方案的最后一部分是演示如何处理文件上传请求。要做到这一点,必须做几件事。首先,如您所料,不要使用 HTTP GET 方法。相反,将上传的方法选项设置为 HTTP POST。然后通过http.request的 write 方法发送数据来处理上传。
现在,您已经看到了如何在 web 服务器上使用 HTTP 客户端请求来处理请求。接下来,您将看到在您的 web 服务器上发出和使用的各种事件。
4-9.响应事件
问题
您已经在 Node.js 中构建了一个 web 服务器,现在您需要处理并正确响应在您的服务器上发出或侦听的事件。
解决办法
为了恰当地描述这个解决方案,您需要理解事件的两个方面。为此,您将构建一个 HTTP web 服务器和一个 HTTP 客户端。
服务器(见清单 4-28 )是为处理不同方法的请求和事件而构建的。首先,您的 web 服务器将监听传入的请求。在出现这些请求时,您会希望用一个明文响应来欢迎请求者。
其次,您将希望通过监听连接事件来监控到该服务器的连接。这将增加与您的服务器建立的连接总数。
您希望您的服务器也能处理一些特殊事件。其中一个事件是监听发送了'Expect: 100-continue'头的传入请求的事件。这适用于希望在实际发送请求正文之前确定您的服务器是否能够接收消息的客户端连接。在这种情况下,您需要监听的事件是'checkContinue'事件。您还需要允许使用“Request: Upgrade”报头,以便通过监听服务器上的“upgrade”事件来升级请求。然后可以发送 upgrade 头,将传输升级到 TLS,或者在本例中,升级到 WebSockets。
清单 4-28 。Web 服务器事件
/**
* Responding to events
*/
var http = require('http'),
server = http.createServer(),
connections = 0;
// request event
server.on('request', function(req, res) {
console.log('request');//, req);
res.writeHead(200, { 'Content-Type': 'text/plain'});
res.end('heyo');
});
server.on('connection', function(socket) {
connections++;
console.log('connection count: ', connections);
});
server.on('checkContinue', function(req, res) {
console.log('checkContinue');
res.writeContinue();
});
server.on('upgrade', function(req, socket, head) {
console.log('upgrade');
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' +
'Sec-WebSocket-Protocol: chat\r\n' +
'\r\n');
socket.pipe(socket);
});
server.listen(8080);
为了正确体验这些事件,您需要有两组连接到此服务器的客户端。您将构建的第一个客户端,清单 4-22 ,您需要在其中提供必要的事件,以便提供 Expect 头,Expect: 100-continue,并正确地响应从服务器发出的 continue 事件。
清单 4-29 。客户端事件用于处理 Expect: 100-continue
/*
* client events
*/
var http = require('http');
var clientOptions = {
host: 'localhost',
// hostname:'nodejs.org',
port: '8080',
path: '/',
method: 'GET',
headers: { 'Expect': '100-continue' }
};
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode);
switch(res.statusCode) {
case 200:
res.setEncoding('utf8'); // unless you can read buffer chunks
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
}
});
clientReq.on('continue', function() {
console.log('client continue');
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
您将创建第二个客户机来演示您的 web 服务器发出和使用的事件,它将创建一个客户机来处理对 WebSocket 服务器的升级。这发生在你在清单 4-30 中创建的客户端中,它在设计上类似于清单 4-29 。但是,它处理不同的事件以提供不同的实现。
清单 4-30 。升级客户端
/*
* client events
*/
var http = require('http');
var clientOptions = {
host: 'localhost',
// hostname:'nodejs.org',
port: '8080',
path: '/',
method: 'GET',
headers: { 'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Origin' :'localhost',
'Sec-WebSocket-Protocol': 'chat',
'Sec-WebSocket-Version': 13 }
};
var clientReq = http.request(clientOptions, function(res) {
console.log('status code', res.statusCode);
switch(res.statusCode) {
case 200:
res.setEncoding('utf8'); // unless you can read buffer chunks
res.on('data', function(data) {
console.log('data', data);
});
break;
case 404:
console.log('404 error');
break;
}
});
clientReq.on('upgrade', function(res, socket, head) {
console.log('client upgrade');
});
clientReq.on('error', function(error) {
throw error;
});
clientReq.end();
它是如何工作的
事件是构建成功的 Node.js 应用的关键部分。就这一点而言,在 Node.js 中构建一个成功的 web 服务器必须包含对客户机和服务器之间发出的事件的正确处理。在本节的解决方案中,您创建了一个同时解决多个问题的 web 服务器。
该服务器开始监听请求事件。每次有对服务器的请求时,都会发出此请求事件。一旦接收到请求,就向请求者发送一个响应,在本例中是一个简单的问候。对请求事件的回调同时提供请求和响应对象;其实这个事件和直接给http.createServer函数添加回调是一样的。
您的下一个侦听器用于通过侦听何时发出连接事件来跟踪与服务器的连接。每次连接到服务器时都会发生这种情况。每当有一个连接时,当您递增计数器时,就会发生这种情况。connection 事件在回调函数中发送连接的 socket 对象,如果您愿意,允许您访问net.Socket。
在下一个事件监听器中处理'Expect: 100-continue'头。该侦听器被绑定到“checkContinue”事件。仅当请求发送 expect 标头时,才会发出此事件。如果您没有监听此事件,服务器将自己发送适当的继续响应。
清单 4-31 。当 Expect 头存在时,Node.js 发出 checkContinue
if (req.headers.expect !== undefined &&
(req.httpVersionMajor == 1 && req.httpVersionMinor == 1) &&
continueExpression.test(req.headers['expect'])) {
res._expect_continue = true;
if (EventEmitter.listenerCount(self, 'checkContinue') > 0) {
self.emit('checkContinue', req, res);
} else {
res.writeContinue();
self.emit('request', req, res);
}
}
如果您正在适当地处理这个事件,您需要向请求表明允许继续发送请求的主体。这是通过调用response.writeContinue()函数来完成的。这个函数向请求者写入适当的 HTTP 100 响应。
清单 4-32 。Response 继续发送 HTTP 100 继续响应
ServerResponse.prototype.writeContinue = function() {
this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii');
this._sent100 = true;
};
这个 continue 事件只有在你发送适当的头时才起作用,就像在来自清单 4-22 : headers: { ‘expect’ : ‘100-continue’ }的客户端请求中一样。然后监听来自客户机请求应用的 connect 事件,表示何时调用了response.writeContinue()函数并发送了 HTTP 100 响应。
最后,您的服务器被设置为处理 WebSocket 协议的升级。这个协议是通过一个握手过程启动的,这个握手过程由客户端请求和 web 服务器发出的事件处理。当客户端发送升级报头时,该过程开始:报头:{ 'Connection': 'Upgrade', 'Upgrade': 'websocket'}。除了这些头字段之外,还会发送一个 WebSocket 密钥,服务器将利用该密钥来验证请求握手是否已收到。当这个头存在时,将发出一个'upgrade’'事件,您将在您的服务器上监听这个升级事件。
服务器上的“upgrade”事件有一个回调函数,它有三个参数:请求、套接字和头。为了完成请求 WebSocket 握手,您必须发送适当的 HTTP 响应。在这种情况下,这是具有相同升级和连接头的 HTTP 101 Web Socket 协议握手。还发回了 websocket-accept 头,这是对收到来自请求的密钥的验证。一旦发送了头,您的客户机就可以接收升级事件并完成 WebSocket 升级握手。
此次升级活动还引入了。套接字流上的管道方法。流是构建许多 Node.js 应用不可或缺的一部分。这是一种以简洁和统一的方式管理流的输入和输出的方法。这可以通过获取可读的源流并将其通过管道传输到可写的目标流来实现。这导致目标流的返回。在这个升级事件回调中,你写socket.pipe(socket);。这需要您刚才调用 socket.write() 的源(或套接字)来添加 WebSocket 升级头。然后,它通过管道把它输出到代表目标流的.pipe(socket)。
4-10.通过文件系统提供静态页面
问题
您正在构建一个 web 服务器。直接从 Node.js 代码提供 HTML 是不可维护的,也是不可取的。您需要能够从驻留在文件系统本身的文件中提供内容。
解决办法
要构建提供内容的 web 服务器,您需要利用 HTTP 模块和文件系统模块,它们是 Node.js 核心的一部分。您将构建您的服务器来处理服务器上的错误,然后您可以用正确的状态代码进行响应。您还将确保随您提供的文件一起发送适当的响应头。这意味着您需要注意所提供内容的 mime 类型。为此,使用一个简单的 URL 结构来了解应用中的哪些路由将从 web 服务器请求哪些类型的文件。
清单 4-33 。静态文件 Web 服务器
/**
* serving static HTML with the file system
*/
var http = require('http'),
fs = require('fs'),
path = require('path');
//Content types map
var contentTypes = {
'.htm' : 'text/html',
'.html' : 'text/html',
'.js' : 'text/javascript',
'.json' : 'application/json',
'.css' : 'text/css'
};
var server = http.createServer(function(req, res) {
var fileStream = fs.createReadStream(req.url.split('/')[1]);
fileStream.on('error', function(error) {
if (error.code === 'ENOENT') {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
} else {
res.statusCode = 500;
res.end(http.STATUS_CODES[500]);
}
});
//Get the extension
var extension = path.extname(req.url);
//read the extension against the content type map - default to plain text
var contentType = contentTypes[extension] || 'text/plain';
// add the content type header
res.writeHead(200, { 'Content-Type' : contentType });
// pipe the stream to the response stream
fileStream.pipe(res);
});
server.listen(8080);
现在您有了一个 web 服务器,可以从文件系统中提供静态文件。为了测试这个功能,您还需要构建两个测试文件。一个是基本 HTML 文件。第二个是 JSON 文件。这个文件表示一个假想的 API 的响应,您可以构建这个 API 来从您的应用中访问它。这些文件显示在清单 4-34 和清单 4-35 中。
清单 4-34 。要提供的基本 HTML 文件
<!doctype html>
<html>
<head>
<title>Static HTML</title>
</head>
<body>
<h2>Node.js Recipes</h2>
<p> Tasty </p>
<button id='getJSON'>Get JSON file</button>
<script type='text/javascript'>
// bind to click
var btn = document.getElementById('getJSON');
btn.addEventListener('click', getJSONContent, false);
// Send a request to the server for the JSON file
function getJSONContent() {
var xhr = new XMLHttpRequest();
xhr.onload = jsonRetrieved;
xhr.open('GET', '/4-10-1.json', true);
xhr.send();
}
// Log to the console
function jsonRetrieved() {
console.log(this.responseText);
}
</script>
</body>
</html>
清单 4-35 。要提供的示例 JSON 文件
{
'Test': 'if',
'this':'sends'
}
它是如何工作的
让我们调查一下您的全功能 web 服务器是如何工作的。首先,您为这个服务器使用 HTTP 模块。您还可以用文件系统和 URL 模块来扩充这些模块。这将允许您从 web 服务器的文件系统中获取和读取文件,并且 URL 模块允许解析 URL,以便正确地路由您的内容。
现在,您通过调用http.createServer创建一个 web 服务器,并让该服务器监听您指定的端口。这个解决方案的实质在于requestListener回调。在这个回调中,您可以处理传入的请求和传出的响应。
当您收到一个请求时,您的服务器做的第一件事就是使用 fs.createReadStream 将传入的请求 URL 读入一个流。这将允许您创建适当的错误响应代码发送到客户端。在您的情况下,如果错误代码是 ENOENT(没有这样的文件或目录),您将发送 404 not found,对于其他错误,您将返回到一般的 500 服务器错误。
然后解析来自请求 URL 的扩展。这是通过使用 Node.js 路径模块完成的,该模块有一个方法“extname ”,它将返回给定路径的扩展名。然后将它用于您创建的内容类型对象,以将给定的扩展映射到您希望从服务器提供的适当内容类型。一旦将扩展映射到内容类型,就可以将内容类型头写入响应。接下来是通过管道将文件流传送到响应。
接下来,您将研究构建到 web 服务器中的模拟 JSON API。这条路线在网址/*.json上。这表示可能调用数据库来检索信息,但是在我们的例子中,它检索的是一个 JSON 文件,该文件将在头中带有“Content-Type: application/json”。
现在,您可以为 web 服务器提供任何类型的内容。您可以通过运行您的服务器,然后导航到各种 URL 来测试这一点。如果您导航到//localhost:8080/4-10-1.html,您将看到一个 html 页面。这个页面有一个按钮,您可以按下它向 JSON API 提交一个 XMLHttpRequest,将内容记录到控制台。当然,您可以直接导航到/4-10-1.json 路径,在那里您也将收到 json。测试一个 404,你可以简单地尝试卷曲http://localhost:8080/404,你会收到预期的 404:
> GET /404 HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Sat, 18 May 2013 19:17:55 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
五、使用事件和子进程
正如您在本书中已经看到的,Node.js 有一个健壮的框架来处理许多例程和重要任务。在这一章中,你将全面理解你在前面章节中看到的一些概念。您将首先深入了解 Node.js 的一个基石,EventEmitters。关于这些,你将看到如何创建自定义事件和为它们添加监听器的方法,以及如何创建单个事件。所有这些都将展示如何通过在 Node.js 中有策略地实现事件来减少无休止的回调噩梦。
接下来,您将揭开用子流程扩展 Node.js 流程的神秘面纱。您将看到如何产生一个子进程,以及如何执行 shell 命令和文件。然后,您将学习如何派生一个流程,这将使我们能够在 Node.js 中对流程进行集群。
5-1.创建自定事件
问题
您已经创建了一个 Node.js 应用,但是您需要通过发出一个自定义事件在其中进行通信。
解决办法
在此解决方案中,您将创建一个 Node.js 应用,演示如何创建和侦听自定义事件。您将创建一个在超时期限到期后执行的事件。这表示操作完成时应用中会出现的情况。这将调用函数doATask,该函数将返回操作是成功还是失败的状态。有两种方法可以实现这一点。
首先,您将创建特定于状态的事件。这需要检查状态并专门为该状态创建一个事件,以及绑定到那些特定的事件来处理特殊情况。这在清单 5-1 中进行了演示。
清单 5-1 。单个状态的自定义事件
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter();
function doATask(status) {
if (status === 'success') {
emitter.emit('taskSuccess'); // Specific event
} else if (status === 'fail') {
emitter.emit('taskFail');
}
}
emitter.on('taskSuccess', function() {
console.log('task success!');
});
emitter.on('taskFail', function() {
console.log('task fail');
});
// call task with success status
setTimeout(doATask, 500, 'success');
// set task to fail
setTimeout(doATask, 1000, 'fail');
虽然您看到这有效地使事件适当地传播,但这仍然会导致您创建两个单独的事件来发出。这可以很容易地修改成更加精简和高效的,正如你将在清单 5-2 中看到的。
清单 5-2 。一个发射器来统治他们
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter();
function doATask(status) {
// This event passes arguments to detail status
emitter.emit('taskComplete', 'complete', status);
}
// register listener for task complete
emitter.on('taskComplete', function(type, status) {
console.log('the task is ', type, ' with status ', status);
});
// call task with success status
setTimeout(doATask, 500, 'success');
// set task to fail
setTimeout(doATask, 1000, 'fail');
这是一个更精简、更高效的实现。您可以发出适用于应用中多个状态的单个事件。在这些示例中,您看到了一个实现,其中所有事件都是从同一个源文件中处理和发出的。在清单 5-3 中,你可以看到一个共享事件发射器来发射事件的例子,该事件将在当前模块之外的模块中被接收。
清单 5-3 。发射模块
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter(),
myModule = require('./5-1-3.js')(emitter);
emitter.on('custom', function() {
console.log('custom event received');
});
emitter.emit('custom');
为应用创建自定义事件的另一种方法是利用全局流程对象。这个 Node.js 对象是一个EventEmitter,它将允许您注册将在流程中共享的事件。这种类型的事件是从清单 5-4 中的代码发出的。
清单 5-4 。在 Node.js 进程上发出一个事件
/* Module.js file */
var myMod = module.exports = {
emitEvent: function() {
process.emit('globalEvent');
}
};
它是如何工作的
在这个例子中,您看到了创建自定义事件的多种方式。这些事件可以在任何有 Node.js EventEmitter的地方发出。Node.js EventEmitter类是组成应用的模块之间和内部通信的基石之一。
当您构建一个事件时,首先遇到的是EventEmitter类。此类由事件的对象集合组成,这些事件引用已注册的不同类型的事件。类型的概念就是你给你的事件起的名字,比如taskComplete或者taskFail。当您实际使用EventEmitter's发出方法发出事件时,这很重要。
清单 5-5 。EventEmitter 的 Emit 方法
EventEmitter.prototype.emit = function(type) {
var er, handler, len, args, i, listeners;
if (!this._events)
this._events = {};
// If there is no 'error' event listener then throw.
if (type === 'error') {
if (!this._events.error ||
(typeof this._events.error === 'object' &&
!this._events.error.length)) {
er = arguments[1];
if (this.domain) {
if (!er) er = new TypeError('Uncaught, unspecified "error" event.');
er.domainEmitter = this;
er.domain = this.domain;
er.domainThrown = false;
this.domain.emit('error', er);
} else if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
throw TypeError('Uncaught, unspecified "error" event.');
}
return false;
}
}
handler = this._events[type];
if (typeof handler === 'undefined')
return false;
if (this.domain && this !== process)
this.domain.enter();
if (typeof handler === 'function') {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
len = arguments.length;
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
handler.apply(this, args);
}
} else if (typeof handler === 'object') {
len = arguments.length;
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
listeners = handler.slice();
len = listeners.length;
for (i = 0; i < len; i++)
listeners[i].apply(this, args);
}
if (this.domain && this !== process)
this.domain.exit();
return true;
};
这种方法包括两个主要部分。首先,是对“错误”事件的特殊处理。这将按照您的预期发出错误事件,除非错误事件没有侦听器。在这种情况下,Node.js 将抛出错误,该方法将返回 false。此方法的第二部分是处理非错误事件的部分。
在检查以确保指定类型的给定事件有处理程序之后,Node.js 接着检查事件处理程序是否是一个函数。如果是函数,Node.js 将解析来自 emit 方法的参数,并将这些参数应用到处理程序。这就是清单 5-2 中的将参数传递给taskComplete事件的方式。提供的额外参数在 emit 方法调用处理程序时应用。
其他解决方案都使用相同的发射方法,但是它们以不同的方式获得发射事件的结果。清单 5-4 表示一个在整个应用中共享的 Node.js 模块。这个模块包含一个函数,该函数将向应用的其余部分发出一个事件。在这个解决方案中实现这一点的方法是利用主 Node.js 进程是一个EventEmitter的知识。这意味着您只需通过调用process.emit('globalEvent')来发出事件,共享该进程的应用的一部分将接收该事件。
5-2.为自定义事件添加侦听器
问题
在上一节中,您已经发出了自定义事件,但是如果没有合适的方法绑定到这些事件,您将无法使用它们。为此,您需要向这些事件添加侦听器。
解决办法
这个解决方案是第 5-1 节的对应部分。在上一节中,您实现了EventEmitters并发出了事件。现在,您需要为这些事件添加侦听器,以便可以在您的应用中处理它们。这个过程就像发射事件一样简单,如清单 5-6 所示。
清单 5-6 。向自定义事件和系统事件添加事件监听器
/**
* Custom Events
*/
var events = require('events'),
emitter = new events.EventEmitter();
function doATask(status) {
if (status === 'success') {
emitter.emit('taskSuccess'); // Specific event
} else if (status === 'fail') {
emitter.emit('taskFail');
}
// This event passes arguments to detail status
emitter.emit('taskComplete', 'complete', status);
}
emitter.on('newListener', function(){
console.log('a new listener was added');
});
emitter.on('taskSuccess', function() {
console.log('task success!');
});
emitter.on('taskFail', function() {
console.log('task fail');
});
// register listener for task complete
emitter.on('taskComplete', function(type, status) {
console.log('the task is ', type, ' with status ', status);
});
// call task with success status
setTimeout(doATask, 2e3, 'success');
// set task to fail
setTimeout(doATask, 4e3, 'fail');
您还可以将您的EventEmitter传递给一个外部模块,然后从那个单独的代码段中监听事件。
清单 5-7 。从外部模块监听事件
/**
* External Module
*/
module.exports = function(emitter) {
emitter.on('custom', function() {
console.log('bazinga');
});
};
正如您使用 Node.js 进程EventEmitter发出事件一样,您可以将侦听器绑定到该进程并接收事件。
清单 5-8 。Node.js 进程范围侦听器
/**
* Global event
*/
var ext = require('./5-1-5.js');
process.on('globalEvent', function() {
console.log('global event');
});
ext.emitEvent();
它是如何工作的
当您检查清单 5-6 中的解决方案时,您应该很快注意到如何向事件添加监听器。这和调用EventEmitter.on()方法 一样简单。的。EventEmitter的on方法接受两个参数:一是事件类型名;第二,侦听器回调,它将接受传递给emit()事件的任何参数。的。on方法实际上只是EventEmitter addListener函数 的包装器,它采用相同的两个参数。您可以直接调用此方法来代替调用。on功能一样。
清单 5-9 。事件发射器 addListener
EventEmitter.prototype.addListener = function(type, listener) {
var m;
if (typeof listener !== 'function')
throw TypeError('listener must be a function');
if (!this._events)
this._events = {};
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (this._events.newListener)
this.emit('newListener', type, typeof listener.listener === 'function' ?
listener.listener : listener);
if (!this._events[type])
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
else if (typeof this._events[type] === 'object')
// If we've already got an array, just append.
this._events[type].push(listener);
else
// Adding the second element, need to change to array.
this._events[type] = [this._events[type], listener];
// Check for listener leak
if (typeof this._events[type] === 'object' && !this._events[type].warned) {
var m;
if (this._maxListeners !== undefined) {
m = this._maxListeners;
} else {
m = EventEmitter.defaultMaxListeners;
}
if (m && m > 0 && this._events[type].length > m) {
this._events[type].warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
this._events[type].length);
console.trace();
}
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
从源代码片段中可以看出,addListener方法完成了几项任务。首先,在验证侦听器回调是一个函数之后,addListener方法发出它自己的事件“newListener”,以表明已经添加了一个给定类型的新侦听器。
发生的第二件事是addListener函数将监听器函数推到它所绑定的事件。在上一节中,这个函数成为了每种事件类型的处理函数。根据发射器本身提供的参数数量,emit()函数将对该函数执行.call()或.apply()操作。
最后在addListener函数中,你会发现 Node.js 非常友好,试图保护你免受潜在的内存泄漏。它通过检查侦听器的数量是否超过预定义的限制(默认为 10)来实现这一点。当然,您可以通过使用setMaxListeners()方法将这个值配置为一个更高的值,当您超过这个侦听器数量时,会出现一个有用的警告。
5-3.实现一次性事件
问题
您需要在 Node.js 应用中实现一个只希望执行一次的事件。
解决办法
假设您有一个要完成重要任务的应用。这个任务需要完成,但只能完成一次。假设您有一个监听聊天室成员的事件,该成员要么退出应用,要么断开连接。这个事件只需要处理一次。将该事件向应用的其他用户广播两次是没有意义的,因此您限制了处理该事件的次数。
有两种方法可以做到这一点。一种是手动处理注册事件,然后在接收到一次事件后删除事件侦听器。
清单 5-10 。手动注册一次事件侦听器
/**
* Implementing a One time event
*/
var events = require('events'),
emitter = new events.EventEmitter();
function listener() {
console.log('one Timer');
emitter.removeListener('oneTimer', listener);
}
emitter.on('oneTimer', listener);
emitter.emit('oneTimer');
emitter.emit('oneTimer');
这需要在侦听器函数中进行二次调用,以便能够从事件中移除侦听器。随着项目的增长,这可能会变得难以处理,因此 Node.js 有一个本机实现来实现同样的效果。
清单 5-11 。emtter.once()
/**
* Implementing a One-time event
*/
var events = require('events'),
emitter = new events.EventEmitter();
/* EASIER */
emitter.once('onceOnly', function() {
console.log('one Only');
});
emitter.emit('onceOnly');
emitter.emit('onceOnly');
它是如何工作的
第一个注册一个事件侦听器只绑定一次所发出的事件的例子非常容易理解。首先用侦听器的函数回调绑定到事件。然后,在侦听器中处理该回调,并从事件中移除该侦听器。这可以防止对来自同一发射器的事件进行任何进一步的处理。
这是因为removeListener方法 的缘故,它接受一个事件类型和一个特定的监听器函数。
清单 5-12 。event 发射器 removeListener 方法
EventEmitter.prototype.removeListener = function(type, listener) {
var list, position, length, i;
if (typeof listener !== 'function')
throw TypeError('listener must be a function');
if (!this._events || !this._events[type])
return this;
list = this._events[type];
length = list.length;
position = -1;
if (list === listener ||
(typeof list.listener === 'function' && list.listener === listener)) {
this._events[type] = undefined;
if (this._events.removeListener)
this.emit('removeListener', type, listener);
} else if (typeof list === 'object') {
for (i = length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
position = i;
break;
}
}
if (position < 0)
return this;
if (list.length === 1) {
list.length = 0;
this._events[type] = undefined;
} else {
list.splice(position, 1);
}
if (this._events.removeListener)
this.emit('removeListener', type, listener);
}
return this;
};
removeListener函数 将通过递归搜索 events 对象来定位需要移除的特定事件,以便找到您正在搜索的类型和函数组合。然后,它将移除事件绑定,以便侦听器不再在后续事件中注册。
一个类似于手工发射一次函数的方法是EventEmitter.once方法 。
清单 5-13 。EventEmitter once 方法
EventEmitter.prototype.once = function(type, listener) {
if (typeof listener !== 'function')
throw TypeError('listener must be a function');
function g() {
this.removeListener(type, g);
listener.apply(this, arguments);
}
g.listener = listener;
this.on(type, g);
return this;
};
该方法接受您希望一次性绑定到的侦听器和事件类型。然后,它创建一个内部函数,该函数将应用侦听器。在内部函数中调用此侦听器之前,实际的侦听器将从事件中移除。这就像您的自定义一次性方法一样,因为它会在事件第一次执行时移除侦听器。
在下一节中,我们将研究如何使用这些事件和自定义事件来减少 Node.js 应用中的回调量。
5-4.使用事件减少回调
问题
您有一个 Node.js 应用,它有多个回调函数。这段代码已经变得有点笨拙,所以你想通过利用EventEmitter来减少回调。
解决办法
想象一下,一个购物应用必须访问数据库中的数据,操作这些数据,然后刷新数据库并将状态发送回客户端。这可能是获取购物车、添加商品,并让客户知道购物车已经更新。第一个例子是使用回调编写的。
清单 5-14 。使用回调的购物车
var initialize = function() {
retrieveCart(function(err, data) {
if (err) console.log(err);
data['new'] = 'other thing';
updateCart(data, function(err, result) {
if (err) console.log(err);
sendResults(result, function(err, status) {
if (err) console.log(err);
console.log(status);
});
});
});
};
// simulated call to a database
var retrieveCart = function(callback) {
var data = { item: 'thing' };
return callback(null, data );
};
// simulated call to a database
var updateCart = function(data, callback) {
return callback(null, data);
};
var sendResults = function(data, callback) {
console.log(data);
return callback(null, 'Cart Updated');
};
initialize();
首先,理解在 Node.js 中使用回调并没有错。事实上,这可能是在 Node.js 中处理异步编程的最流行的方式。然而,你也可以看到,在你有大量必须连续发生的回调的情况下,比如在清单 5-14 中,代码可能变得不那么容易理解。为了纠正这一点,您可以合并事件,以更简洁的方式管理应用流。
清单 5-15 。使用事件代替回调
/**
* Reducing callbacks
*/
var events = require('events');
var MyCart = function() {
this.data = { item: 'thing' };
};
MyCart.prototype = new events.EventEmitter();
MyCart.prototype.retrieveCart = function() {
//Fetch Data then emit
this.emit('data', this.data);
};
MyCart.prototype.updateCart = function() {
// Update data then emit
this.emit('result', this.data);
};
MyCart.prototype.sendResults = function() {
console.log(this.data);
this.emit('complete');
};
var cart = new MyCart();
cart.on('data', function(data) {
cart.data['new'] = 'other thing';
cart.updateCart();
});
cart.on('result', function(data) {
cart.sendResults(data);
});
cart.on('complete', function() {
console.log('Cart Updated');
});
cart.retrieveCart();
对于同一任务的不同解决方案,两者都使用了相似数量的代码,但是通过转移到事件驱动的模块而不是回调流,回调的数量已经大大减少了。
它是如何工作的
当您想要检查对应用至关重要的代码时,独占使用回调可能会成为一种负担。这也会让参与项目的开发人员感到头疼,因为他们可能不太熟悉项目,不知道给定的回调函数嵌套在哪里。当您希望重构应用以添加另一个要在回调期间执行的方法时,这也会成为一个问题。这些都是您可能选择迁移到事件驱动模型的原因。
在清单 5-11 的事件驱动解决方案中,首先创建一个名为MyCart的新对象。你可以假设MyCart用存储在其中的一个项目MyCart初始化。data。然后你的MyCart对象继承了events。EventEmitter对象。这意味着MyCart可以通过 Node.js 事件模块发送和接收数据。
既然您的对象可以发出和监听事件,您可以通过使用EventEmitter的方法来扩充您的对象。例如,您创建了一个retrieveCart方法,它将从数据存储中获取数据;一旦完成,就会发出']'事件,传递从购物车中检索到的任何数据。类似地,您创建一个updateCart方法和一个sendResults方法,这两个方法将提醒客户端更新的结果。
然后实例化一个新的MyCart实例。这个新的购物车现在可以绑定到将从MyCart对象发送的事件。您有一个单独的函数来处理每个事件。这使得代码更易于维护,并且在许多情况下更易于扩展。例如,假设您需要为MyCart添加另一个日志功能。现在,您可以将其绑定到每个事件并记录交互,而无需重写整个回调流。
5-5.生下一个孩子。产卵
问题
您需要创建一个子进程来执行 Node.js 应用中的辅助操作。
解决办法
您希望从 Node.js 应用中派生出一个子进程的原因有很多。其中几个可以简单地执行命令行任务,而不需要为应用要求或构建整个模块。在这个解决方案中,您将突出显示两个命令行应用和第三个解决方案,它们将从 spawn 方法执行另一个 Node.js 进程。
清单 5-16 。产卵的孩子
/**
* .spawn
*/
var spawn = require('child_process').spawn,
pwd = spawn('pwd'),
ls = spawn('ls', ['-G']),
nd = spawn('node', ['5-4-1.js']);
pwd.stdout.setEncoding('utf8');
pwd.stdout.on('data', function(data) {
console.log(data);
});
pwd.stderr.on('data', function(data) {
console.log(data);
});
pwd.on('close', function(){
console.log('closed');
});
ls.stdout.setEncoding('utf8');
ls.stdout.on('data', function(data) {
console.log(data);
});
nd.stdout.setEncoding('utf8');
nd.stdout.on('data', function(data) {
console.log(data);
});
第一个 spawn 是在当前目录下运行'pwd'命令;第二个是列出该目录中的所有文件。这些只是内置于操作系统中的命令行实用程序。但是,此解决方案中的第三个示例执行命令来运行 Node.js 文件;然后,像前面的例子一样,将输出记录到控制台。
它是如何工作的
产卵是调用 Node.js 中子进程的一种方法,也是child_process模块的一种方法。child_process模块创建了一种通过stdout、stdin和stderr传输数据流的方式。由于这个模块的性质,这可以以非阻塞的方式完成,很好地适应 Node.js 模型。
child_process spawn 方法将实例化一个ChildProcess对象,这是一个 Node.js EventEmitter。与ChildProcess对象相关的事件如表 5-1 所示。
表 5-1 。ChildProcess 事件
| 事件 | 详述 |
|---|---|
| '消息' | 传输一个消息对象,它是 JSON 或一个值。这也可以将套接字或服务器对象作为可选的第二个参数进行传输。 |
| '错误' | 将错误传输到回调。当子进程无法生成、无法终止或消息传输失败时,会发生这种情况。 |
| '关闭' | 当子进程的所有 stdio 流都完成时发生。这将发送退出代码和与之一起发送的信号。 |
| '断开连接' | 方法终止连接时发出。子对象(或父对象)上的 disconnect()方法。 |
| '退出' | 在子进程结束后发出。如果进程正常终止,code 是进程的最终退出代码,否则为 null。如果进程因收到信号而终止,signal 是信号的字符串名称,否则为 null。 |
除了这些ChildProcess事件,产生的孩子也是一个流,正如你在上面看到的。该流包含来自子进程的标准 I/O 的数据。在表 5-2 中列出了与这些流相关的方法,以及子流上的其他方法。
表 5-2 。子进程和其他方法的流事件
| 方法 | 描述 |
|---|---|
| 。标准输入设备 | 代表子进程 stdin 的可写流。 |
| 。标准输出 | 表示子进程的标准输出的可读流。 |
| 。标准错误 | 子进程的 stderr 的可读流。 |
| 。pid | 子进程进程标识符(PID)。 |
| 。杀 | 终止一个进程,可以选择发送终止信号。 |
| 。拆开 | 断开与父级的连接。 |
| 。派遣 | 向. fork 进程发送消息。(在第 5-8 节中有更多的细节。) |
现在您对什么是ChildProcess以及它如何适应child_process模块有了更多的了解,您可以看到您的生成子流程的解决方案直接调用了流程。您创建了三个衍生进程。其中每一个都以命令参数开始。这个参数,'pwd、' 'ls、' ']'是将被执行的命令,就像您在终端应用的命令行上运行它一样。child_process.spawn方法中的下一个参数是传递给命令参数的可选参数数组。
您会看到,本例中衍生的进程与在终端命令行中运行以下内容是一样的:
$ pwd &
$ ls –G &
$ node 5-4-1.js &
您也可以从您派生的进程中读取这些命令的输出。这是通过监听child_process.stdout流实现的。如果绑定到数据事件,您会看到这些命令的标准输出,就像在终端中运行命令一样。在第三个 spawn 的例子中,您可以看到本章前一节中整个模块的输出。
还有一个可选的第三个参数可以出现在child_process.spawn方法中。该参数表示帮助设置衍生进程的一组选项。这些选项的值如表 5-3 所示。
表 5-3 。繁殖选项
| [计]选项 | 类型 | 描述 |
|---|---|---|
| 粗木质残体 | 线 | 子进程的当前工作目录。 |
| 刺痛 | 数组或字符串 | 子进程的 stdio 配置。 |
| 自定义 Fds | 排列 | 不推荐使用的功能。 |
| 包封/包围(动词 envelop 的简写) | 目标 | 环境键值对。 |
| 分离的 | 布尔代数学体系的 | 这个子进程将成为一个组长。 |
| 用户界面设计(User Interface Design 的缩写) | 数字 | 设置进程的用户标识。 |
| 眩倒病 | 数字 | 设置进程的组标识。 |
5-6.使用运行 Shell 命令。执行
问题
您希望从 Node.js 应用中直接执行一个 shell 命令作为子进程。
解决办法
在上一节中,您看到了如何通过使用child_process模块轻松地产生子进程。这可能是一个长时间运行的流程,您希望访问流程中可用的stdio流。与此类似的是child_process.exec法。这两种方法的区别在于。spawn 方法将以流的形式返回所有数据,而。exec 方法将数据作为缓冲区返回。使用这种方法,您可以直接从 Node.js 应用中执行 shell 命令,在 Windows 中执行cmd.exe,或者在其他地方执行/bin/sh。使用本节中的解决方案,您将列出一组文件,并将该操作的结果记录到控制台。然后,您将在系统中搜索包含单词 node 的所有正在运行的进程,再次将输出记录到您的控制台。
清单 5-17 。。高级管理人员
/**
* Running Shell commands with .exec
*/
var exec = require('child_process').exec;
exec('ls -g', function(error, stdout, stderr) {
if (error) console.log(error);
console.log(stdout);
});
exec('ps ax | grep node', function(error, stdout, stderr) {
if (error) console.log(error);
console.log(stdout);
});
它是如何工作的
这个解决方案通过利用child_process.spawn方法和child_process.execFile方法的各个方面来工作,您将在下一节中研究这两个方法。本质上,当您告诉child_process使用exec方法时,您是在告诉它运行一个将/bin/sh文件(Windows 上的cmd.exe)作为可执行文件的进程。
清单 5-18 。子进程。执行功能
exports.exec = function(command /*, options, callback */) {
var file, args, options, callback;
if (typeof arguments[1] === 'function') {
options = undefined;
callback = arguments[1];
} else {
options = arguments[1];
callback = arguments[2];
}
if (process.platform === 'win32') {
file = 'cmd.exe';
args = ['/s', '/c', '"' + command + '"'];
// Make a shallow copy before patching so we don't clobber the user's
// options object.
options = util._extend({}, options);
options.windowsVerbatimArguments = true;
} else {
file = '/bin/sh';
args = ['-c', command];
}
return exports.execFile(file, args, options, callback);
};
事实上,这个函数调用了execFile方法。您将在下一节中看到,这意味着该进程是根据传递给函数的文件参数生成的。
var
child
=
spawn(file, args, {
cwd
:
options.cwd,
env
:
options.env,
windowsVerbatimArguments
: !!
options.windowsVerbatimArguments
});
这意味着您想在命令行中运行的任何东西,都可以通过exec来运行。这就是为什么当您试图以ps ax | grep node的身份运行exec函数来识别所有包含单词 node 的正在运行的进程时,您会看到stdout结果,就像在 shell 中运行它一样。
17774 s001 S+ 0:00.06 node 5-6-1.js
17776 s001 S+ 0:00.00 /bin/sh -c ps ax | grep node
17778 s001 S+ 0:00.00 grep node
11503 s002 S+ 0:00.07 node
5-7.使用执行外壳文件。execFile
问题
在您的应用中,您需要将一个文件作为 Node.js 进程的子进程来执行。
解决办法
您已经对child_process模块的这个方法有些熟悉了。在这个解决方案中,您有一个 shell 脚本,其中包含您希望从 Node.js 应用中执行的几个步骤。这些可以在 Node.js 中直接完成,要么生成它们,要么调用.exec方法。然而,通过将它们作为一个文件调用一次,你可以将它们组合在一起,并且仍然可以将它们的组合输出缓冲到execFile的回调函数中。您可以在接下来的两个清单中看到示例 Node.js 应用和将要执行的文件。
清单 5-19 。使用。execFile
/**
* execFile
*/
var execFile = require('child_process').execFile;
execFile('./5-7-1.sh', function(error, stdout, stderr) {
console.log(stdout);
console.log(stderr);
console.log(error);
});
清单 5-20 。要执行的外壳文件
#!/bin/sh
echo "running this shell script from child_process.execFile"
# run another node process
node 5-6-1.js
# and another
node 5-5-1.js
ps ax | grep node
它是如何工作的
当您开始研究execFile方法如何工作时,您会很快意识到它是.spawn方法的衍生物。这个方法非常复杂,为了执行一个文件要做很多事情。首先,execFile函数将接受四个参数。第一个是文件,它是查找要执行的文件和路径所必需的。
第二个是 args 数组,它将把参数传递给要执行的文件;第三个由在衍生进程上设置的特定选项组成;第四个是回调。如你所见,这些选项默认为通用设置,如 utf8 编码,超时设置为零,以及其他如清单 5-21 所示的设置。这个回调就像来自child_process.exec的回调一样,它将一组缓冲的error、stdout和stderr传递给函数,您可以直接从回调中使用这些流。
清单 5-21 。设置 execFile 的文件、参数和选项
exports.execFile = function(file /* args, options, callback */) {
var args, optionArg, callback;
var options = {
encoding: 'utf8',
timeout: 0,
maxBuffer: 200 * 1024,
killSignal: 'SIGTERM',
cwd: null,
env: null
};
// Parse the parameters.
if (typeof arguments[arguments.length - 1] === 'function') {
callback = arguments[arguments.length - 1];
}
if (Array.isArray(arguments[1])) {
args = arguments[1];
options = util._extend(options, arguments[2]);
} else {
args = [];
options = util._extend(options, arguments[1]);
}
Node.js 现在通过传入 options 对象来产生子进程。然后,产生的子 Node 通过各种事件监听器和回调函数传递,以便在返回子 Node 本身之前将stdio流聚合到提供给execFile方法的回调函数中,如清单 5-22 所示。这与。spawn 方法将直接返回stdout和stderr流。这里使用。exec方法返回一个从stdout和stderr流创建的缓冲区。
清单 5-22 。正在生成 execFile
var child = spawn(file, args, {
cwd: options.cwd,
env: options.env,
windowsVerbatimArguments: !!options.windowsVerbatimArguments
});
var stdout = '';
var stderr = '';
var killed = false;
var exited = false;
var timeoutId;
var err;
function exithandler(code, signal) {
if (exited) return;
exited = true;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (!callback) return;
if (err) {
callback(err, stdout, stderr);
} else if (code === 0 && signal === null) {
callback(null, stdout, stderr);
} else {
var e = new Error('Command failed: ' + stderr);
e.killed = child.killed || killed;
e.code = code;
e.signal = signal;
callback(e, stdout, stderr);
}
}
function errorhandler(e) {
err = e;
child.stdout.destroy();
child.stderr.destroy();
exithandler();
}
function kill() {
child.stdout.destroy();
child.stderr.destroy();
killed = true;
try {
child.kill(options.killSignal);
} catch (e) {
err = e;
exithandler();
}
}
if (options.timeout > 0) {
timeoutId = setTimeout(function() {
kill();
timeoutId = null;
}, options.timeout);
}
child.stdout.setEncoding(options.encoding);
child.stderr.setEncoding(options.encoding);
child.stdout.addListener('data', function(chunk) {
stdout += chunk;
if (stdout.length > options.maxBuffer) {
err = new Error('stdout maxBuffer exceeded.');
kill();
}
});
child.stderr.addListener('data', function(chunk) {
stderr += chunk;
if (stderr.length > options.maxBuffer) {
err = new Error('stderr maxBuffer exceeded.');
kill();
}
});
child.addListener('close', exithandler);
child.addListener('error', errorhandler);
return child;
};
5-8.使用。fork 用于进程间通信
问题
您需要在 Node.js 中创建一个子流程,但是您还需要能够在这些子流程之间轻松地进行通信。
解决办法
使用 fork 方法在进程间通信的解决方案非常简单。您将构建一个创建 HTTP 服务器的主进程。该流程还将派生一个子流程,并将服务器对象传递给该子流程。即使服务器不是在子进程上创建的,子进程也能够处理来自该服务器的请求。服务器对象和所有消息都通过。send()法。
清单 5-23 。父进程
/**
* .fork main
*/
var cp = require('child_process');
http = require('http');
var child = cp.fork('5-8-2.js');
var server = http.createServer(function(req, res) {
res.end('hello');
}).listen(8080);
child.send('hello');
child.send('server', server);
清单 5-24 。分叉过程
/**
* forked process
*/
process.on('message', function(msg, hndl) {
console.log(msg);
if (msg === 'server') {
hndl.on('connection', function() {
console.log('connected on the child');
});
}
});
它是如何工作的
正如你在第 5-5 节看到的,创建一个分叉的进程和创建一个衍生的进程几乎是一样的。主要区别是通过child.send方法实现的跨进程通信。
这个send事件发送一个消息字符串和一个可选的句柄。手柄可以是五种类型之一:net.Socket, net.Server, net.Native, dgram.Socket, or dgram.Native。乍一看,要适应这些不同类型的方法可能令人望而生畏。幸运的是,Node.js 会为您转换句柄类型。这种处理也适用于衍生进程的响应。
消息发送到子流程时发生的事件是'message'事件。在这个解决方案中,您看到'message'事件包含消息的命名类型。首先,您发送了一条问候消息。接下来,您发送了一个服务器对象。一旦事件被确定为服务器,这个对象就被绑定到“connection”事件。然后,您可以像在单个流程模块中一样处理连接。
摘要
在本章中,您研究并实现了 Node.js 固有的两个重要模块的解决方案:事件和子流程。
在 events 模块中,您首先创建了一个自定义事件,然后解决了如何使用侦听器绑定到该事件。之后,您研究了当您只需要一个绑定时添加一次性事件侦听器的特殊情况。最后,您可以看到如何利用 Node.js 事件模块,通过使用事件来驱动功能,可以非常明显地减少回调的数量。
在本章的第二部分,您检查了子流程模块。您首先看到了如何生成一个子进程来运行主进程之外的命令。然后您看到了如何通过使用exec和execFile方法直接运行 shell 命令和文件。这些都是从 spawn 进程派生出来的,正如.fork()进程一样,spawn 是 spawn 的一个特例,它允许简单的进程间通信,为多进程 Node.js 应用提供了无限的可能性。