From be6e07bed9c80d70bd5540e53189b4ede6aa65cb Mon Sep 17 00:00:00 2001 From: MaleDong Date: Tue, 19 Jun 2018 12:30:15 +0800 Subject: [PATCH 1/4] Translations of files in `locale\zh-cn\docs` --- locale/zh-cn/docs/es6.md | 45 ++ .../guides/anatomy-of-an-http-transaction.md | 354 +++++++++++++ .../docs/guides/backpressuring-in-streams.md | 445 +++++++++++++++++ .../docs/guides/blocking-vs-non-blocking.md | 106 ++++ .../guides/buffer-constructor-deprecation.md | 226 +++++++++ .../docs/guides/debugging-getting-started.md | 198 ++++++++ locale/zh-cn/docs/guides/domain-postmortem.md | 347 +++++++++++++ .../docs/guides/dont-block-the-event-loop.md | 471 ++++++++++++++++++ .../guides/event-loop-timers-and-nexttick.md | 344 +++++++++++++ .../docs/guides/getting-started-guide.md | 28 ++ locale/zh-cn/docs/guides/index.md | 31 ++ .../zh-cn/docs/guides/nodejs-docker-webapp.md | 237 +++++++++ .../docs/guides/publishing-napi-modules.md | 39 ++ locale/zh-cn/docs/guides/simple-profiling.md | 220 ++++++++ locale/zh-cn/docs/guides/timers-in-node.md | 125 +++++ .../working-with-different-filesystems.md | 92 ++++ locale/zh-cn/docs/index.md | 44 ++ locale/zh-cn/docs/meta/topics/dependencies.md | 78 +++ 18 files changed, 3430 insertions(+) create mode 100644 locale/zh-cn/docs/es6.md create mode 100644 locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md create mode 100644 locale/zh-cn/docs/guides/backpressuring-in-streams.md create mode 100644 locale/zh-cn/docs/guides/blocking-vs-non-blocking.md create mode 100644 locale/zh-cn/docs/guides/buffer-constructor-deprecation.md create mode 100644 locale/zh-cn/docs/guides/debugging-getting-started.md create mode 100644 locale/zh-cn/docs/guides/domain-postmortem.md create mode 100644 locale/zh-cn/docs/guides/dont-block-the-event-loop.md create mode 100644 locale/zh-cn/docs/guides/event-loop-timers-and-nexttick.md create mode 100644 locale/zh-cn/docs/guides/getting-started-guide.md create mode 100644 locale/zh-cn/docs/guides/index.md create mode 100644 locale/zh-cn/docs/guides/nodejs-docker-webapp.md create mode 100644 locale/zh-cn/docs/guides/publishing-napi-modules.md create mode 100644 locale/zh-cn/docs/guides/simple-profiling.md create mode 100644 locale/zh-cn/docs/guides/timers-in-node.md create mode 100644 locale/zh-cn/docs/guides/working-with-different-filesystems.md create mode 100644 locale/zh-cn/docs/index.md create mode 100644 locale/zh-cn/docs/meta/topics/dependencies.md diff --git a/locale/zh-cn/docs/es6.md b/locale/zh-cn/docs/es6.md new file mode 100644 index 0000000000000..9757703823f4c --- /dev/null +++ b/locale/zh-cn/docs/es6.md @@ -0,0 +1,45 @@ +--- +title: ECMAScript 2015 (ES6) 以及未来展望 +layout: docs.hbs +--- +# ECMAScript 2015 (ES6) 以及未来展望 + +Node.js 不是为 [V8](https://developers.google.com/v8/) 引擎构建的。为了与此引擎保持实时更新,我们确保来源于 [JavaScript ECMA-262 specification](http://www.ecma-international.org/publications/standards/Ecma-262.htm) 的新功将被及时地给予 Node.js 开发者们,同时继续包含性能以及稳定性上的更新。 + +所有的 ECMAScript 2015 (ES6) 功能将被分为三个部分 **shipping**,**staged** 和 **in progress**: + +* 所有 **shipping**:在 V8 引擎中认为是稳定的,转变成 **Node.js 默认**,并且 **不会** 需要任何运行时标记。 +* **Staged**:这些是将要完成的特性,并且在 V8 团队看来并不一定稳定,需要一个 `--harmony` 标记。 +* **In progress**:这些特性可以通过各自单独的 harmony 标记被激活使用。除非为了测试用途,否则绝不鼓励这样做。值得注意的是这些标记是借由 V8 引擎公开,将来或许会有潜在的变化而不会有任何免责声明或者协议。 + +## 默认情况下什么特性随着 Node.js一起发布? + +[node.green](http://node.green) 提供了非常完整、几乎涵盖了不同版本的 Node.js 中所支持的 ECMAScript 特性。它基于 kangax 的兼容性对照表构建。 + +## 有哪些特性在开发中? + +新特性源源不断地被加入 V8 引擎。一般说来,虽然具体的时间未知,但我们总希望他们将来在 Node.js 中有所体现。 + +在每个 Node.js 发布版中,你可以通过 greeping 配上 `--v8-options` 参数罗列出全部处于 *in progress* 状态的特性功能。请注意:他们尚未完成,可能因为 V8 引擎自带的功能而夭折。所以使用这些特性时会冒风险。 + +```bash +node --v8-options | grep "in progress" +``` + +## 对于一个特定的功能,它的特性又如何呢? + +V8 团队现正在努力持续工作,他们借助 EcmaScript 5 或是更早时代的语言规范,利用转译或是本地相当的技术以提高新语言的功能特性,使之达到平衡。目前研发进度记录可以在 [six-speed](https://fhinkel.github.io/six-speed) 查询到,它会告诉你 ES2015 和 ESNext 的性能究竟如何(和本地的 ES5 同等技术相比)。 + +特性优化的工作也随着 ES2015 到来,以上计划是通过 [执行计划](https://docs.google.com/document/d/1EA9EbfnydAmmU_lM8R_uEMQ-U_v4l9zulePSBkeYWmY),有一个 V8 团队在那儿收集整理相关信息,并协调需要提高性能、设计追踪问题的文稿等部门。 + +## 我有我自己的基本框架,可以平衡 --harmony,所以我可以移除这个标记吗? + +目前来说,`--harmony` 在 Node.js 的作用是让 **staged** 特性起作用。它本质上等同于 `--es_staging`。如上所述,有些特性尚未完全确认是稳定的,所以如果你希望一个安全的环境(尤其是在发布环境),你应该考虑移除这个运行时的环境标记,直到它在 V8 中以默认形式发布,或者在 Node.js 中落地。如果你开启了这个开关,你应该有对未来 Node.js 升级而造成代码破坏(无法正常工作)的准备,比如 V8 引擎做了更改,它的语法变得更接近标准。 + +## 我怎么知道某个特定版本的 Node.js 发布是随着哪个版本的 V8 引擎呢? + +Node.js 提供了一个简单的方法以列出所有依赖项,以及通过 `process` 全局对象,借助特定的二进制包发布的不同版本。由于是 V8 引擎,在你的终端输入以下命令就可以获取相关版本号: + +```bash +node -p process.versions.v8 +``` diff --git a/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md b/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md new file mode 100644 index 0000000000000..ade359fc9f9ea --- /dev/null +++ b/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md @@ -0,0 +1,354 @@ +--- +title: HTTP 传输解析 +layout: docs.hbs +--- + +# 一次 HTTP 传输解析 + +本指南的宗旨将让你对 HTTP 传输处理有一个清晰完整的了解。在不考虑特定编程语言及开发环境下,我们假设你已经知道在一般情况下 HTTP 是如何进行工作的。我们同样假定你熟悉 Node.js 的 [`EventEmitters`][] 和 [`Streams`][]。当然,如果你确实不了解它们,我们强烈建议你把以上列出的内容快速而完整地阅读一遍。 + +## 创建一个后台服务 + +任何网络服务应用程序总是要先创建一个服务对象。这在 Node.js 中通常通过 [`createServer`][] 方法。 + +```javascript +const http = require('http'); + +const server = http.createServer((request, response) => { + // magic happens here! +}); +``` + +每当有 HTTP 请求到达服务器时,[`createServer`][] 中传入的函数就被自动执行。所以这个函数也被称为是请求处理函数。实际上,由 [`createServer`][] 构造函数返回的 [`Server`][] 对象是一个 [`EventEmitter`][],我们在这里仅是对创建 `server` 和对它添加监听事件进行了简化处理。 + +```javascript +const server = http.createServer(); +server.on('request', (request, response) => { + // the same kind of magic happens here! +}); +``` + +当一个 HTTP 到达服务端,node 调用 request 处理程序,并产生一些唾手可得的对象用以处理传输, +这些对象就是 `request` 和 `response`。我们马上会讲到。 + +实际上,为了处理请求,[`listen`][] 方法需要在 `server` 对象上被显式调用。在大多数情况下,你只要把端口号作为参数传入 `listen` 方法中,作为监听端口即可。当然也有一些其它选项,具体可以参考 [API 参考文档][]。 + +## 方法, 访问地址以及请求头 + +当处理一个请求时,第一件事你需要做的是看一下这个方法和其访问地址,以此决定你到底采取何种合理的行为。Node 通过把这些行为属性附加到 `request` 对象上,使得我们处理起来相对而言可以轻松一些。 + +```javascript +const { method, url } = request; +``` +> **注意:** `request` 对象是 [`IncomingMessage`][] 的一个实例。 + +这里的 `method` 总是一个普通的 HTTP 方法动作行为 (verb),`url` 是指没有服务器协议和 +端口号的完整访问地址。一个典型的访问地址通常意味着包括第三个斜杠以及后面的所有内容。 + +请求头也不是很难得到,它们也在 `request` 对象里,称为 `headers`。 + +```javascript +const { headers } = request; +const userAgent = headers['user-agent']; +``` + +非常重要的一点是:所有的请求头全是小写字母,而不管实际上它们是怎么进行传输的。所以在无论任何 +情况下,解析请求头就得到了简化。 + +如果一些请求头出现重复,它们的值不是被覆盖,就是通过英文分号进行分割。究竟哪种方式取决于 +具体的信息头。在某些情况下这可能出现问题,所以我们还可以直接使用 [`rawHeaders`][]。 + +## 请求体 + +当接受到了一个 `POST` 或者 `PUT` 请求时,请求体对于你的应用程序非常重要。相对于访问请求 +头而言,获取请求体有些麻烦。传入请求对象的 `request` 其实实现了 [`ReadableStream`][] 接口, +这个信息流可以被监听,或者与其它流进行对接。我们可以通过监听 `'data'` 和 `'end'` 事件从而把 +数据给取出来。 + +每次在 `'data'` 事件中触发抓获的数据块是一个 [`Buffer`][]。如果你已知是一个字符串对象,那么 +最好的方案就是把这些数据收集到一个数组中,然后在 `'end'` 事件中拼接并且把它转化为字符串。 + +```javascript +let body = []; +request.on('data', (chunk) => { + body.push(chunk); +}).on('end', () => { + body = Buffer.concat(body).toString(); + // at this point, `body` has the entire request body stored in it as a string +}); +``` + +> **注意:** 或许你感到这样做有些枯燥乏味,不过大多数情况下你算是幸运的。 +因为 [`npm`][] 上实在有太多的诸如 [`concat-stream`][] 和 [`body`][] 一类类库屏蔽了部分 +细节逻辑而替你做了这些事情。当然,对于你而言在使用这些类库前知道它们到底干了什么非常重要,这 +就是你为什么需要读这篇文章! + +## 一笔带过关于错误的一些信息 + +因为 `request` 是一个 [`ReadableStream`][] 对象,它同样也是 [`EventEmitter`][] 对象。所以当有错误发生时,表现的行为是很相像的。当有错误在 `request` 流上发生时,它会自动激发自身的 `'error'` 事件。**如果你不去处理监听这个事件,此错误将被*抛出*,这导致你的程序崩溃。** 你应该无论如何都要添加 `'error'` 事件去监听你的请求对象,哪怕你只是做一个日志或者用你自己的独有方式去处理(当然,最佳的处理方式是返回一些出错的信息,这已是后话了)。 + +```javascript +request.on('error', (err) => { + // This prints the error message and stack trace to `stderr`. + console.error(err.stack); +}); +``` + +当然还有一些其它的方法来 [处理错误][],诸如其它的抽象化概念和工具等。但是你总是要意识到错误的确会发生,所以你应当处理它们。 + +## 我们已经聊得那么多了 + +直到现在,我们已经谈到了如何创建一个对象,如果从请求中获取方法,请求地址,请求头和请求体。当我们把它们组合到一起,它就看上去是这个样子: + +```javascript +const http = require('http'); + +http.createServer((request, response) => { + const { headers, method, url } = request; + let body = []; + request.on('error', (err) => { + console.error(err); + }).on('data', (chunk) => { + body.push(chunk); + }).on('end', () => { + body = Buffer.concat(body).toString(); + // At this point, we have the headers, method, url and body, and can now + // do whatever we need to in order to respond to this request. + }); +}).listen(8080); // Activates this server, listening on port 8080. +``` + +如果我们运行这个示例代码,我们只能 *接收* 到请求但得不到 *回应*。实际上,如果你在浏览器内运行这个示例,你的请求只会超时,因为服务器那边根本没有返回给客户端任何东西。 + +谈了那么就,我们都还没有说到 `response` 对象。它是一个 [`ServerResponse`][] 实例,而 ServerRespose 又是 [`WritableStream`][]。它包含了很多方法可以用以把数据返回给客户端。我们下面就将涉及到此议题。 + + +## HTTP 状态码 + +如果你嫌麻烦不想设置它,返回客户端的默认状态码总是 200。当然,不是每个 HTTP 返回码必须都是 200,在某些情况下你一定希望返回一个不同的状态码,所以你应该设置 `statusCode` 属性。 + +```javascript +response.statusCode = 404; // Tell the client that the resource wasn't found. +``` + +我们同样也有其它捷径去做这件事,后面我们会很快看到。 + +## 设置响应头 + +响应头通过一个 [`setHeader`][] 的属性很方便的设置。 + +```javascript +response.setHeader('Content-Type', 'application/json'); +response.setHeader('X-Powered-By', 'bacon'); +``` + +设置响应头时,它们的名字是大小写敏感的。如果你重复设置响应头,最后一次设置的值也就是系统得到的值。 + +## 显示发送头数据 + +我们之前讨论的设置响应头以及状态码的方法建立在你使用“隐式设置”的方式,这意味着你在发送消息体之前依赖于 node 发送请求头。 +如果你愿意,你可以为返回流重写响应头。为做到这点,你可以使用 [`writeHead`][] 方法向消息流重写状态码和响应头。 + +```javascript +response.writeHead(200, { + 'Content-Type': 'application/json', + 'X-Powered-By': 'bacon' +}); +``` + +一旦设置了响应头(无论是隐式还是显式设置),你已经为发送返回数据做好了准备。 + +## 发送返回体 + +既然 `response` 对象是一个 [`WritableStream`][],向客户端写入返回体只是一个普通的流方法的问题。 + +```javascript +response.write(''); +response.write(''); +response.write('

Hello, World!

'); +response.write(''); +response.write(''); +response.end(); +``` + +消息流上的 `end` 方法同时还可以带入一些可选数据作为流上最后需要发送的一些数据,所以我们可以简单地把以上的代码做如下形式的简化: + +```javascript +response.end('

Hello, World!

'); +``` + +> **注意:** 你只有在开始向返回体写数据 *之前* 设置状态和响应头,这点很重要。 +因为响应头信息总是在消息体前到达。 + +## 另一件一笔带过关于错误的事 + + `response` 返回流同样也会触发 `'error'` 事件,某种程度上说你不得不自己去处理它。之前全部关于 `request` 消息流出错的处理方法在这里也同样适用。 + +## 把之前所学的全部整合到一起 + +现在既然我们已经学了如何处理 HTTP 返回信息,现在让我们把这些零碎东西组合到一起。基于先前的示例代码,我们将作出一个服务端,使它可以将从用户接受到的全部信息返回给用户。我们将通过 `JSON.stringify` 对消息数据进行格式化。 + +```javascript +const http = require('http'); + +http.createServer((request, response) => { + const { headers, method, url } = request; + let body = []; + request.on('error', (err) => { + console.error(err); + }).on('data', (chunk) => { + body.push(chunk); + }).on('end', () => { + body = Buffer.concat(body).toString(); + // BEGINNING OF NEW STUFF + + response.on('error', (err) => { + console.error(err); + }); + + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + // Note: the 2 lines above could be replaced with this next one: + // response.writeHead(200, {'Content-Type': 'application/json'}) + + const responseBody = { headers, method, url, body }; + + response.write(JSON.stringify(responseBody)); + response.end(); + // Note: the 2 lines above could be replaced with this next one: + // response.end(JSON.stringify(responseBody)) + + // END OF NEW STUFF + }); +}).listen(8080); +``` + +## 服务器响应的示例代码 + +让我们简化之前的代码,做一个可以有响应的简单的服务端。它同样也可以把接受到的任何信息返回给客户端。我们所要做的就是从请求流中把请求数据取出,然后原样写回到返回流中即可。就如我们之前做的那么简单。 + +```javascript +const http = require('http'); + +http.createServer((request, response) => { + let body = []; + request.on('data', (chunk) => { + body.push(chunk); + }).on('end', () => { + body = Buffer.concat(body).toString(); + response.end(body); + }); +}).listen(8080); +``` + +现在让我们调整一下,我们只对以下条件应答: + +* 请求方法是 POST 方式。 +* 访问路径是 `/echo`。 + +其它任何情况均返回 404。 + +```javascript +const http = require('http'); + +http.createServer((request, response) => { + if (request.method === 'POST' && request.url === '/echo') { + let body = []; + request.on('data', (chunk) => { + body.push(chunk); + }).on('end', () => { + body = Buffer.concat(body).toString(); + response.end(body); + }); + } else { + response.statusCode = 404; + response.end(); + } +}).listen(8080); +``` + +> **注意:** 为了检查请求路径,我们设计了一个路由格式。 +其它形式的路由 `switch`,简单的可以通过 `switch` 的形式检查,复杂的诸如 [`express`][] 框架,如果你正在寻找路由而不需要做其它事情,简单用 [`router`][]。 + +太棒了!现在我们进一步简化它。记住,`request` 是一个 [`ReadableStream`][]对象,`response`对象是一个 [`WritableStream`][]。那意味着我们可以使用 [`pipe`][]直接从一个流转到另外一个流。那的确是我们需要的: + +```javascript +const http = require('http'); + +http.createServer((request, response) => { + if (request.method === 'POST' && request.url === '/echo') { + request.pipe(response); + } else { + response.statusCode = 404; + response.end(); + } +}).listen(8080); +``` + +就是这样! + +我们还尚未完全完成,如之前多次谈到,错误随时可能发生,所以我们需要处理它们。 + +为了处理请求流上的错误,我们把错误记录到 `stderr` 对象中,然后回发一个 400 的代码表示 `错误请求`。在现实生活中,我们想检查分析错误,了解它们正确的状态码以及具体出错信息。具体可以参考 [`Error` 文档信息][] + +对于返回,我们把错误日志记录到 `stderr` 中。 + +```javascript +const http = require('http'); + +http.createServer((request, response) => { + request.on('error', (err) => { + console.error(err); + response.statusCode = 400; + response.end(); + }); + response.on('error', (err) => { + console.error(err); + }); + if (request.method === 'POST' && request.url === '/echo') { + request.pipe(response); + } else { + response.statusCode = 404; + response.end(); + } +}).listen(8080); +``` + +我们现在已经涉及到了大部分基本的 HTTP 请求知识,此时此刻,你应该已经具备了: + +* 实例化一个 HTTP 服务,它有一个处理请求的函数,并对某个特定端口进行监听。 +* 从 `request` 中获取请求头,访问路径,方法以及消息体。 +* 让路由决定依赖于访问路径,或者在 `request` 对象其它数据中。 +* 通过 `response` 对象发送响应头,HTTP 状态码以及消息体。 +* 通过 `request` 对象与 `response` 对象对接,传输数据。 +* 在 `request` 和 `response` 流中处理错误。 + +从这些基础知识中,关于 Node.js 的 HTTP 服务一些实用案例已经逐步被构建起来,API 文档还提供了大量其它的说明,所以请详细阅读 [`EventEmitters`][],[`Streams`][] 以及 [`HTTP`][]。 + + + +[`EventEmitters`]: https://nodejs.org/api/events.html +[`Streams`]: https://nodejs.org/api/stream.html +[`createServer`]: https://nodejs.org/api/http.html#http_http_createserver_requestlistener +[`Server`]: https://nodejs.org/api/http.html#http_class_http_server +[`listen`]: https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback +[API 参考文档]: https://nodejs.org/api/http.html +[`IncomingMessage`]: https://nodejs.org/api/http.html#http_class_http_incomingmessage +[`ReadableStream`]: https://nodejs.org/api/stream.html#stream_class_stream_readable +[`rawHeaders`]: https://nodejs.org/api/http.html#http_message_rawheaders +[`Buffer`]: https://nodejs.org/api/buffer.html +[`concat-stream`]: https://www.npmjs.com/package/concat-stream +[`body`]: https://www.npmjs.com/package/body +[`npm`]: https://www.npmjs.com +[`EventEmitter`]: https://nodejs.org/api/events.html#events_class_eventemitter +[处理错误]: https://nodejs.org/api/errors.html +[`domains`]: https://nodejs.org/api/domain.html +[`ServerResponse`]: https://nodejs.org/api/http.html#http_class_http_serverresponse +[`setHeader`]: https://nodejs.org/api/http.html#http_response_setheader_name_value +[`WritableStream`]: https://nodejs.org/api/stream.html#stream_class_stream_writable +[`writeHead`]: https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers +[`express`]: https://www.npmjs.com/package/express +[`router`]: https://www.npmjs.com/package/router +[`pipe`]: https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options +[`Error` 文档信息]: https://nodejs.org/api/errors.html +[`HTTP`]: https://nodejs.org/api/http.html diff --git a/locale/zh-cn/docs/guides/backpressuring-in-streams.md b/locale/zh-cn/docs/guides/backpressuring-in-streams.md new file mode 100644 index 0000000000000..664c7425dfa7a --- /dev/null +++ b/locale/zh-cn/docs/guides/backpressuring-in-streams.md @@ -0,0 +1,445 @@ +--- +title: 数据流中的积压问题 +layout: docs.hbs +--- + +# 数据流中的背压问题 + +通常在数据处理的时候我们会遇到一个普遍的问题:[`背压`][],意思是在数据传输过程中有一大堆数据在缓存之后积压着。每次当数据到达结尾又遇到复杂的运算,又或者无论什么原因它比预期的慢,这样累积下来,从源头来的数据就会变得很庞大,像一个塞子一样堵塞住。 + +为解决这个问题,必须存在一种适当的代理机制,确保流从一个源流入另外一个的时候是平滑顺畅的。不同的社区组织针对他们各自的问题单独做了解决,好例子比如 Unix 的管道和 TCP 的 Socket。此问题经常与 _流控制_ 相关。在 Node.js 中,流已经是被采纳的解决方案。 + +此文的目的在于详细深入介绍什么是背压,并从代码角度详细解释在 Node.js 中,流是如何针对此问题进行处理的。本文的第二部分将对你在实现流的功能时给予最佳实战经验,以确保你的程序既安全又精简。 + +我们假定你对 Node.js 中的 [`背压`][],[`缓存`][],[`事件发射器`][] 和 [`流`][] 的基本概念有一点了解。如果你尚未完整阅读过 API 文档,那么最好是先看一下相关 API 说明,它也会有助于你扩展理解本文的主旨。 + +## 处理数据中遇到的问题 + +在一个计算机系统中,通过管道,socket 和 信号量将数据从一个进程传到另外一个进程中。在 Node.js 中,我们发明了一个类似的机制,它称为 [`流`][]。流太棒了!它们为 Node.js 做了太多的事情,而且内部代码库的每个角落都用到了那个模块。作为一个开发者,你也应该鼓励自己多去使用这个模块! + +```javascript +const readline = require('readline'); + +// process.stdin and process.stdout are both instances of Streams +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +rl.question('Why should you use streams? ', (answer) => { + console.log(`Maybe it's ${answer}, maybe it's because they are awesome! :)`); + + rl.close(); +}); +``` + +通过流实现背压机制的一个很好的例子是通过比较内部系统工具可以证明一个很大的优化。它通过 Node.js 的[`流`][]实现。 + +在以下场景中,我们将拿一个巨大的文件(大概有 9gb 那么大),然后用熟悉的 [`zip(1)`][] 的工具压缩。 + +```bash +$ zip The.Matrix.1080p.mkv +``` + +当这个终端还需要等待一些时间来完成时,我们另起一个终端运行 Node.js 的模块: [`zlib`][],它对 [`gzip(1)`][] 进行了包装。 + +```javascript +const gzip = require('zlib').createGzip(); +const fs = require('fs'); + +const inp = fs.createReadStream('The.Matrix.1080p.mkv'); +const out = fs.createWriteStream('The.Matrix.1080p.mkv.gz'); + +inp.pipe(gzip).pipe(out); +``` + +现在尝试打开每个压缩的文件来测试结果。由 [`zip(1)`][] 压缩的文件会提醒你文件中断了,但通过 [`流`][] 的压缩在解压时无任何错误。 + +请注意:这个例子中我们使用 `.pipe()` 从一个数据源终端到另外一个终端,不过没有使用任何出错处理机制。如果一大堆数据出错了但是又要被接收, `可读` 和 `gzip` 流不会被销毁。 [`pump`][] 是一个工具类,如果有某个流发生错误或者关闭,它会自动销毁相关所有的流,在这个情况下是必须使用的! + +## 数据太多,速度太快 +有太多的例子证明有时 [`可读`][] 传输给 [`可写`][] 的速度远大于它接受和处理的速度! + +如果发生了这种情况,消费者开始为后面的消费而将数据列队形式积压起来。写入队列的时间越来越长,也正因为如此,更多的数据不得不保存在内存中知道整个流程全部处理完毕。 + +写入磁盘的速度远比从磁盘读取数据慢得多,因此,当我们试图压缩一个文件并写入磁盘时,背压的问题也就出现了。因为写磁盘的速度不能跟上读磁盘的速度。 + +```javascript +// Secretly the stream is saying: "whoa, whoa! hang on, this is way too much!" +// Data will begin to build up on the read-side of the data buffer as +// `write` tries to keep up with the incoming data flow. +inp.pipe(gzip).pipe(outputFile); +``` +这就是为什么说背压机制很重要——如果背压机制不存在,进程将用完你全部的系统内存,从而对其它进程产生显著影响,它独占系统大量资源直到任务完成为止。 + +这最终导致一些问题: + +* 明显使得其它进程处理变慢 +* 太多繁重的垃圾回收 +* 内存耗尽 + +以下例子中我们把 `.write()` 函数的 [返回值][] 值取出,改成 `true`,这样明显地禁止了 Node.js 核心的背压的支持。在任何引用了 'modified' 二进制库的地方,我们探讨在不适用 `return ret;` 的情况下运行 `node` 二进制代码,并用 `return true;` 取代它。 + +## 过度的垃圾收集 + +让我们来看一个快速的基准。使用上面的同一个例子,我们进行两次试验以获得两个二进制文件的中位时间。 + + +```javascript + trial (#) | `node` binary (ms) | modified `node` binary (ms) +================================================================= + 1 | 56924 | 55011 + 2 | 52686 | 55869 + 3 | 59479 | 54043 + 4 | 54473 | 55229 + 5 | 52933 | 59723 +================================================================= +average time: | 55299 | 55975 +``` + +两者都跑一分钟,所以几乎没有什么区别。但让我们仔细看看我们的猜测是否正确。我们使用 Linux 工具 [`dtrace`][] 来评估 V8 垃圾回收机制发生了什么。 + +GC(垃圾回收器)测量表明一个完整的周期间隔一个由垃圾回收器进行扫描: + + +```javascript +approx. time (ms) | GC (ms) | modified GC (ms) +================================================= + 0 | 0 | 0 + 1 | 0 | 0 + 40 | 0 | 2 + 170 | 3 | 1 + 300 | 3 | 1 + + * * * + * * * + * * * + + 39000 | 6 | 26 + 42000 | 6 | 21 + 47000 | 5 | 32 + 50000 | 8 | 28 + 54000 | 6 | 35 +``` + +当两个进程同时运行,并似乎以同样的效率开始工作,在若干秒随着适当的背压开始变得有效率起来。它将 GC 负载扩展到每隔一定的 4-8 毫秒的间隔,直到数据传输结束。 + +但是,当背压机制处理不恰当,V8 垃圾回收机制开始变慢。一般情况下 GC 一分钟内进行 75 次回收,但是修改过的二进制库仅 36 次。 + +随着内存占用越来越多,缓慢和渐进的欠债也不断积累。随着数据的传输,在没有背压系统的情况下,每个块传输都使用更多的内存。 + +内存分配使用越多,GC 就越要照顾内存交换。内存交换得越多,GC 就需要考虑决定哪些内存可以被释放,并且要一直在大块内存中扫描独立区块,而这又要消耗更多的计算功率。 + +## 内存耗尽 + +为判断每个程序内存消耗,我们使用 `/usr/bin/time -lp sudo ./node ./backpressure-example/zlib.js` 单独计算每个进程所用时间。 + +这是普通程序输出结果: + + +```javascript +Respecting the return value of .write() +============================================= +real 58.88 +user 56.79 +sys 8.79 + 87810048 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 19427 page reclaims + 3134 page faults + 0 swaps + 5 block input operations + 194 block output operations + 0 messages sent + 0 messages received + 1 signals received + 12 voluntary context switches + 666037 involuntary context switches +``` + +虚拟内存占用的最大字节块消耗了 87.81 mb。 + +现在改变 [`.write()`][] 方法的 [返回值][],我们得到以下结果: + + +```javascript +Without respecting the return value of .write(): +================================================== +real 54.48 +user 53.15 +sys 7.43 +1524965376 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 373617 page reclaims + 3139 page faults + 0 swaps + 18 block input operations + 199 block output operations + 0 messages sent + 0 messages received + 1 signals received + 25 voluntary context switches + 629566 involuntary context switches +``` + +虚拟内存占用的最大的字节块达到了 1.52 gb。 + +没有合适的流来处理背压,就会产生一个内存占用的震级顺序——与同样的进程处理有着天壤之别! + +这个实验展示了如何精简以对你的计算系统进行精简,以及有效的资源消耗。现在,我们故意弄出一个故障看看它又是怎么工作的。 + +## 背压是怎么处理这些问题的? + +我们有不同的函数将数据从一个进程传入另外一个进程。在 Node.js 中,有一个内置函数称为 [`.pipe()`][],同样地,你们也可以使用 [其它工具包][]。最终,在这个进程的基本层面上我们有二个互不相关的组件:数据的 _源头_,和 _消费者_。 + +当 [`.pipe()`][] 被源调用之后,它通知消费者有数据需要传输。管道函数为事件触发建立了合适的积压封装。 + +在 Node.js 中,源头是一个 [`可读`][] 的流,消费者是 [`可写`][] 流(它们两者可能通过 [`Duplex`][] 或 [`Transform`][] 进行交互)。只不过这超出我们本文讨论范围了。 + +当背压被触发的一刹那,它可以被缩略成 [`可写`][] 的 [`.write()`][] 方法。返回函数值当然是根据一些条件所决定的。 + +在数据缓存超出了 [`水准值`][] 或者写入的列队处于繁忙状态,[`.write()`][] 会返回 `false`。 + +当 `false` 返回之后,背压系统介入了。它将暂停从任何发送数据的数据流中进入的 [`可读`][]。一旦数据流清空了, [`.drain()`][] 事件将被触发,消耗进来的数据流。 + +一旦队列全部处理完毕,背压机制将允许允许数据再次发送。在使用中的内存空间将自我释放,同时准备接收下一次的批量数据。 + +这个有效的举措允许一大堆锁住的内存可以为 [`.pipe()`][] 函数随时使用而并没有内存泄露、无限扩大的内存缓冲。同时垃圾回收器仅需要处理一处地方。 + +所以,背压既然如此重要,为什么还有理由说你没有听说过呢?显然答案很明显:Node.js 为你做了一切。 + +这太好了!不过当我们试图去理解如何实现我们自己的背压流,这却并不太好。 + +注意:对于大部分机器,存在着一个字节的大小用以决定一个缓存是否已经满了(不同机器此值有变化)。Node.js 将允许你设置你自己的 [`水准值`][]。但是通常来说,默认是设置为 16kb(16384,对于对象模型流而言是 16)。在某些实例中你或许想提高那个值,尽管去提高吧,但是也要小心使用! + +## `.pipe()` 的生命周期 + +为了对背压有一个更好的理解,这里有一副 [`可读`][] 流正通过 [管道][] 流入 [`可写`][] 流的整个生命周期图: + + +```javascript + +===================+ + x--> Piping functions +--> src.pipe(dest) | + x are set up during |===================| + x the .pipe method. | Event callbacks | + +===============+ x |-------------------| + | Your Data | x They exist outside | .on('close', cb) | + +=======+=======+ x the data flow, but | .on('data', cb) | + | x importantly attach | .on('drain', cb) | + | x events, and their | .on('unpipe', cb) | ++---------v---------+ x respective callbacks. | .on('error', cb) | +| Readable Stream +----+ | .on('finish', cb) | ++-^-------^-------^-+ | | .on('end', cb) | + ^ | ^ | +-------------------+ + | | | | + | ^ | | + ^ ^ ^ | +-------------------+ +=================+ + ^ | ^ +----> Writable Stream +---------> .write(chunk) | + | | | +-------------------+ +=======+=========+ + | | | | + | ^ | +------------------v---------+ + ^ | +-> if (!chunk) | Is this chunk too big? | + ^ | | emit .end(); | Is the queue busy? | + | | +-> else +-------+----------------+---+ + | ^ | emit .write(); | | + | ^ ^ +--v---+ +---v---+ + | | ^-----------------------------------< No | | Yes | + ^ | +------+ +---v---+ + ^ | | + | ^ emit .pause(); +=================+ | + | ^---------------^-----------------------+ return false; <-----+---+ + | +=================+ | + | | + ^ when queue is empty +============+ | + ^------------^-----------------------< Buffering | | + | |============| | + +> emit .drain(); | ^Buffer^ | | + +> emit .resume(); +------------+ | + | ^Buffer^ | | + +------------+ add chunk to queue | + | <---^---------------------< + +============+ +``` + +注意:如果你创建一些管道准备把一些流串联起来从而操纵数据,你应该实现 [`Transform`][] 流。 + +在这种情况下,从 [`可读`][] 流中的输出进入 [`Transform`][],并且会被管道输送进入 [`可写`][]。 + +```javascript +Readable.pipe(Transformable).pipe(Writable); +``` + +背压将被自动应用,但是同时请注意输入和输出 [`Transform`][] 的 `水准值` 可以手动控制,并且会影响到背压系统。 + +## 背压行为的准则 + +从 [Node.js v0.10][] 开始,[`流`][] 类借助带有下划线一些相关函数([`._read()`][] 和 [`._write()`][]),提供了访问 [`.read()`][] 或[`.write()`][] 的能力。 + +这里有一些准则文档可供参考: [实现可读的流][] 和 [实现可写的流][]。我们假设你可以把这些文章已经读过了,下个章节将做稍许的深入讲解。 + +## 实现用户自定义流须知 + +流的黄金法则是 __总是接受背压__。作为最佳实践的构成是不矛盾的实践。只要你小心避免与内部背压支持冲突的行为,你可以确信你在遵循良好的实践。 + +一般而说。 + +1. 没有特殊要求下,绝对不要用 `.push()`。 +2. 在流返回 `false` 后不要调用 `.write()` 方法,而是等待 'drain'。 +3. 流在不同的 Node.js 版本和库中是有变化的。小心你的测试。 + +注意:关于第三点,构建浏览器流的一个难以置信的方法是使用 [`可读流`][]。Rodd Vagg 曾经写过一篇 [大作][] 详细描述这个工具库。简而言之,它为 [`可读`][] 流提供了自动可销毁降解的类型,并且支持旧版的 Node.js 和 浏览器。 + +## 对于可读流的规则 + +迄今为止,我们已经看了 [`.write()`][] 方法对于背压的影响,并且过多关注在 [`可写`][] 流上,因为 Node.js 的功能,数据从 [`可读`][] 流到 [`可写`][] 流。但是正如我们在数据流传输过程中我们观察到,源和 [`可读`][] 目标一样重要, [`可读`][] 流对于背压是如何处理的至关重要。 + +这两个过程相互依赖地进行有效沟通,如果 [`可读`][] 流在 [`可写`][] 流需要它停止发送数据的时候忽略了,那么当 [`.write()`][] 方法返回时,会产生问题。 + +所以,除了谨慎对待 [`.write()`][] 方法,我们同样要小心在 [`._read()`][] 方法中使用 [`.push()`][] 方法的返回值。如果 [`.push()`][] 方法返回一个 `false`,流就会停止从源读数据。否则,它就不会停止而继续读下去。 + +这里有个糟糕的使用 [`.push()`][] 的例子: + +```javascript +// This is problematic as it completely ignores return value from push +// which may be a signal for backpressure from the destination stream! +class MyReadable extends Readable { + _read(size) { + let chunk; + while (null !== (chunk = getNextChunk())) { + this.push(chunk); + } + } +} +``` + +另外,从定制流之外,忽略背压简直可笑至极。在以下反例中,代码仅关注数据是否到达(通过 [`.data` 事件 ][] 订阅): + +```javascript +// This ignores the backpressure mechanisms Node.js has set in place, +// and unconditionally pushes through data, regardless if the +// destination stream is ready for it or not. +readable.on('data', (data) => + writable.write(data) +); +``` + +## 关于可写流的规则 + +重新调用 [`.write()`][] 方法根据一些条件可能返回 true 或者 false。幸运地是,当我们构建属于自己的 [`可写`][] 流的时候, [`流状态机`][] 会处理我们的回调,并且决定什么时候处理背压并且为我们简化数据流。 + +但是当我们需要直接使用 [`可写`][] 流时,我们必须考虑 [`.write()`][] 方法返回的值,并且注意到以下一些情况: + +* 如果写队列确实繁忙,[`.write()`][] 方法将返回 false。 +* 如果数据块太大, [`.write()`][] 方法将返回 false(限定通过 [`水准值`][] 决定)。 + + +```javascript +// This writable is invalid because of the async nature of JavaScript callbacks. +// Without a return statement for each callback prior to the last, +// there is a great chance multiple callbacks will be called. +class MyWritable extends Writable { + _write(chunk, encoding, callback) { + if (chunk.toString().indexOf('a') >= 0) + callback(); + else if (chunk.toString().indexOf('b') >= 0) + callback(); + callback(); + } +} + +// The proper way to write this would be: + if (chunk.contains('a')) + return callback(); + else if (chunk.contains('b')) + return callback(); + callback(); +``` + +在实现 [`._writev()`][] 方法时还有其它一些东西值得考虑。此函数与 [`.cork()`][] 耦合,但是编写代码的时有一个容易犯的错误: + +```javascript +// Using .uncork() twice here makes two calls on the C++ layer, rendering the +// cork/uncork technique useless. +ws.cork(); +ws.write('hello '); +ws.write('world '); +ws.uncork(); + +ws.cork(); +ws.write('from '); +ws.write('Matteo'); +ws.uncork(); + +// The correct way to write this is to utilize process.nextTick(), which fires +// on the next event loop. +ws.cork(); +ws.write('hello '); +ws.write('world '); +process.nextTick(doUncork, ws); + +ws.cork(); +ws.write('from '); +ws.write('Matteo'); +process.nextTick(doUncork, ws); + +// as a global function +function doUncork(stream) { + stream.uncork(); +} +``` + +[`.cork()`][] 方法可以让我们调用任意多次,我们只需小心调用 [`.uncork()`][] 方法使得它可以正常流入。 + +## 总结 + +流经常作为一个模块用于 Node.js 中,对于内部的系统结构而言非常重要。对于开发者而言,可以通过 Node.js 扩展连接应答系统。 + +现在我们希望你有能力进行故障排除,记住了是如何为你的 [`可写`][] 和 [`可读`][] 流编写背压处理的。并且你还可以把这些知识分享给你的同事和朋友们。 + +在此之后请仔细阅读更多的有关 [`流`][] 其它 API 函数,这样有助于当你在构建 Node.js 的应用程序之时更好地理解关于流的能力。 + + +[`流`]: https://nodejs.org/api/stream.html +[`缓存`]: https://nodejs.org/api/buffer.html +[`事件发射器`]: https://nodejs.org/api/events.html +[`可写`]: https://nodejs.org/api/stream.html#stream_writable_streams +[`可读`]: https://nodejs.org/api/stream.html#stream_readable_streams +[`Duplex`]: https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams +[`Transform`]: https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams +[`zlib`]: https://nodejs.org/api/zlib.html +[`.drain()`]: https://nodejs.org/api/stream.html#stream_event_drain +[`.data` 事件 ]: https://nodejs.org/api/stream.html#stream_event_data +[`.read()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_read_size +[`.write()`]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback +[`._read()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_read_size_1 +[`._write()`]: https://nodejs.org/docs/latest/api/stream.html#stream_writable_write_chunk_encoding_callback_1 +[`._writev()`]: https://nodejs.org/api/stream.html#stream_writable_writev_chunks_callback +[`.cork()`]: https://nodejs.org/api/stream.html#stream_writable_cork +[`.uncork()`]: https://nodejs.org/api/stream.html#stream_writable_uncork + +[`.push()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_push_chunk_encoding + +[实现可写的流]: https://nodejs.org/docs/latest/api/stream.html#stream_implementing_a_writable_stream +[实现可读的流]: https://nodejs.org/docs/latest/api/stream.html#stream_implementing_a_readable_stream + +[其它工具包]: https://github.com/sindresorhus/awesome-nodejs#streams +[`背压`]: https://en.wikipedia.org/wiki/Back_pressure#Backpressure_in_information_technology +[Node.js v0.10]: https://nodejs.org/docs/v0.10.0/ +[`水准值`]: https://nodejs.org/api/stream.html#stream_buffering +[返回值]: https://github.com/nodejs/node/blob/55c42bc6e5602e5a47fb774009cfe9289cb88e71/lib/_stream_writable.js#L239 + +[`可读流`]: https://github.com/nodejs/可读流 +[大作]:https://r.va.gg/2014/06/why-i-dont-use-nodes-core-stream-module.html + +[`dtrace`]: http://dtrace.org/blogs/about/ +[`zip(1)`]: https://linux.die.net/man/1/zip +[`gzip(1)`]: https://linux.die.net/man/1/gzip +[`流状态机`]: https://en.wikipedia.org/wiki/Finite-state_machine + +[`.pipe()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options +[管道]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options +[`pump`]: https://github.com/mafintosh/pump diff --git a/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md b/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md new file mode 100644 index 0000000000000..b85587e97de3c --- /dev/null +++ b/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md @@ -0,0 +1,106 @@ +--- +title: 阻塞对比非阻塞一览 +layout: docs.hbs +--- + +# 阻塞对比非阻塞一览 + +本概论涵盖了在 Node.js 中 **阻塞** and **非阻塞** 的区别,同时也会牵涉到时间轮询和 libuv 方面,不需要先行了解这些方面的知识也可以继续阅读。我们假定读者对于 JavaScript 语言和 Node.js 的回调机制有一个基本的了解。 + +> "I/O" 指的是系统磁盘和由 [libuv](http://libuv.org/) 支持的网络之间的交互。 + +## 阻塞 + +**阻塞** 是说 Node.js 中其它的 JavaScript 命令必须等到一个非 JavaScript 操作完成之后才可以执行。这是因为当 **阻塞** 发生时,事件机制无法继续运行JavaSCript。 + +在 Node.js 中,JavaScript由于 CPU 密集操作而表现不佳。而不是等待非 JavaScript操作 (例如I/O)。这被称为 **阻塞**。在 Node.js 基本类库中,使用 libuv 的同步方法大多数都是 **阻塞** 的。原生方法也可能是 **阻塞** 的。 + +所有在 Node.js 中提供的 I/O 方法也包括异步版本,它们都是 **非阻塞** 的,接受回调函数。一些方法同时也具备 **阻塞** 功能,它们的名字结尾都以 `Sync` 结尾。 + + +## 代码比较 + +**阻塞** 方法执行起来是 **同步地**,但是 **非阻塞** 方法执行起来是 **异步地**。 +如果你使用文件系统模块读取一个文件,同步方法看上去如下: + +```js +const fs = require('fs'); +const data = fs.readFileSync('/file.md'); // blocks here until file is read +``` + +这是一个与之功能等同的 **异步** 版本示例: + +```js +const fs = require('fs'); +fs.readFile('/file.md', (err, data) => { + if (err) throw err; +}); +``` + +第一个示例看上去比第二个似乎简单些,但是有一个缺陷:第二行语句会 **阻塞** 其它 JavaScript 语句的执行直到整个文件全部读取完毕。注意在同步版本的代码中,任何异常都会抛出,会导致整个程序崩溃。在异步版本示例代码中,它由作者来决定是否抛出异常。 + +让我们扩展一点我们的同步代码: + +```js +const fs = require('fs'); +const data = fs.readFileSync('/file.md'); // blocks here until file is read +console.log(data); +// moreWork(); will run after console.log +``` + +这是一个类似的,但是功能上不等同的异步代码示例版本: + +```js +const fs = require('fs'); +fs.readFile('/file.md', (err, data) => { + if (err) throw err; + console.log(data); +}); +// moreWork(); will run before console.log +``` + +第一个示例代码中, `console.log` 将在 `moreWork()` 之前被调用。在第二个例子中, `fs.readFile()` 因为是 **非阻塞** 的,所以 JavaScript 会继续执行, `moreWork()` 将被首先调用。`moreWork()` 无需等待文件读完而先行执行完毕,这对于高效吞吐来说是一个绝佳的设计。 + + +## 并行和吞吐 + +在 Node.js 中 JavaScript 的执行是单线程的,所以并行与事件轮询能力(即在完成其它任务之后处理 JavaScript 回调函数的能力)有关。任何一个企图以并行的方式运行的代码必须让事件轮询机制以非 JavaScript 操作来运行,像 I/O 操作。 + +举个例子,让我们思考一个案例:案例中每个对服务器的请求消耗 50 毫秒完成,其中的 45 毫秒又是可以通过异步操作而完成的数据库操作。选择 **非阻塞** 操作可以释放那 45 毫秒用以处理其它的请求操作。这是在选择 **阻塞** 和 **非阻塞** 方法上的重大区别。 + +Node.js 中的事件轮询机制和其它语言相比而言有区别,其它语言需要创建线程来处理并行任务。 + + +## 把阻塞和非阻塞代码混在一起写的危险 + +在处理 I/O 问题时,有些东西必须避免。下面让我们看一个例子: + +```js +const fs = require('fs'); +fs.readFile('/file.md', (err, data) => { + if (err) throw err; + console.log(data); +}); +fs.unlinkSync('/file.md'); +``` + +在以上的例子中, `fs.unlinkSync()` 极有可能在 `fs.readFile()` 之前执行,所以在真正准备开始读取文件前此文件就已经被删除了。一个更好的处理方法就是彻底让使它变得 **非阻塞化**,并且保证按照正确顺序执行: + +```js +const fs = require('fs'); +fs.readFile('/file.md', (readFileErr, data) => { + if (readFileErr) throw readFileErr; + console.log(data); + fs.unlink('/file.md', (unlinkErr) => { + if (unlinkErr) throw unlinkErr; + }); +}); +``` + +以上代码在 `fs.readFile()` 用异步方式调用 `fs.unlink()`,这就保证了执行顺序的正确。 + + +## 其它资料 + +- [libuv](http://libuv.org/) +- [关于 Node.js](https://nodejs.org/en/about/) diff --git a/locale/zh-cn/docs/guides/buffer-constructor-deprecation.md b/locale/zh-cn/docs/guides/buffer-constructor-deprecation.md new file mode 100644 index 0000000000000..a9355429a4377 --- /dev/null +++ b/locale/zh-cn/docs/guides/buffer-constructor-deprecation.md @@ -0,0 +1,226 @@ +--- +title: 请使用 Buffer.from()/Buffer.alloc() +layout: docs.hbs +--- + +# 请使用 `Buffer.from()`/`Buffer.alloc()` + + +## 概括 + +本教程将向你介绍如果迁移到安全的 `Buffer` 构造函数方法。此合并将消除以下已废除的警告: + +
+Buffer() 和 new Buffer() 构造函数对于有安全顾虑的人而言是不推荐使用的。请使用新的方法 Buffer.alloc(),Buffer.allocUnsafe() 或者是 Buffer.from() 构造函数。 +
+ +- [变化 1: 在 Node.js ≤ 4.4.x 和 5.0.0 — 5.9.x 版本中不支持](#variant-1) (*recommended*) +- [变化 2: 使用 polyfill 库](#variant-2) +- [变化 3: 带安全守护的手动检测](#variant-3) + +### 在使用 `grep` 的代码中找出一些问题 + +运行 `grep -nrE '[^a-zA-Z](Slow)?Buffer\s*\(' --exclude-dir node_modules`. + +此代码将从你的代码中找出潜在的不安全的代码(包括一些潜在地,没有考虑周到的异常)。 + +### 在使用 `Node 8` 的代码中找出一些问题 + +如果你使用的 Node.js 版本大于等于 8.0.0,Node.js 提供了一些选项帮助你在代码中寻找相关问题: + +- `--trace-warnings` 通过 Node.js 向您展示堆栈信息跟踪,打印出此警告和其它警告信息。 +- `--trace-deprecation` 和上面差不多,不过只打印废弃警告。 +- `--pending-deprecation` 将对废弃警告给出更多的类型。尤其它会展示 `Buffer()` 的废弃警告,即便在 Node.js 8 也是如此。 + +你可以使用环境变量设置这些开关: + +```bash +$ export NODE_OPTIONS='--trace-warnings --pending-deprecation' +$ cat example.js +'use strict'; +const foo = new Buffer('foo'); +$ node example.js +(node:7147) [DEP0005] DeprecationWarning: The Buffer() and new Buffer() constructors are not recommended for use due to security and usability concerns. Please use the new Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() construction methods instead. + at showFlaggedDeprecation (buffer.js:127:13) + at new Buffer (buffer.js:148:3) + at Object. (/path/to/example.js:2:13) + [... more stack trace lines ...] +``` + +### 在使用 `linter` 的代码中找出一些问题 + +ESLint 规则 [不使用缓存构造函数](https://eslint.org/docs/rules/no-buffer-constructor) +或 [node/ 无废除的 Api](https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-deprecated-api.md) 也会寻找到使用 `Buffer()` 废弃的函数。 这些规则预先已经包含了。 + +不过这存在一个劣势,举个例子,当 `Buffer` 被 polyfill重写的时候,它不保证一直[正常工作](https://github.com/chalker/safer-buffer#why-not-safe-buffer)。所以推荐使用此方法和其它如上描述的方法在一起使用。 + + +## 变化 1: 在 Node.js ≤ 4.4.x 和 5.0.0 — 5.9.x 版本中不支持 + +这是现在的一个推荐的解决方案,暗示仅有极小的成本。 + +Node.js 5.x 发行自 2016 年就不再支持,而 4.x 版本 发行线支持到 2018 年 4 月就寿终正寝了(→ [计划表](https://github.com/nodejs/Release#release-schedule))。这意味着这些版本 *不会* 接受任何更新,即便有安全问题也不会被修复,所以如果可能,我们不应使用这些版本。 + +在这种情况下,你应该把全部的 `new Buffer()` 或 `Buffer()` 更改为 `Buffer.alloc()` 或 `Buffer.from()`,规则如下: + +- 对于 `new Buffer(number)`, 请用 `Buffer.alloc(number)` 替换。 +- 对于 `new Buffer(string)` (或 `new Buffer(string, encoding)`),请用对应的 `Buffer.from(string)` (或 `Buffer.from(string, encoding)`)进行替换。 +- 对于其它情况(一般极为罕见)中使用了 `new Buffer(...arguments)` 的,请用 `Buffer.from(...arguments)` 进行替换。 + +注意:`Buffer.alloc()` 在当前的 Node.js 版本上 _快于_ +`new Buffer(size).fill(0)`,后者是当你确认需要用 0 对整个缓存进行初始化。 + +启用 ESLint 检查规则 [不使用缓存构造函数](https://eslint.org/docs/rules/no-buffer-constructor) +或[node/ 无废除的 Api](https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-deprecated-api.md) 时,也会建议避免使用不安全的 `Buffer` 函数。 + +同样我们还有 [JSCodeshift codemod](https://github.com/joyeecheung/node-dep-codemod#dep005),它可以把 `Buffer` 构造函数的地方自动替换成 `Buffer.alloc()` 或 `Buffer.from()`。注意目前它只会工作在参数是文本型,或者带有两个参数的构造函数的情况下。 + +_如果你目前支持那些旧版本的 Node.js,并且抛弃对它们的支持又不可能的情况下,或者你需要支持你包中的旧版本情况下,请考虑使用 [版本 2](#variant-2),或者 [版本 3](#variant-3)。这样人们可以在使用这些旧版本情况下照样修复这些安全问题。那样的话,这些由不安全的 `Buffer` 所引发的问题会被你彻底根除,你的用户也不用在你运行 Node.js 10 的时候观察你的运行时废弃警告。_ + + +## 变化 2: 使用替换库 + +存在着三种替换库: + +- **[更安全的缓存](https://www.npmjs.com/package/safer-buffer)** 是整个用来替换 `Buffer` 函数的方法。当你在使用 `new Buffer()` 的时候,将会 _抛出_ 异常。 + 和 [变化 1](#版本-1) 中一样,你会得到详细同样的步骤。不过请用 `const Buffer = require('safer-buffer').Buffer` 在你所有文件中对 `Buffer` 函数进行替换。 + + 请不要使用旧版本的 `new Buffer()` 函数,在添加上面的行的任何文件中,使用 `new Buffer()` 会抛出 _异常_。 + +- **[buffer-from](https://www.npmjs.com/package/buffer-from) 或 + [buffer-alloc](https://www.npmjs.com/package/buffer-alloc)** 都是 + [ponyfills](https://ponyfill.com/) `Buffer` 可接受的方案。 你所要做的就是针对你自己的 API 添加所需的包。 + + 你需要用一个合适的名字为这些调入的模块重命名,例如 `const bufferFrom = require('buffer-from')`。并且使用它们取代你的 `new Buffer()`。例如 `new Buffer('test')` 变成了 `bufferFrom('test')`。 + + 这种方法的缺点是稍微改变代码以迁移它们(如您所希望的那样)。例如在不同的名称下使用 `Buffer.from()`。 + +- **[安全的缓存](https://www.npmjs.com/package/safe-buffer)** 同样也是替换整个 `Buffer` 的方案,但是用 `new Buffer()` 也可以像以前一样正常工作。 + + 欲达此目的而降阶到此,可以让你在代码中使用稍旧一些的 `new Buffer()` 函数,它会引发代码一些问题,并在 Node.js 10 ([阅读更多详情](https://github.com/chalker/safer-buffer#why-not-safe-buffer)) 激发对于运行时废弃函数的警告检查。 + +注意,在任意一种情况下,手动移除你代码中所有关于 `Buffer` 的调用非常重要——仅在 `safe-buffer` 中抛出警告不解决问题,它只是为新的 API 提供了一种替换而已。我亲眼见过人们犯过这类错误。 + +启用 ESLint 规则 [不使用缓存构造函数](https://eslint.org/docs/rules/no-buffer-constructor) +或是 [node/ 无废除的 Api](https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-deprecated-api.md) 是推荐的。 + +_如果你抛弃了对 Node.js 版本小于 4.5.0 的支持,请不要忘记把替代库也一起去掉。_ + + +## 变化 3 — 带安全守护的手动检测 + +在某些情况下这对于你创建 `Buffer` 实例是有帮助的(例如:只需要一个实例的情况下)。或者你有你自己的包装围绕在它们身边。 + +### `Buffer(0)` + +创建空缓存这个特殊的例子可以用 `Buffer.concat([])` 来代替,对于 Node.js 0.8.x 版本而言返回同样的结果。 + +### `Buffer(notNumber)` + +Before: + +```js +const buf = new Buffer(notNumber, encoding); +``` + +After: + +```js +let buf; +if (Buffer.from && Buffer.from !== Uint8Array.from) { + buf = Buffer.from(notNumber, encoding); +} else { + if (typeof notNumber === 'number') { + throw new Error('The "size" argument must be not of type number.'); + } + buf = new Buffer(notNumber, encoding); +} +``` + +`encoding` 为可选参数。 + +注意在 `new Buffer()` 前的 `typeof notNumber` 检测必不可少(对于 `notNumber` 参数不是硬编码的情况下),并且 _不会引发 `Buffer` 构造函数废弃的警报_——这就是 _为什么_ 说 `Buffer` 构造函数被废弃的原因。缺少此类型检测的生态系统引发过不计其数的安全事故——如脏用户的某些输入,在 `Buffer(arg)` 可能会意外终止,这会导致从DoS攻击到从进程内存向攻击者泄露敏感信息范围内一系列问题。 + +当 `notNumber` 被硬编码(如文本型 `"abc"` 或者 `[0,1,2]`), `typeof` 类型检查可以被忽略。 + +当然,注意使用 TypeScript 也不解决问题——用 `TypeScript` 写的库从 JS 方引用,或者用户的输入停在那边——它表现得就和原生态的 JS 一模一样,因为所有类型检查仅仅在翻译解析阶段,在 TS 编译成真实的 JS 代码时却不存在了。 + +### `Buffer(number)` + +对于 Node.js 0.10.x (和之后的版本)支持: + +```js +var buf; +if (Buffer.alloc) { + buf = Buffer.alloc(number); +} else { + buf = new Buffer(number); + buf.fill(0); +} +``` + +其余版本(Node.js ≥ 0.12.x): + +```js +const buf = Buffer.alloc ? Buffer.alloc(number) : new Buffer(number).fill(0); +``` + +## 关于 `Buffer.allocUnsafe()` + +使用 `Buffer.allocUnsafe()` 须格外谨慎几点: + * 如果没有一个很好的理由,请不要使用它: + * 对于小缓存,你或许不想看到性能上的差别。实际上,用 `Buffer.alloc()` 甚至更快。 + * 如果你的代码不是在热代码路径中——你也不希望看到有差别,记住用零填充将把潜在的风险降到最低。 + * 如果你使用它,请务必保证你从不会返回只填充了一部分的缓存, + * 如果你按顺序写入此缓存——总是截取此缓存到你写入缓存的实际长度。 + +处理与 `Buffer.allocUnsafe()` 相关的缓存错误可能会引发各种各样的问题,从你代码的不确定行为表现到敏感数据(如用户输入,密码,相关证书等)被泄露给远程的攻击者等。 + +_注意,当你不用 0 去填充缓存,此问题同样发生在 `new Buffer()` 上。这依赖于 Node.js 版本(缺少类型检查也会额外增加 DoS 攻击)。_ + + +## 常见问题 + + +### `Buffer` 构造函数有什么问题? + +`Buffer` 构造函数可以用不同方式创建缓存: + +- `new Buffer(42)` 创建一个 42 个字节的 `缓存`。在 Node.js 8 之前,该缓存考虑性能,它包含 *随机内存*,而这可能包括任何数据,从编码的源码到密码,以及加密秘钥等。 +- `new Buffer('abc')` 创建一个 UTF-8 编码的字符串 `'abc'`。第二个参数可以指定用何种编码:举一个例子,`new Buffer(string, 'base64')` 可用于将Base64字符串转换为原始字符串表示的字节序列。 +- 除此之外,还有一些其它参数的组合。 + +这意味着在代码中诸如 `var buffer = new Buffer(foo);`,当你不知道 `foo` 是什么类型,想要知道生成的缓存里边到底存了什么内容几乎是不可能的。 + +有时,`foo` 的值来源于外部源头。举一个例子,以下函数作为服务器上的一个函数公开,把 UTF-8 的编码字符串转换成 Base64 形式: + +```js +function stringToBase64(req, res) { + // The request body should have the format of `{ string: 'foobar' }`. + const rawBytes = new Buffer(req.body.string); + const encoded = rawBytes.toString('base64'); + res.end({ encoded }); +} +``` + +注意这个代码 *不会* 验证 `req.body.string` 的类型: + +- `req.body.string` 期望的类型是字符串型。如果是这种情况一切正常。 +- `req.body.string` 受客户端发送请求所控制。 +- 如果 `req.body.string` 是 *数字* `50`,`rawBytes` 将变成 `50` 个字节: + - 在 Node.js 8 之前,内容是未经初始化的。 + - 在 Node.js 8 之后,内容是 50 个 0。 + +因为缺少类型检查,攻击者可以别有用心地发送一个数字作为请求的一部分,借助它,他们可以: + +- 读取未初始化的内存数据。 这显然 **会** 导致密码、秘钥和其它敏感数据的泄露(信息泄露)。 +- 强迫程序开辟一个超大内存区域。举一个例子,当指定 `500000000` 作为输入数据时,每个请求将开辟 500MB 内存区。这不是会耗尽内存使得程序崩溃,就会导致明显的程序性能下降(服务拒绝攻击)。 + +这些情况在现实的网络服务中都被认为是非常严重的安全问题。 + +当使用 `Buffer.from(req.body.string)` 的时候,如果传入一个数字总是抛出异常,给程序提供了一个总是可以自我处理的机会。 + + +###`Buffer()` 构造函数废弃有一阵了,它有问题吗? + +检测 `npm` 生态系统的代码,表明 `Buffer()` 仍然广泛被使用。这包含新提交的代码,以及这类代码的使用仍然在 *增长中*。 diff --git a/locale/zh-cn/docs/guides/debugging-getting-started.md b/locale/zh-cn/docs/guides/debugging-getting-started.md new file mode 100644 index 0000000000000..80a87cbeb8b08 --- /dev/null +++ b/locale/zh-cn/docs/guides/debugging-getting-started.md @@ -0,0 +1,198 @@ +--- +title: 调试 - 入门教程 +layout: docs.hbs +--- + +# 调试指南 + +本指南将帮助你开始学习调试 Node.js 程序和脚本。 + +## 启用检查器 + +当启动 **--inspect** 开关时,Node.js 进程通过网络套接字监听;对于由 [检查器协议][] 定义的诊断命令,默认情况下主机和端口 127.0.0.1:9229。每个进程也被分配一个独特的 [UUID][] (e.g. `0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e`)。 + +检查器的客户端必须知道且指定宿主地址、端口以及连接网络套接字接口的 UUID。完整的地址是:`ws://127.0.0.1:9229/0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e`。当然,这依赖实际上的宿主地址、端口和相关连接实体的 UUID。 + +检查器同时也包含一个 HTTP 的终结点,它是为服务除错工具的元数据而准备的,包含网络套接字地址、UUID 以及 Chrome 开发工具的 URL。通过发送一个 HTTP 请求到 `http://[host:port]/json/list` 就可以得到元数据,它返回一个 JSON 对象如下所示。使用 `webSocketDebuggerUrl` 属性作为直接连接检查器的地址。 + + +```javascript +{ + "description": "node.js instance", + "devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e", + "faviconUrl": "https://nodejs.org/static/favicon.ico", + "id": "0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e", + "title": "node", + "type": "node", + "url": "file://", + "webSocketDebuggerUrl": "ws://127.0.0.1:9229/0f2c936f-b1cd-4ac9-aab3-f63b0f33d55e" +} +``` + +Node.js 进程如果 *不使用* `--inspect` 开关启动,也可以由指令启动,通过用 `SIGUSR1` 发出信号侦听调试消息(在Linux上 和 OSX)。在 Node 7上,激活了传统的调试器 API;在 Node 8 和以后它将激活监视器 API。 + +--- +## 安全含义 + +由于调试器对 Node.js 执行环境具有完全访问权限,所以能够连接到该端口的恶意角色可以执行任意操作。代码代表节点进程。理解将调试器端口暴露在公共和专用网络上所受影响的安全性是很重要的。 + +### 把调试端口暴漏在公共网络是不安全的 + +如果调试器与一个公共的 IP 地址绑定,或者与 0.0.0.0 绑定,任何可以访问你 IP 地址的客户端都可以在不受限的情况下连接调试器,然后随意运行代码。 + +默认情况下,`node --inspect` 绑定 127.0.0.1。你可以显式提供一个 IP 地址或是 0.0.0.0 的地址等。如果你有意想要外部连接可以访问此调试器,这么做恐怕会把你置于潜在的巨大的威胁中。我们建议你有合适的防火墙以及访问控制权限,以提供一个安全的暴漏。 + +查看 '[ 启用远程调试情形 ](#enabling-remote-debugging-scenarios)' 章节部分,以了解如何安全地允许远程调试器连接调试。 + +### 本地应用有足够的权限访问监视器 + +即便你把监视器绑定到 127.0.0.1(默认),任何在你本地机器上运行的应用程序仍然毫无限制地可以访问。这是因为在设计上我们允许本地调试器可以轻松方便地进行连接。 + +### 浏览器,网络套接字和同源政策 + +网站的开放是通过一个可以让网络套接字和 HTTP 请求在浏览器安全模式下进行的。一个初始化的 HTTP 连接必须先获得一个唯一的调试会话 ID。同源政策能够阻止这个网站与 HTTP 连接。对于其它额外的安全防范[DNS 重新绑定攻击](https://en.wikipedia.org/wiki/DNS_rebinding),Node.js 会先精确验证‘宿主’头连接不是一个一个指定的 IP 地址,就是 `localhost`,又或者是 `localhost6`。 + +这些安全政策不允许通过指定主机名的方式直接进行远程连接。你不是通过指定 IP 地址,就是使用 ssh 管道的方式(下面将会有所陈述)绕开此限制。 + +## 监视器客户端 + +一些商业和开源工具可以连接到 Node 的监视器上,关于它们基本信息如下: + +#### [ Node 监视器 ](https://github.com/nodejs/node-inspect) + +* 由 Node.js 基础库,使用 [检查器协议][] 支持的 CLI 调试器。 +* 和 Node 绑定在一起的版本,并且可以使用 `node inspect myscript.js`。 +* 最新的版本同样可以单独通过(例如 `npm install -g node-inspect`)方式安装,并使用 `node-inspect myscript.js`。 + +#### [Chrome 开发工具 ](https://github.com/ChromeDevTools/devtools-frontend) 55+ + +* **选项 1**: 在基于 Chromium 内核的浏览器下打开 `chrome://inspect`。点击配置按钮确保你的目标宿主和端口号列入其中。 +* **选项 2**: 从 `/json/list` 中拷贝 `devtoolsFrontendUrl`(见上),或者加上 --inspect 以检查提示文本并粘贴到 Chrome 中。 +* **选项 3**: 安装 Chrome 扩展(Node 监视管理器):https://chrome.google.com/webstore/detail/nim-node-inspector-manage/gnhhdgbaldcilmgcpfddgdbkhjohddkj + +#### [Visual Studio Code](https://github.com/microsoft/vscode) 1.10+ + +* 在 Debug 面板中,点击设置按钮打开 `.vscode/launch.json`,选择 "Node.js" 进行初始化构建。 + +#### [Visual Studio](https://github.com/Microsoft/nodejstools) 2017 + +* 从菜单中或者单击 F5, "Debug > Start Debugging"。 +* [详细指导](https://github.com/Microsoft/nodejstools/wiki/Debugging) + +#### [JetBrains WebStorm](https://www.jetbrains.com/webstorm/) 2017.1+ 以及其它版本 + +* 创建一个新的 Node.js 调试配置,点击调试。在 Node.js 7 版本上默认会加上 `--inspect` 开关。禁用 uncheck `js.debugger.node.use.inspect` IDE 注册表。 + +#### [chrome 远程接口](https://github.com/cyrus-and/chrome-remote-interface) + +* 简化对检查器协议终端连接的库。 + +--- + +## 命令行选项 + +以下命令表列出了在调试状态下不同标示符的影响: + + + + + + + + + + + + + + + + + + + + + + + + + + + +
标示符含义
--inspect +
    +
  • 启用监视器代理
  • +
  • 在默认地址和端口上监听(127.0.0.1:9229)
  • +
+
--inspect=[host:port] +
    +
  • 启用监视器代理
  • +
  • 绑定地址或主机名宿主 (默认:127.0.0.1)
  • +
  • 监听端口 (默认:9229)
  • +
+
--inspect-brk +
    +
  • 启用监视器代理
  • +
  • 监听默认地址和端口(127.0.0.1:9229)
  • +
  • 在用户代码启动前终止
  • +
+
--inspect-brk=[host:port] +
    +
  • 启用监视器代理
  • +
  • 绑定地址和主机名宿主(默认:127.0.0.1)
  • +
  • 监听端口(默认:9229)
  • +
  • 在用户代码启动前终止
  • +
+
Node 监视script.js +
    +
  • 通过 --inspect 标志生成一个新的子进程,使用主进程运行 CLI 调试器。
  • +
+
node inspect --port=xxxx script.js +
    +
  • 通过 --inspect 标志生成一个新的子进程,使用主进程运行 CLI 调试器。
  • +
  • 监听端口(默认:9229)
  • +
+
+ +--- + +## 启用远程调试的情形 + +我们推荐你千万不要使用调试器监听公共的 IP 地址。如果你真需要允许远程调试连接,那么就请使用 SSH 代替。以下我们提供你例子仅是为解释目的。请在开始前理解允许远程访问特权的安全风险。 + +让我们假定你在一台远程机器上运行 Node,譬如 remote.example.com。你想进行调试。在那台机器上你应该启动 node 进程,让监视器仅监听本地(默认)。 + +```bash +$ node --inspect server.js +``` + +现在,在你本地机器上,从你初始化一个调试客户端连接开始,你创建了一个 SSH 管道: + +```bash +$ ssh -L 9221:localhost:9229 user@remote.example.com +``` + +ssh 管道启动,在你机器上连接到 9221 端口将被重定向到 9229 的 remote.example.com 地址上。你可以附加一个调试器,例如 Chrome 开发工具或者是指向 localhost:9221 的 Visual Studio Code。如果 Node.js 本地正在运行,应该可以调试了。 + +--- + +## 遗留的调试器 + +** 遗留的调试器自 Node 7.7.0 已被启用。请使用 --inspect 代替。** + +在版本 7 以及更早的版本使用 **--debug** 或 **--debug-brk** 开关启动调试时,Node.js 侦听由中断定义的调试命令,TCP 端口上的 V8 调试协议,默认为 `5858`。任何遵守此协议的调试客户端都可以连接并调试运行这个进程,下面有一些热门的说明。 + +V8 调试协议再也不维护或是归档了。 + +#### [ 内置调试器 ](https://nodejs.org/dist/latest-v6.x/docs/api/debugger.html) + +在 Node.js 内置命令行调试器中 用 `node debug script_name.js` 启动你的脚本。你的脚本就在 Node 另外一个进程中随着 `--debug-brk` 启动了起来,并且初始化的 Node 进程运行 `_debugger.js` 脚本连接上你的目标。 + +#### [node 监视器](https://github.com/node-inspector/node-inspector) + +用 Chrome 开发工具,通过 Node.js 的中间进程把 Chromium 中的检查器协议转换成 V8 调试器协议进行程序调试。 + + + +[检查器协议]: https://chromedevtools.github.io/debugger-protocol-viewer/v8/ +[UUID]: https://tools.ietf.org/html/rfc4122 diff --git a/locale/zh-cn/docs/guides/domain-postmortem.md b/locale/zh-cn/docs/guides/domain-postmortem.md new file mode 100644 index 0000000000000..1377a2d5b454e --- /dev/null +++ b/locale/zh-cn/docs/guides/domain-postmortem.md @@ -0,0 +1,347 @@ +--- +title: 域模块已死 +layout: docs.hbs +--- + +# 域模块已死 + +## 使用中的问题 + +### 隐式行为 + +对于一个开发者而言,创建一个新的域然后简单地通过 `domain.enter()` 运行起来,然后在将来它就可以捕获全部抛出异常者无法观察到的异常。允许模块作者截取不同模块中无关代码的异常而不让原来的代码知道它的自身异常。 + +下面是一个间接链接模块如何影响另一个模块的例子: + +```js +// module a.js +const b = require('./b'); +const c = require('./c'); + + +// module b.js +const d = require('domain').create(); +d.on('error', () => { /* silence everything */ }); +d.enter(); + + +// module c.js +const dep = require('some-dep'); +dep.method(); // Uh-oh! This method doesn't actually exist. +``` + +因为模块 `b` 进入了域中且从未退出,任何未捕获的异常将被吞掉。茫茫然地留下模块 `c` 而为什么它没有运行整个脚本?留下一个可能部分填充的模块 `module.exports`。这么做与监听 `'uncaughtException'`是不同的。后者明显指全局捕获异常错误,另外一个问题是域在任何 `'uncaughtException'` 处理程序之前进行处理,并阻止它们继续执行。 + +另一个问题是如果在事件激发者上没有相关 `'错误'` 控制,域自动路由错误。这种选择机制对你而言没的选择,它会在整个异步链上自动传播。这个或许一开始看起来有用,但是一旦异步调用是两个或多个模块,其中有一个不包含域的创建者的错误处理程序突然遇到了意想不到的例外,抛出异常的也就不会被作者注意到。 + +下面是一个简单的例子,说明丢失的 `'错误'` 处理程序是如何允许的主动域劫持错误的: + +```js +const domain = require('domain'); +const net = require('net'); +const d = domain.create(); +d.on('error', (err) => console.error(err.message)); + +d.run(() => net.createServer((c) => { + c.end(); + c.write('bye'); +}).listen(8000)); +``` + +即便通过 `d.remove(c)` 手动移除连接也不会阻止连接错误不会自动捕获。 + +困扰错误路由和异常处理的故障是不一致的错误如何冒泡。下面是一个例子说明嵌套域怎样在异常发生时向上冒泡异常: + +```js +const domain = require('domain'); +const net = require('net'); +const d = domain.create(); +d.on('error', () => console.error('d intercepted an error')); + +d.run(() => { + const server = net.createServer((c) => { + const e = domain.create(); // No 'error' handler being set. + e.run(() => { + // This will not be caught by d's error handler. + setImmediate(() => { + throw new Error('thrown from setImmediate'); + }); + // Though this one will bubble to d's error handler. + throw new Error('immediately thrown'); + }); + }).listen(8080); +}); +``` + +可以预期嵌套域始终保持嵌套,并且总是将异常传播到域堆栈上。或者那些例外不会自动发生冒泡。不幸的是,这两种情况都会发生,导致可能混淆的行为,甚至可能很难调试时序冲突。 + + +### API 缺陷 + +虽然基于 `EventEmitter` 的 API 可以使用 `bind()` ,而错误回调风格的回调可以使用 `intercept()`,但是隐式绑定到活动域的替代 API 必须在 `run()` 中执行。这意思是,如果模块作者想用一种机制来支持域,那么就可以使用必须手动实现域自身的支持机制。而不是能够利用已经存在的隐含机制。 + + +### 错误传递 + +如果可能的话,跨嵌套域传播错误不是直截了当的。现有文档显示了在请求处理程序中出现错误时如何使用 `close()` 方法关闭 `http` 服务器的简单示例。它没有解释的是,如果请求处理程序为另一个异步请求创建另一个域实例,则如何关闭服务器。以下作为错误传播失败的简单例子: + +```js +const d1 = domain.create(); +d1.foo = true; // custom member to make more visible in console +d1.on('error', (er) => { /* handle error */ }); + +d1.run(() => setTimeout(() => { + const d2 = domain.create(); + d2.bar = 43; + d2.on('error', (er) => console.error(er.message, domain._stack)); + d2.run(() => { + setTimeout(() => { + setTimeout(() => { + throw new Error('outer'); + }); + throw new Error('inner'); + }); + }); +})); +``` + +即使在域实例被用于本地存储的情况下,使得对资源的访问仍可用,仍然没有办法允许错误从 `d2` 继续传播到 `d1`。快速检查可以告诉我们简单地从 `d2` 域 `'错误'` 处理程序中抛出将允许 `d1` 捕获异常并执行它自己的错误处理程序。虽然不是这种情况。在检查 `domain._stack` 时,您会看到堆栈只包含 `d2` 。 + +这可能被认为是 API 的一个失败,但即使确定它在这方面可以运行,还有一个问题,就是当异步执行中的某个分支发生了失败,那么所有进一步的操作都必须终止。在 http 请求处理程序的示例中,如果我们发起几个异步请求,然后每个 `write()` 方法数据返回到客户端,试图对一个已经关闭的处理调用 `write()` 方法时会出现更多的错误。关于 _异常的资源清理_ 中对此进行了更多的讨论。 + + +### 异常的资源清理 + +下面的脚本包含一个更复杂的示例。它是关于在一个小的资源依赖树中正确资源清理,在一个给定的连接或其任何依赖关系中发生异常的情况下,将脚本分解为基本操作: + +```js +'use strict'; + +const domain = require('domain'); +const EE = require('events'); +const fs = require('fs'); +const net = require('net'); +const util = require('util'); +const print = process._rawDebug; + +const pipeList = []; +const FILENAME = '/tmp/tmp.tmp'; +const PIPENAME = '/tmp/node-domain-example-'; +const FILESIZE = 1024; +let uid = 0; + +// Setting up temporary resources +const buf = Buffer.alloc(FILESIZE); +for (let i = 0; i < buf.length; i++) + buf[i] = ((Math.random() * 1e3) % 78) + 48; // Basic ASCII +fs.writeFileSync(FILENAME, buf); + +function ConnectionResource(c) { + EE.call(this); + this._connection = c; + this._alive = true; + this._domain = domain.create(); + this._id = Math.random().toString(32).substr(2).substr(0, 8) + (++uid); + + this._domain.add(c); + this._domain.on('error', () => { + this._alive = false; + }); +} +util.inherits(ConnectionResource, EE); + +ConnectionResource.prototype.end = function end(chunk) { + this._alive = false; + this._connection.end(chunk); + this.emit('end'); +}; + +ConnectionResource.prototype.isAlive = function isAlive() { + return this._alive; +}; + +ConnectionResource.prototype.id = function id() { + return this._id; +}; + +ConnectionResource.prototype.write = function write(chunk) { + this.emit('data', chunk); + return this._connection.write(chunk); +}; + +// Example begin +net.createServer((c) => { + const cr = new ConnectionResource(c); + + const d1 = domain.create(); + fs.open(FILENAME, 'r', d1.intercept((fd) => { + streamInParts(fd, cr, 0); + })); + + pipeData(cr); + + c.on('close', () => cr.end()); +}).listen(8080); + +function streamInParts(fd, cr, pos) { + const d2 = domain.create(); + const alive = true; + d2.on('error', (er) => { + print('d2 error:', er.message); + cr.end(); + }); + fs.read(fd, Buffer.alloc(10), 0, 10, pos, d2.intercept((bRead, buf) => { + if (!cr.isAlive()) { + return fs.close(fd); + } + if (cr._connection.bytesWritten < FILESIZE) { + // Documentation says callback is optional, but doesn't mention that if + // the write fails an exception will be thrown. + const goodtogo = cr.write(buf); + if (goodtogo) { + setTimeout(() => streamInParts(fd, cr, pos + bRead), 1000); + } else { + cr._connection.once('drain', () => streamInParts(fd, cr, pos + bRead)); + } + return; + } + cr.end(buf); + fs.close(fd); + })); +} + +function pipeData(cr) { + const pname = PIPENAME + cr.id(); + const ps = net.createServer(); + const d3 = domain.create(); + const connectionList = []; + d3.on('error', (er) => { + print('d3 error:', er.message); + cr.end(); + }); + d3.add(ps); + ps.on('connection', (conn) => { + connectionList.push(conn); + conn.on('data', () => {}); // don't care about incoming data. + conn.on('close', () => { + connectionList.splice(connectionList.indexOf(conn), 1); + }); + }); + cr.on('data', (chunk) => { + for (let i = 0; i < connectionList.length; i++) { + connectionList[i].write(chunk); + } + }); + cr.on('end', () => { + for (let i = 0; i < connectionList.length; i++) { + connectionList[i].end(); + } + ps.close(); + }); + pipeList.push(pname); + ps.listen(pname); +} + +process.on('SIGINT', () => process.exit()); +process.on('exit', () => { + try { + for (let i = 0; i < pipeList.length; i++) { + fs.unlinkSync(pipeList[i]); + } + fs.unlinkSync(FILENAME); + } catch (e) { } +}); + +``` + +- 当一个新的连接发生时,同时也发生: + - 打开文件系统上的文件 + - 针对唯一的套接字打开文件管道 +- 异步读取文件块 +- 将块写入 TCP 连接和任何监听套接字中 +- 如果这些资源中的任何一个出错,则通知其它需要清理和关闭的附加资源 + +正如我们从这个例子中看到的那样,当某些事情发生故障时必须做正确的清理。所有域提供的是异常聚合机制。在这个例子中,即使通过域传递数据的潜在有用能力也很容易通过将需要的资源作为函数参数传递。 + +一个问题是把域持久化。它是对能够继续执行的简单假设,与文档所述的相反,尽管有一个意外的例外。这个例子说明了这个想法背后的谬误。 + +当应用程序本身的复杂性增加时,尝试对意外异常进行适当的资源清理变得更加复杂。此示例只有三个基本资源在应用中,并且它们都具有明确的依赖路径。如果应用程序使用诸如共享资源或资源重用之类的东西,清理和测试该清理的能力已经大大提高。 + +最后,在处理错误方面,域不仅仅是一个值得骄傲的 `'uncaughtException'` 处理程序。除第三方更隐秘和不可观察的行为以外。 + + +### 资源传播 + +域的另一个用例是使用它来沿着异步数据路径传播数据。一个问题是当在堆栈中有多个时,期望何时得到正确的域的模糊性(必须假定)。异步堆栈与其它模块一起工作。还可以依赖于域进行错误处理,同时也有可能用于检索必要的数据。 + +下面是一个示例,演示了使用域沿着异步堆栈传输数据的失败情况: + +```js +const domain = require('domain'); +const net = require('net'); + +const server = net.createServer((c) => { + // Use a domain to propagate data across events within the + // connection so that we don't have to pass arguments + // everywhere. + const d = domain.create(); + d.data = { connection: c }; + d.add(c); + // Mock class that does some useless async data transformation + // for demonstration purposes. + const ds = new DataStream(dataTransformed); + c.on('data', (chunk) => ds.data(chunk)); +}).listen(8080, () => console.log('listening on 8080')); + +function dataTransformed(chunk) { + // FAIL! Because the DataStream instance also created a + // domain we have now lost the active domain we had + // hoped to use. + domain.active.data.connection.write(chunk); +} + +function DataStream(cb) { + this.cb = cb; + // DataStream wants to use domains for data propagation too! + // Unfortunately this will conflict with any domain that + // already exists. + this.domain = domain.create(); + this.domain.data = { inst: this }; +} + +DataStream.prototype.data = function data(chunk) { + // This code is self contained, but pretend it's a complex + // operation that crosses at least one other module. So + // passing along "this", etc., is not easy. + this.domain.run(() => { + // Simulate an async operation that does the data transform. + setImmediate(() => { + for (let i = 0; i < chunk.length; i++) + chunk[i] = ((chunk[i] + Math.random() * 100) % 96) + 33; + // Grab the instance from the active domain and use that + // to call the user's callback. + const self = domain.active.data.inst; + self.cb(chunk); + }); + }); +}; +``` + +以上表明,尝试使用一个以上的异步 API 借助域来传播数据是困难的。这个例子是固定假设通过在 `DataStream` 构造函数中赋值 `parent: domain.active`。然后通过 `domain.active = domain.active.data.parent` 在用户的回调函数被调用前恢复它。 `DataStream` 的实例化`'连接'`回调必须在 `d.run()` 中运行,而不是简单地使用 `d.add(c)`,否则将没有活动域。 + +简而言之,倘若要祈祷有机会运用这样的方式处理问题,需要严格遵守一组很难实施或测试的指导方针。 + + +## 性能问题 + +使用域名的显著威慑是开销。使用节点内置的 http 基准,`http_simple.js`,没有域,它可以处理超 22000 个请求/秒。而如果它运行的是 `NODE_USE_DOMAINS=1`,则该数字下降到 17000个请求/秒以下。在这种情况下只有一个全局域。如果我们编辑基准,那么 http 请求回调会创建一个新的域实例,性能进一步下降到 15000 个请求/秒。 + +虽然这可能不会影响每秒只有几百甚至一千个请求的服务器,但开销的数量与异步请求的数量成正比。因此,如果单个连接需要连接到其它几个服务,所有这些都将对最终交付给客户机的等待时间产生影响。 + +使用 `AsyncWrap` 并跟踪次数 +`init`/`pre`/`post`/`destroy` 在以上基准中所提及,我们发现所有事件的总和超过每秒 170000 次。这意味着即使为每种类型的设置增加 1 微秒的开销,任何类型的安装或拆除都会导致 17% 的性能损失。当然,这是针对基准的优化方案,但我相信这证明了一种机制,如域尽可能简单方便地运行的必要性。 + + +## 展望未来 + +自从 2014 年 12 月以来,域模块已经被软弃用,但由于节点目前暂时没有替代功能,所以还没有彻底被移除。在撰写本文时,`AsyncWrap` 的 API 的工作正在进行,以及为 TC39 准备的区域的建议。在这种时候,如有合适的功能来替换域,它将经历完整的弃用周期而最终从核心移除。 diff --git a/locale/zh-cn/docs/guides/dont-block-the-event-loop.md b/locale/zh-cn/docs/guides/dont-block-the-event-loop.md new file mode 100644 index 0000000000000..372cbcb6bbd11 --- /dev/null +++ b/locale/zh-cn/docs/guides/dont-block-the-event-loop.md @@ -0,0 +1,471 @@ +--- +title: 不要阻塞你的事件轮询(或是工作池) +layout: docs.hbs +--- + +# 不要阻塞你的事件轮询(或是工作池) + +## 你是否应该读这篇指南? +如果你写出的代码不是一行那么简单,阅读本篇指南可以帮助你写出高质量、更安全的程序。 + +此文档是用节点服务器编写的,但这些概念也适用于复杂的节点应用程序。 +在特定于不同的操作系统,请此文档以 Linux 为中心。 + +## TL; DR +Node.js 通过事件循环机制(初始化和回调)的方式运行 JavaScript 代码,并且提供了一个工作池处理诸如 I/O 高成本的任务。 +Node 自我会调节尺度,有时甚至比更重量级的 Apache 服务器都要好。 +Node 可伸缩性的秘密在于它使用了一些小线程处理许多客户端。 +如果 Node 用更少的线程做这些事,或许将占用工作在客户端更多的系统时间和内存,而不是为这些线程(内存,上下文交换)消耗更多的内存和时间。 +但是 Node 只有少量线程,你必须重构你的程序,有智慧地在程序中使用它们。 + +这里有一个很好的经验法则,能使您的节点服务器变快: +*在任何时候,当分配到每个客户端的任务是“少量”的情况下,Node 是非常快的。* + +这条法则可以应用于事件轮询中的回调机制,以及在工作池上的任务。 + +## 为什么不要阻塞你的事件轮询(或是工作池)? +Node 是用一组少量的线程来处理许多客户端请求的。 +在 Node 中,有两种类型的线程:一个事件循环(即主循环,主线程,事件线程等)。另外一个是在工作池里的 `k` 个工作线程(即线程池)。 + +如果一个线程执行一个回调函数(事件轮询)或者任务(工作线程)需要耗费很长时间,我们称为“阻塞”。 +当一个线程在一个客户端上被阻塞了,它也就无法处理其它客户端的请求了。 +这里给出两个不能阻塞事件轮询和工作池的理由: + +1. 性能:如果你定期通过任意的某种形式线程处理繁重的任务,你的服务器将面临 *吞吐量*(请求/秒)的考验。 +2. 安全性:如果对于特定的输入,你其中的一个线程阻塞了,那么恶意攻击者可以提交如此的“邪恶输入”,故意让你的线程阻塞,然后使得其它客户端得不到处理。这就是 [拒绝式攻击](https://en.wikipedia.org/wiki/Denial-of-service_attack)。 +## 对 Node 的快速回顾 + +Node 使用事件驱动机制:它有一个事件轮询的编排,和一个为高消费任务的处理工作池。 + +### 什么代码是在事件轮询上运行的? +当 Node 程序运行时,程序首先完成初始化部分,`需要` 模块为事件注册回调函数。 +然后,Node 应用程序进入事件轮询中,通过执行对应的回调函数对客户端请求做出回应。 +此回调将异步执行,并且可能在完成之后异步注册请求继续处理。 +对于这些异步请求的回调也会在事件轮询中被处理。 + +事件轮询通过回调函数完成非阻塞异步请求,如网络的 I/O。 + +总而言之,事件轮询执行为事件而注册的回调函数,并且负责对完成诸如网络 I/O 一样的非阻塞异步请求。 + +### 什么代码又运行在工作池上呢? +Node 的工作池通过 libuv ([相关文档](http://docs.libuv.org/en/v1.x/threadpool.html)) 来实现的,它对外提供了一个通用的任务提交 API。 + +Node 使用工作池来处理“高消费”的任务。 +这包含为一个操作系统执行非阻塞版本的 I/O 操作,同时也包含对 CPU 密集任务的处理。 + +Node 模块中有些 API 用到了工作池: + +1. I/O 密集型任务: + 1. [DNS](https://nodejs.org/api/dns.html):`dns.lookup()`,`dns.lookupService()`。 + 2. [文件系统](https://nodejs.org/api/fs.html#fs_threadpool_usage):所有的文件系统 API。除 `fs.FSWatcher()` 和那些显式同步使用 libuv 的线程池函数之外。 +2. CPU 密集型任务: + 1. [Crypto](https://nodejs.org/api/crypto.html):`crypto.pbkdf2()`,`crypto.randomBytes()`,`crypto.randomFill()`。 + 2. [Zlib](https://nodejs.org/api/zlib.html#zlib_threadpool_usage):所有 Zlib 相关函数,除那些显式同步使用 libuv 的线程池函数之外。 + +在许多 Node 应用程序中,对于工作池而言这些 API 只是任务源。使用了 [C++ 插件](https://nodejs.org/api/addons.html) 的应用程序和模块可以向工作池提交其它任务。 + +为了完整性考虑,我们注意到当你从事件轮询机制里的一个回调函数中调用这些 API 函数中的某一个时,事件轮询机制将花费少量的开销,因为这已经进入了为那个函数绑定的 Node C++ 中,并对工作池提交了一个任务。 +和整个任务相比,这些开销微不足道。这就是为什么事件轮询总是要卸载它的原因。 +当向工作池中提交了这些任务中的某个的时候,Node C++ 绑定提供了指向这些相关函数的指针。 + +### Node 怎么决定下一步该运行哪些代码? +抽象来说,事件轮询机制和工作池为事件等待、任务等待维护了多个队列。 + +实际上,事件轮询机制本身并不维护队列,它拥有一堆文件描述符,要求操作系统监视、使用诸如 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) (Linux),[kqueue](https://developer.apple.com/library/content/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html) (OSX),event ports (Solaris) 或者 [IOCP](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365198.aspx) (Windows) 的机制。 +当操作系统确定某个文件的描述符准备完毕,时间轮询机制将把它转换成合适的事件,然后通过那个事件触发相对应的回调函数。 +你可以通过 [这里](https://www.youtube.com/watch?v=P9csgxBgaZ8) 学习到更多有关这个过程的知识。 + +相比较而言,工作池使用一个真实的队列,里边装的都是要被处理的任务。 +一个工作线程从这个队列中取出一个任务,开始处理它。当完成之后这个工作线程在事件循环机制中发出一个“至少有一个任务处于完成状态”的信息。 + +### 对于应用设计而言,这意味着什么? +在“来一个客户,开辟一个线程”的应用中,诸如 Apache,每个等待的客户被分配到对应的线程中去处理。 +如果处理某个客户端的线程阻塞了,操作系统会终止它,并给予下一个客户端一个机会去处理。 +操作系统必须确保客户端是需要少量的工作,而不是被需要更多工作的客户端惩罚。 + +因为 Node 用少量的线程处理许多客户端,因此如果在处理某个客户端的时候阻塞了,它不能给予其它客户端机会,而是一直等待直到完成全部的回调函数或者任务。 +*因此,对待每个请求客户都应该公平,这是你程序的责任。*. +这意味着,对于每个客户端,在任何简单的回调函数或者任务中,你不应该做太多的事情。 + +这是为什么 Node 能够那么成规模地处理,同时也意味着你有义务确保公平排程。 +下一部分将探讨如何对事件循环和工作池进行公平排程。 + +## 不要阻塞你的时间轮询 +时间轮询关注着每个新的客户端连接,协调产生一个回应。 +所有这些进入的请求和输出的应答都要通过事件轮询机制。 +这意味着如果你的时间轮询在某个地方花费太多的时间,所有现在和新的客户端请求都得不到机会了。 + +所以,你应该保证你绝不阻塞时间轮询。 +换句话说,每个 JavaScript 回调应该很快就可以完成。 +这些当然对于 `await`,`Promise.then` 也同样适用。 + +一个能确保做到这一点的方法是解释关于你回调任何的 ["计算复杂度"](https://en.wikipedia.org/wiki/Time_complexity)。 +如果你的回调函数接受固定几个步骤可以完成任务,无论你的参数是什么,那么你总能保证每个等候的请求者一个公平的轮询。 +如果回调根据其参数采取不同的步骤, 则应考虑根据参数导致多长时间执行完该任务。 + +例子 1:固定的回调。 + +```javascript +app.get('/constant-time', (req, res) => { + res.sendStatus(200); +}); +``` + +例子 2:一个 `O(n)` 回调。该回调对于小的输入 `n` 执行很快,但是 `n` 如果很大,会执行得很慢。 + +```javascript +app.get('/countToN', (req, res) => { + let n = req.query.n; + + // n iterations before giving someone else a turn + for (let i = 0; i < n; i++) { + console.log(`Iter {$i}`); + } + + res.sendStatus(200); +}); +``` + +例子 3:一个 `O(n^2)` 函数回调。 该回调对于小的输入 `n` 同样执行很快, 但是 `n` 如果很大,会比之前 `O(n)` 那个例子慢得多。 + +```javascript +app.get('/countToN2', (req, res) => { + let n = req.query.n; + + // n^2 iterations before giving someone else a turn + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + console.log(`Iter ${i}.${j}`); + } + } + + res.sendStatus(200); +}); +``` + +### 你应当注意些什么呢? +Node 使用谷歌的 V8 引擎处理 JavaScript,对于大部分操作确实很快。 +对于这个规则的例外是正则表达式以及 JSON 的处理,下面会讨论。 + +但是,对于复杂的任务你应当考虑限定输入范围,拒绝会导致太长时间执行的输入。 +那样的话,即便你的输入相当长而且复杂,因为你限定了输入范围,你也可以确保回调函数在最长的那个参数输入时不会花费比最糟糕的可接受执行时间还要长。 +你甚至可以评估此回调函数最糟糕执行时间,根据你的上下文决定此运行时间是否可以接受。 + +### 阻塞事件轮询:REDOS +一个灾难性地阻塞事件轮询的普遍方法是使用“脆弱”的 [正则表达式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)。 + +#### 避免脆弱的正则表达式 +一个正则表达式能够根据一定的规则匹配到一个输入的字符串。 +我们通常认为正则表达式匹配要求单通过输入字符串—— `O(n)` 时间,其中 `n` 是输入字符串的长度。 +这在大部分情况下的确是这样。 +不幸的是,在某些情况下,正则表达式匹配随着输入字符串呈指数增长——时间是 `O(2^n)`。 +一个指数的旅行意味着如果引擎需要 `x` 时间行程来确定匹配;如果我们只增加一个字符到输入字符串,它将需要 `2 * x` 的时间。 +由于行程数与所需时间呈线性关系,因此,此评估的效果将是阻止事件循环。 + +*易受攻击的正则表达式* 是正则表达式引擎可能需要指数时间的一种, 它使您在“恶意输入” [REDOS](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS)上呈现“邪恶输入”。 +无论您的正则表达式模式是易受攻击的(例如,正则表达式引擎可能会在其上使用指数时间),这实际上是一个很难回答的问题。并且根据您是用 Perl、Python、Ruby、Java、JavaScript 等等来决定的,但这里有在所有这些语言中应用的一些经验法则: + +1. 避免嵌套量词,如 `(a+)*`。节点的正则表达式引擎可以快速处理其中的一些,但其它的则是易受攻击的。 +2. 避免带有“或”的重叠情况,如 `(a|a)*`。同样,这些有时是快速的。 +3. 避免使用回溯,如 `(a.*) \1`。没有正则表达式引擎可以保证在线性时间内评估这些。 +4. 如果您正在进行简单的字符串匹配,请使用 `indexOf` 或本地等效项。这将是更便宜,永远不会超过 `O(n)` 的时间。 + +如果您不确定正则表达式是否易受攻击,请记住:即使对于易受攻击的正则表达式和长输入字符串,该节点通常也不会有 *匹配* 的问题报告。 +当存在不匹配的情况时,指数行为要触发;但 Node 是无法确定的,直到它通过输入字符串尝试了许多路径。 + +#### 一个 REDOS 例子 +下面是一个易受攻击的示例。 正则表达式将其服务器暴露给 REDOS: + +```javascript +app.get('/redos-me', (req, res) => { + let filePath = req.query.filePath; + + // REDOS + if (fileName.match(/(\/.+)+$/)) { + console.log('valid path'); + } + else { + console.log('invalid path'); + } + + res.sendStatus(200); +}); +``` + +在这个例子中,用脆弱的正则表达式检查一个合法的路径是 (糟糕的!)。 +它匹配的字符串是以 "/" 分隔的名称序列,如 "/a/b/c"。 +这是危险的,因为它违反了规则 1:它有一个双嵌套的量词。 + +如果客户端查询的是路径 `///.../\n` (100个“/”后跟正则表达式的“.”不匹配的换行符),则事件循环将有效地永久执行,并阻止事件循环。 +此客户端的 REDOS 攻击会导致所有其它客户端在此正则表达式匹配完成之前得不到机会执行了。 + +因此,您应该警惕使用复杂的正则表达式来验证用户输入。 + +#### 抵制 REDOS 资源 +这里提供了你一些工具帮助你检查你的正则表达式是否安全,像: +- [ 安全的正则表达式 ](https://github.com/substack/safe-regex) +- [rxxr2](http://www.cs.bham.ac.uk/~hxt/research/rxxr2/). +但是上述都并不能保证能够捕获全部的脆弱正则表达式。 + +另一个方案是使用一个不同的正则表达式引擎。 +你可以使用 [node-re2](https://github.com/uhop/node-re2) 模块,它使用谷歌的超快正则表达式引擎 [RE2](https://github.com/google/re2)。 +但注意,RE2 对 Node 正则表达式不是 100% 兼容,所以如果你想用 node-re2 模块来处理你的正则表达式的话,请检查你的表达式。 +这里尤其值得提醒的是,编译过的正则表达式不被 node-re2 支持。 + +如果你想匹配一些较为“明显”的东西,如网络路径或者是文件路径,请在 [正则表达式库](http://www.regexlib.com) 中寻找到对应例子,或者使用一个 npm 的模块,如 [ip-regex](https://www.npmjs.com/package/ip-regex)。 + +### 阻塞事件轮询:Node 的核心模块 +一些 Node 的核心模块有同步的高消费的 API 方法,包含: +- [加密](https://nodejs.org/api/crypto.html) +- [压缩](https://nodejs.org/api/zlib.html) +- [文件系统](https://nodejs.org/api/fs.html) +- [子进程](https://nodejs.org/api/child_process.html) + +这些 API 是高消费的,因为它们包括了非常巨大的计算(体现在加密、压缩上),需要 I/O(体现在文件 I/O),或者两者都有潜在包含(体现在子进程处理上)。这些 API 是为脚本提供方便,并非让你在服务器上下文中使用。如果你在事件循环中使用它们,则需要花费比一般的 JavaScript 更长的执行时间而且阻塞事件轮询。 + +对于一个服务器而言,*你不应当使用以下同步的 API 函数*: +- 加密: + - `crypto.randomBytes`(同步版本) + - `crypto.randomFillSync` + - `crypto.pbkdf2Sync` + - 同时你应当小心地对加密和解密给予大数据输入。 +- 压缩: + - `zlib.inflateSync` + - `zlib.deflateSync` +- 文件系统: + - 不能使用同步文件系统方法 API 函数。举个例子,如果你在一个[分布式文件系统](https://en.wikipedia.org/wiki/Clustered_file_system#Distributed_file_systems),像 [NFS](https://en.wikipedia.org/wiki/Network_File_System),则访问次数会发生很大变化。 +- 子进程: + - `child_process.spawnSync` + - `child_process.execSync` + - `child_process.execFileSync` + +此列表在 Node 9 时是合理完成的。 + +### 阻塞事件循环:JSON DOS +`JSON.parse` 以及 `JSON.stringify` 是其它潜在高消费的操作。 +当这些输入的长度是 `O(n)` 时,对于大型的 `n` 消耗的时间惊人的长。 + +如果您的服务器操作 JSON 对象(特别是来自客户端),则应谨慎处理在事件循环上使用的对象或字符串的大小。 + +关于 JSON 阻止事件循环的示例:我们创建一个大小为 2^21 的 JSON 的对象,然后用 `JSON.stringify` 序列化它;在此字符串上运行 `indexOf` 函数,然后使用 JSON.parse 解析它。 `JSON.stringify` 字符串为 50MB。字符串化对象耗时 0.7 秒,对这个 50MB 的字符串使用 indexOf 函数耗时 0.03 秒,用了 1.3 秒解析字符串。 + +```javascript +var obj = { a: 1 }; +var niter = 20; + +var before, res, took; + +for (var i = 0; i < len; i++) { + obj = { obj1: obj, obj2: obj }; // Doubles in size each iter +} + +before = process.hrtime(); +res = JSON.stringify(obj); +took = process.hrtime(n); +console.log('JSON.stringify took ' + took); + +before = process.hrtime(); +res = str.indexOf('nomatch'); +took = process.hrtime(n); +console.log('Pure indexof took ' + took); + +before = process.hrtime(); +res = JSON.parse(str); +took = process.hrtime(n); +console.log('JSON.parse took ' + took); +``` + +有一些 npm 的模块提供了异步的 JSON API 函数,参考: +- [JSONStream](https://www.npmjs.com/package/JSONStream),有关于流的函数。 +- [Big-Friendly JSON](https://github.com/philbooth/bfj),使用下面概述的事件循环模式,具有流 api 以及标准 JSON api 的异步版本。 + +### 复杂的计算而不阻塞的事件循环 +假设你想在 JavaScript 处理一个复杂的计算,而又不想阻塞事件循环。 +你有两种选择:分区或卸载。 + +#### 分区 +你可以把你的复杂计算 *拆分开*,然后让每个计算分别运行在事件循环中,不过你要定期地让其它一些等待的事件有机会执行。 +在 JavaScript 中,存储一个在闭包中的持续任务很容易,请看例子 2。 + +举个例子,假设你想计算 `1` to `n` 的平均值。 + +例子1:不分区算平均数,开销是 `O(n)` +```javascript +for (let i = 0; i < n; i++) + sum += i; +let avg = sum / n; +console.log('avg: ' + avg); +``` + +例子2:分区算平均值,每个 `n` 开销为 `O(1)`。 +```javascript +function asyncAvg(n, avgCB) { + // Save ongoing sum in JS closure. + var sum = 0; + function help(i, cb) { + sum += i; + if (i == n) { + cb(sum); + return; + } + + // "Asynchronous recursion". + // Schedule next operation asynchronously. + setImmediate(help.bind(null, i+1, cb)); + } + + // Start the helper, with CB to call avgCB. + help(1, function(sum){ + var avg = sum/n; + avgCB(avg); + }); +} + +asyncAvg(n, function(avg){ + console.log('avg of 1-n: ' + avg); +}); +``` + +你可以把此规则应用到数组迭代和其它等方面。 + +#### 卸载 +如果你需要做更复杂的任务,分区不是一个好选项。这是因为分区只能在事件循环中使用,并且你不会受益于多个核心几乎肯定可以在您的机器上。 +*请记住,时间轮询只是协调客户端的请求,而不是完成它们各自的任务。* +对一个复杂的任务,把它从事件循环中转义到工作池上。 + +##### 如何进行卸载? +对目标工作池而言,在此你有二个选择决定到底选择哪个来卸载工作。 +1. 你可以通过开发 [C++ 插件](https://nodejs.org/api/addons.html) 的方式使用内置的 Node 工作池。稍早之前的 Node 版本,通过使用 [NAN](https://github.com/nodejs/nan) 的方式编译你的 C++ 插件,在新版的 Node 上使用 [N-API](https://nodejs.org/api/n-api.html)。 [node-webworker-threads](https://www.npmjs.com/package/webworker-threads) 提供了你一个仅用 JavaScript 就可以访问 Node 的工作池的方式。 +2. 您可以创建和管理自己专用于计算的工作池,而不是节点的 I/O 主题工作池。最直接的方法就是使用 [子进程](https://nodejs.org/api/child_process.html) 或者是 [集群](https://nodejs.org/api/cluster.html)。 + +你 *不* 应该为任何请求都创建一个[ 子进程 ](https://nodejs.org/api/child_process.html)。 +你可以快速地接受客户端的全部请求,而不是创建和管理这些子进程,否则你的服务器就变成了一个 [Fork 炸弹](https://en.wikipedia.org/wiki/Fork_bomb)。 + +##### 卸载方法的缺陷 +卸载方法的缺点是它以 *通信成本* 的形式招致开销。 +仅允许事件循环查看应用程序的“命名空间”(JavaScript 状态)。 +从工作线程中,您不能在事件循环的命名空间中操作 JavaScript 对象。 +相反地,您必须序列化和反序列化要共享的任何对象。 +然后,该工作线程可以对其自己的这些对象的副本进行操作,并将修改后的对象(或“补丁”) 返回到事件循环。 + +有关序列化问题,请参阅 JSON 文档部分。 + +##### 一些关于卸载的建议 +您可能希望区分 CPU 密集型和 I/O 密集型任务,因为它们具有明显不同的特性。 + +CPU 密集型任务只有在计划工作时才会取得进展,并且必须将该工作人员安排到您的计算机的 [逻辑核心](https://nodejs.org/api/os.html#os_os_cpus)中。 + +如果你有 4 个逻辑核心和 5 名工作线程,这些工作线程中的一个不能取得进展。 +因此,您要为该工作线程支付开销(内存和计划成本),并且无法弥补。 + +I/O 密集型任务包括查询外部服务提供程序(DNS、文件系统等)并等待其响应。 +当具有 I/O 密集型任务的工作线程正在等待其响应时,它没有其它工作可做,并且可以由操作系统取消计划,从而使另一个工作人员有机会提交他们的请求。 +因此,*I/O 密集型任务即使关联线程没有运行,也将取得进展*。 +像数据库和文件系统这样的外部服务提供程序经过高度优化,可以同时处理许多挂起的请求。 +例如,文件系统将检查一大组挂起的写入和读取请求,以合并冲突更新并以最佳顺序检索文件(请参阅 [这些幻灯片](http://researcher.ibm.com/researcher/files/il-AVISHAY/01-block_io-v1.3.pdf))。 + +如果只依赖一个工作池(例如 Node 工作池),则 CPU 绑定和 I/O 绑定的工作由于不同特性可能会损害应用程序的性能。 + +因此,您可能希望维护单独的计算工作池。 + +#### 卸载:总结 +对于简单的任务:比如遍历任意长数组的元素,分区可能是一个很好的选择。 +如果计算更加复杂,则卸载是一种更好的方法:通信成本(即在事件循环和工作池之间传递序列化对象的开销)被使用多个内核的好处抵消。 +但是,如果服务器严重依赖复杂的计算,则应该考虑 Node 是否真的很适合?Node 擅长于 I/O 绑定工作,但对于昂贵的计算,它可能不是最好的选择。 + +如果采用卸载方法,请参阅“不阻塞工作池”一节。 + +## 不要阻塞你的工作池 +Node 由 `k` 个工作线程组成了工作池。 +如果您使用上面讨论过的卸载范式,则可能有一个单独的计算工作池,同样的原则也适用。 +在这两种情况下,让我们假设 `k` 比您可能同时处理的客户端数量要小得多。 +这与节点的“一个线程为许多客户端”的哲学是一致的,这是它的可伸缩性秘诀。 + +如上所述:每个工作线程完成其当前任务,然后再继续执行工作池队列中的下一项。 + +现在,处理客户请求所需的任务成本将发生变化。 +有些任务可以快速完成(例如读取短文件或缓存文档,或者生成少量的随机字节),而另一些则需要更长的时间(即读取较大或缓存的文件,或生成更多的随机字节)。 +您的目标应该是 *最小化任务时间的变化*,并且您应该使用 *任务分区* 来完成此工作。 + +### 最小化任务时间的变化 +如果工作线程的当前任务比其它任务昂贵得多,则无法处理其它未决任务。 +换言之,*每个相对长的任务有效地减少了工作池的大小,直到完成*。 +这是不可取的。因为从某种程度上说,工作池中的工作线程越多,工作池吞吐量(任务/秒)就越大,因此服务器吞吐量(客户端请求/秒)就越大。 +一个具有相对昂贵任务的客户端将减少工作池的吞吐量,从而降低服务器的吞吐量。 + +为避免这种情况,应尽量减少提交给工作池的任务长度的变化。 +虽然将 I/O 请求(DB、FS 等)访问的外部系统视为黑盒是适当的;但您应该知道这些 I/O 请求的相对成本,并应避免提交您可能期望特别长的请求。 + +两个示例应说明任务时间可能发生的变化。 + +#### 变体示例: 长时间运行的文件系统读取 +假设您的服务器必须读取文件以处理某些客户端请求。 +在了解 Node 的 [文件系统](https://nodejs.org/api/fs.html) 的 API 之后,您选择使用 `fs.readFile()` 进行简单操作。 +但是,`fs.readFile()` 是([当前](https://github.com/nodejs/node/pull/17054))未分区的:它提交一个 `fs.read()` 任务来跨越整个文件。 +如果您为某些用户阅读较短的文件,并为其它人读取较长的文件,`fs.readFile()` 可能会在任务长度上引入显著的变化,从而损害工作池吞吐量。 + +对于最坏的情况,假设攻击者可以说服您的服务器读取 *任意* 文件(这是一个 [目录遍历漏洞](https://www.owasp.org/index.php/Path_Traversal))。 +如果您的服务器运行的是 Linux,攻击者可以命名一个非常慢的文件:[`/dev/random`](http://man7.org/linux/man-pages/man4/random.4.html)。 +对于所有实际的目的,`/dev/random` 是无限缓慢的;每个工作线程都被要求读取 `/dev/random`,这样下去将永远不会完成这项任务。 +然后,攻击者提交 `k` 个请求,其中一个用于每个工作线程,而使用该工作池的其它客户端请求也不会取得进展。 + +#### 变体示例: 长时间运行的加密操作 +假设您的服务器使用 [`crypto.randomBytes()`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) 来生成加密的安全随机字节。`crypto.randomBytes()` 是不分区的:它创建单个 `randomBytes()` 任务,以生成您请求的字节数。 +如果为某些用户创建的字节数较少,并且其它字节数较多;则 `crypto.randomBytes()` 是任务长度变化的另一个来源。 + +### 任务分区 +具有可变时间成本的任务可能会损害工作池的吞吐量。 +为了尽量减少任务时间的变化,应尽可能将每个任务 *划分* 为类似成本的子任务。 +当每个子任务完成时,它应该提交下一个子任务;并且当最终的子任务完成时,它应该通知提交者。 + +要继续使用 `fs.readFile()` 示例,应改用 `fs.read()`(手动分区)或 `ReadStream`(自动分区)。 + +同样的原理也适用于 CPU 绑定任务; `asyncAvg` 示例可能不适用于事件循环,但它非常适合于工作池。 + +将任务划分为子任务时,较短的任务将扩展为少量的子任务,而更长的任务将扩展到更多的子任务。 +在较长任务的每个子任务之间,分配给它的工作线程可以从另一个更短的任务中处理子任务,从而提高工作池的总体任务吞吐量。 + +请注意:完成的子任务数对于辅助池的吞吐量不是一个有用的度量。 +相反,请关注完成的 *任务* 数。 + +### 避免任务分区 +记得任务分区的目的是尽量减少任务时间的变化。 +如果可以区分较短的任务和较长的任务(例如,求和数组与排序数组),则可以为每个任务类创建一个工作池。 +将较短的任务和更长的任务路由到单独的工作池是减少任务时间变化的另一种方法。 + +为支持此方法,分区任务会招致开销(创建工作池任务表示法和操作工作池队列的成本),而避免分区会为您节省额外旅行到工作池的成本。 + +这种方法的缺点是:所有这些工作池中的工作线程都将承担空间和时间开销,并将在 CPU 时间内相互竞争。 +请记住:每个 CPU 绑定任务只在计划时才会取得进展。 +因此,您应该只在仔细分析后才考虑此方法。 + +### 工作池:总结 +无论您只使用节点工作池还是维护单独的工作池,都应优化池的任务吞吐量。 + +为此,请使用任务分区最小化任务时间的变化。 + +## 使用 npm 模块的风险 +虽然 Node 核心模块为各种应用程序提供了构建块,但有时还需要更多的内容。Node 的开发人员从 [npm 生态系统](https://www.npmjs.com/) 中获益良多,有成百上千个模块提供功能以加速您的开发过程。 + +但是,请记住,这些模块中的大多数是由第三方开发人员编写的;通常只用最努力的保证发布。使用 npm 模块的开发人员应该关注两件事,尽管后者经常被遗忘。 +1. 它是否尊重其 API? +2. 它的 api 可能会阻止事件循环或工作线程吗? +许多模块对它们 API 开销没有产生任何影响,这对社区不利。 + +对于简单的 API,您可以估计 API 的成本;字符串操作的成本并不难捉摸。 +但在许多情况下却不清楚 API 可能花费多少。 + +*如果您调用的 API 可能会做一些昂贵的事情,请加倍检查成本;要求开发人员记录它,或者自己检查源代码(并提交一个公关记录成本)。* + +请记住:即使 API 是异步的,您也不知道它可能在每个分区的工作线程或事件循环上花费多少时间。 +例如,假设在上面给出的 `asyncAvg` 示例中,每个对 helper 函数的调用都概括了数字的一半而不是其中的一个。 +然后这个函数仍然是异步的,但每个分区的成本将是 `O(n)` 而不是 `O(1)`, 使它更不安全地使用任意值 `n`。 + +## 最后总结 +Node 有两种类型的线程:一个事件循环和一个由 `k` 个工作线程。 +事件循环负责 JavaScript 回调和非阻塞 I/O,工作人员执行与 C++ 代码对应的、完成异步请求的任务,包括阻塞 I/O 和 CPU 密集型工作。 +两种类型的线程一次只处理一个活动。 +如果任何回调或任务需要很长时间,则运行它的线程将变为 *阻止*。 +如果应用程序阻止回调或任务,这可能会导致吞吐量下降(客户端/秒),并且在最坏情况下完全拒绝服务。 + +要编写高吞吐量、防 DoS 攻击的 web 服务,您必须确保在良性和恶意输入的情况下,您的事件循环和您的工作线程都不会阻塞。 diff --git a/locale/zh-cn/docs/guides/event-loop-timers-and-nexttick.md b/locale/zh-cn/docs/guides/event-loop-timers-and-nexttick.md new file mode 100644 index 0000000000000..f0e6e1e47d05f --- /dev/null +++ b/locale/zh-cn/docs/guides/event-loop-timers-and-nexttick.md @@ -0,0 +1,344 @@ +--- +title: Node.js 事件循环,定时器和 process.nextTick() +layout: docs.hbs +--- + +# Node.js 事件循环,定时器和 `process.nextTick()` + +## 什么是事件轮询 + +事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。 + +既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到 *轮询* 队列中等待时机执行。我们在本文后面会进行详细介绍。 + +## 事件轮询机制解析 + +当 Node.js 启动后,它或初始化事件轮询;处理已提供的输入脚本(或丢入 [REPL][],本文不涉及到),它可能会调用一些异步的 API 函数调用,安排任务处理事件,或者调用 `process.nextTick()`,然后开始处理事件循环。 + +下面的图表显示了事件循环的概述以及操作顺序。 + +``` + ┌───────────────────────────┐ +┌─>│ timers │ +│ └─────────────┬─────────────┘ +│ ┌─────────────┴─────────────┐ +│ │ pending callbacks │ +│ └─────────────┬─────────────┘ +│ ┌─────────────┴─────────────┐ +│ │ idle, prepare │ +│ └─────────────┬─────────────┘ ┌───────────────┐ +│ ┌─────────────┴─────────────┐ │ incoming: │ +│ │ poll │<─────┤ connections, │ +│ └─────────────┬─────────────┘ │ data, etc. │ +│ ┌─────────────┴─────────────┐ └───────────────┘ +│ │ check │ +│ └─────────────┬─────────────┘ +│ ┌─────────────┴─────────────┐ +└──┤ close callbacks │ + └───────────────────────────┘ +``` + +*注意:每个框框里每一步都是事件循环机制的一个阶段。* + +每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。 + +由于这些操作中的任何一个都可能计划 _更多的_ 操作,并且在 **轮询** 阶段处理的新事件由内核排队,因此在处理轮询事件时,轮询事件可以排队。因此,长时间运行回调可以允许轮询阶段运行大量长于计时器的阈值。有关详细信息,请参阅 [**计时器**](#timers) 和 [**轮询**](#poll) 部分。 + +_**注意:** 在 Windows 和 Unix/Linux 实现之间存在细微的差异,但这对演示来说并不重要。最重要的部分在这里。实际上有七或八个步骤,但我们关心的是 Node.js 实际上使用以上的某些步骤。_ + + +## 阶段概述 + +* **定时器**:本阶段执行已经安排的 `setTimeout()` 和 `setInterval()` 的回调函数。 +* **待定回调**:执行延迟到下一个循环迭代的 I/O 回调。 +* **idle, prepare**:仅系统内部使用。 +* **轮询**:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 `setImmediate()` 排定的之外),其余情况 node 将在此处阻塞。 +* **检测**:`setImmediate()` 回调函数在这里执行。 +* **关闭的回调函数**:一些准备关闭的回调函数,如:`socket.on('close', ...)`。 + +在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则关闭干净。 + +## 阶段的详细概述 + +### 定时器 + +计时器指定 _可执行所提供回调_ 的 **阈值**,而不是用户希望其执行的确切时间。计时器回调将尽可能早地运行,因为它们可以在指定的时间间隔后进行调度。但是,操作系统调度或其它回调的运行可能会延迟它们。 + +_**注意**:[**轮询** 阶段](#poll) 控制何时定时器执行。_ + +例如,假设您计划在 100 毫秒后执行超时阈值,然后您的脚本开始异步读取文件,这需要 95 毫秒: + +```js +const fs = require('fs'); + +function someAsyncOperation(callback) { + // Assume this takes 95ms to complete + fs.readFile('/path/to/file', callback); +} + +const timeoutScheduled = Date.now(); + +setTimeout(() => { + const delay = Date.now() - timeoutScheduled; + + console.log(`${delay}ms have passed since I was scheduled`); +}, 100); + + +// do someAsyncOperation which takes 95 ms to complete +someAsyncOperation(() => { + const startCallback = Date.now(); + + // do something that will take 10ms... + while (Date.now() - startCallback < 10) { + // do nothing + } +}); +``` + +当事件循环进入 **轮询** 阶段时,它有一个空队列(此时 `fs.readFile()` 尚未完成),因此它将等待毫秒数,直到达到最快的计时器阈值为止。当它等待 95 毫秒通过时,`fs.readFile()` 完成读取文件,它需要 10 毫秒完成的回调将添加到 **轮询** 队列中并执行。当回调完成时,队列中不再有回调,因此事件循环将看到已达到最快计时器的阈值,然后将回滚到 **计时器** 阶段,以执行定时器的回调。在本示例中,您将看到计划中的计时器和执行的回调之间的总延迟将为 105 毫秒。 + +注意:为了防止 **轮询** 阶段饿死事件循环,[libuv][](实现 Node.js +事件循环和平台的所有异步行为的 C 函数库),在停止轮询以获得更多事件之前,还有一个最大的(系统依赖)。 + +### 挂起的回调函数 + +此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 `ECONNREFUSED`,则某些 \*nix 的系统希望等待报告错误。这将被排队以在 **挂起的回调** 阶段执行。 + +### 轮询 + +**轮询** 阶段有两个重要的功能: + +1. 计算应该阻塞和轮询 I/O 的时间。 +2. 然后,处理 **轮询** 队列里的事件。 + +当事件循环进入 **轮询** 阶段且 _没有计划计时器时_,将发生以下两种情况之一: + +* _如果 **轮询** 队列 **不是空的**_,事件循环将循环访问其回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬限制。 + +* _如果 **轮询** 队列 **是空的**_,还有两件事发生: + * 如果脚本已按 `setImmediate()` 排定,则事件循环将结束 **轮询** 阶段,并继续 **检查** 阶段以执行这些计划脚本。 + + * 如果脚本 **尚未** 按 `setImmediate()`排定,则事件循环将等待回调添加到队列中,然后立即执行。 + +一旦 **轮询** 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。 + +### 检查阶段 + +此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本已排队使用 `setImmediate()`,则事件循环可能继续到 **检查** 阶段而不是等待。 + +`setImmediate()` 实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 **轮询** 阶段完成后执行。 + +通常,在执行代码时,事件循环最终会命中轮询阶段,等待传入连接、请求等。但是,如果回调已计划为 `setImmediate()`,并且轮询阶段变为空闲状态,则它将结束并继续到检查阶段而不是等待轮询事件。 + +### 关闭的回调函数 + +如果套接字或处理函数突然关闭(例如 `socket.destroy()`),则`'close'` 事件将在这个阶段发出。否则它将通过 `process.nextTick()` 发出。 + +## `setImmediate()` 对比 `setTimeout()` + +`setImmediate` 和 `setTimeout()` 很类似,但何时调用行为完全不同。 + +* `setImmediate()` 设计为在当前 **轮询** 阶段完成后执行脚本。 +* `setTimeout()` 计划在毫秒的最小阈值经过后运行的脚本。 + +执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时将受进程性能的约束(这可能会受到计算机上运行的其它应用程序的影响)。 + +例如,如果运行的是不属于 I/O 周期(即主模块)的以下脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束: + +```js +// timeout_vs_immediate.js +setTimeout(() => { + console.log('timeout'); +}, 0); + +setImmediate(() => { + console.log('immediate'); +}); +``` + +``` +$ node timeout_vs_immediate.js +timeout +immediate + +$ node timeout_vs_immediate.js +immediate +timeout +``` + +但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用: + +```js +// timeout_vs_immediate.js +const fs = require('fs'); + +fs.readFile(__filename, () => { + setTimeout(() => { + console.log('timeout'); + }, 0); + setImmediate(() => { + console.log('immediate'); + }); +}); +``` + +``` +$ node timeout_vs_immediate.js +immediate +timeout + +$ node timeout_vs_immediate.js +immediate +timeout +``` + +使用 `setImmediate()` 超过 `setTimeout()` 的主要优点是 `setImmediate()` 在任何计时器(如果在 I/O 周期内)都将始终执行,而不依赖于存在多少个计时器。 + +## `process.nextTick()` + +### 理解 `process.nextTick()` + +您可能已经注意到 `process.nextTick()` 在关系图中没有显示,即使它是异步 API 的一部分。这是因为 `process.nextTick()` 在技术上不是事件循环的一部分。相反,无论事件循环的当前阶段如何,都将在当前操作完成后处理 `nextTickQueue`。 + +回顾我们的关系图,任何时候您调用 `process.nextTick()` 在给定的阶段中,所有传递到 `process.nextTick()` 的回调将在事件循环继续之前得到解决。这可能会造成一些糟糕的情况, 因为**它允许您通过进行递归 `process.nextTick()` 来“饿死”您的 I/O 调用**,阻止事件循环到达 **轮询** 阶段。 + +### 为什么会允许这样? + +为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例: + +```js +function apiCall(arg, callback) { + if (typeof arg !== 'string') + return process.nextTick(callback, + new TypeError('argument should be string')); +} +``` + +代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对 API 进行了更新,允许将参数传递给 `process.nextTick()`,允许它在回调后传递任何参数作为回调的参数传播,这样您就不必嵌套函数了。 + +我们正在做的是将错误传递给用户,但仅在我们允许用户的其余代码执行之后。通过使用`process.nextTick()`,我们保证 `apiCall()` 始终在用户代码的其余部分 *之后* 运行其回调函数,并在允许事件循环 *之前* 继续进行。为了实现这一点,JS 调用栈被允许 +展开,然后立即执行提供的回调,允许进行递归调用 `process.nextTick()`,而不达到 `RangeError: 超过 v8 的最大调用堆栈大小`。 + +这种哲学可能会导致一些潜在的问题。 +以此代码段为例: + +```js +let bar; + +// this has an asynchronous signature, but calls callback synchronously +function someAsyncApiCall(callback) { callback(); } + +// the callback is called before `someAsyncApiCall` completes. +someAsyncApiCall(() => { + // since someAsyncApiCall has completed, bar hasn't been assigned any value + console.log('bar', bar); // undefined +}); + +bar = 1; +``` + +用户将 `someAsyncApiCall()` 定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给 `someAsyncApiCall()` 的回调在同一阶段调用事件循环,因为 `someAsyncApiCall()` 实际上并没有异步执行任何事情。因此,回调尝试引用 `bar`,即使它在范围内可能还没有该变量,因为脚本无法运行到完成。 + +通过将回调置于 `process.nextTick()` 中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有变量、函数等。它还具有不允许事件循环继续的优点。在允许事件循环继续之前,对用户发出错误警报可能很有用。下面是使用 `process.nextTick()` 的上一个示例: + +```js +let bar; + +function someAsyncApiCall(callback) { + process.nextTick(callback); +} + +someAsyncApiCall(() => { + console.log('bar', bar); // 1 +}); + +bar = 1; +``` + +这又是另外一个真实的例子: + +```js +const server = net.createServer(() => {}).listen(8080); + +server.on('listening', () => {}); +``` + +只有端口通过时,端口才会立即被绑定。因此,可以立即调用 `'listening'` 回调。问题是 `.on('listening')` 回调将不会被设置的时间。 + +为了绕过此现象,`'listening'` 事件在 `nextTick()` 中排队,以允许脚本运行到完成阶段。这允许用户设置所需的任何事件处理程序。 + +## `process.nextTick()` 对比 `setImmediate()` + +就用户而言我们有两个类似的调用,但它们的名称令人费解。 + +* `process.nextTick()` 在同一个阶段立即执行。 +* `setImmediate()` 在以下迭代或 ‘tick’ 上触发事件循环。 + +实质上,应该交换名称。`process.nextTick()` 比 `setImmediate()` 触发得更直接,但这是过去遗留的,所以不太可能改变。进行此开关将会破坏 npm 上的大部分软件包。每天都有新的模块在不断增长,这意味着我们每天等待,而更多的潜在破损在发生。 +虽然他们很迷惑,但名字本身不会改变。 + +*我们建议开发人员在所有情况下都使用 `setImmediate()`,因为它更容易被推理(并且它导致代码与更广泛的环境,如浏览器 JS 所兼容。)* + +## 为什么要使用 `process.nextTick()`? + +主要有两个原因: + +1. 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。 + +2. 有时在调用堆栈已解除但在事件循环继续之前,必须允许回调运行。 + +一个例子就是要符合用户的期望。简单示例: + +```js +const server = net.createServer(); +server.on('connection', (conn) => { }); + +server.listen(8080); +server.on('listening', () => { }); +``` + +假设 `listen()` 在事件循环开始时运行,但侦听回调被放置在 `setImmediate()` 中。除非通过主机名,否则将立即绑定到端口。为使事件循环继续进行,它必须命中 **轮询** 阶段,这意味着可能会收到连接,从而允许在侦听事件之前激发连接事件。 + +另一个示例运行的函数构造函数是从 `EventEmitter` 继承的,它想调用构造函数: + +```js +const EventEmitter = require('events'); +const util = require('util'); + +function MyEmitter() { + EventEmitter.call(this); + this.emit('event'); +} +util.inherits(MyEmitter, EventEmitter); + +const myEmitter = new MyEmitter(); +myEmitter.on('event', () => { + console.log('an event occurred!'); +}); +``` + +不能立即从构造函数中发出事件。因为脚本不会处理到用户为该事件分配回调的点。因此,在构造函数本身中可以使用 `process.nextTick()` 来设置回调,以便在构造函数完成后发出该事件,从而提供预期的结果: + +```js +const EventEmitter = require('events'); +const util = require('util'); + +function MyEmitter() { + EventEmitter.call(this); + + // use nextTick to emit the event once a handler is assigned + process.nextTick(() => { + this.emit('event'); + }); +} +util.inherits(MyEmitter, EventEmitter); + +const myEmitter = new MyEmitter(); +myEmitter.on('event', () => { + console.log('an event occurred!'); +}); +``` + +[libuv]: http://libuv.org +[REPL]: https://nodejs.org/api/repl.html#repl_repl diff --git a/locale/zh-cn/docs/guides/getting-started-guide.md b/locale/zh-cn/docs/guides/getting-started-guide.md new file mode 100644 index 0000000000000..80fce04199ae8 --- /dev/null +++ b/locale/zh-cn/docs/guides/getting-started-guide.md @@ -0,0 +1,28 @@ +--- +title: 入门教程 +layout: docs.hbs +--- + +# 在安装了 Node.js 之后,我怎么开始呢? + +一旦你已经安装了 Node,让我们尝试构建第一个 Web 服务器。 +请创建一个“app.js”文件,黏贴以下代码: + +```javascript +const http = require('http'); + +const hostname = '127.0.0.1'; +const port = 3000; + +const server = http.createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('Hello World\n'); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); +``` + +然后使用 ``` node app.js ``` 运行程序,访问 http://localhost:3000,你就会看到一个消息,写着“Hello World”。 diff --git a/locale/zh-cn/docs/guides/index.md b/locale/zh-cn/docs/guides/index.md new file mode 100644 index 0000000000000..7efcaf1cc2463 --- /dev/null +++ b/locale/zh-cn/docs/guides/index.md @@ -0,0 +1,31 @@ +--- +title: 指南 +layout: docs.hbs +--- + +# 指南 + +## 一般教程 + +- [入门教程](getting-started-guide/) +- [调试入门](debugging-getting-started/) +- [Node.js 应用一窥](simple-profiling/) +- [将 Node.js 网络应用安装到 Docker 中](nodejs-docker-webapp/) +- [迁移到安全的 Buffer 构造函数中](buffer-constructor-deprecation/) + + +## Node.js 核心概念 + +- [阻塞对比非阻塞一览](blocking-vs-non-blocking/) +- [Node.js 事件轮询,定时器 和 process.nextTick()](event-loop-timers-and-nexttick/) +- [不要阻塞你的事件轮询(或是工作池)](dont-block-the-event-loop/) +- [Node.js 中的定时器](timers-in-node/) + + +## 与模块相关的教程 + +- [HTTP 传输解析](anatomy-of-an-http-transaction/) +- [搭配不同的文件系统工作](working-with-different-filesystems/) +- [流的背压](backpressuring-in-streams/) +- [Postmortem 中的主要模块](domain-postmortem/) +- [如何发布 N-API 包](publishing-napi-modules/) diff --git a/locale/zh-cn/docs/guides/nodejs-docker-webapp.md b/locale/zh-cn/docs/guides/nodejs-docker-webapp.md new file mode 100644 index 0000000000000..f75e0f2b059ef --- /dev/null +++ b/locale/zh-cn/docs/guides/nodejs-docker-webapp.md @@ -0,0 +1,237 @@ +--- +title: 把一个 Node.js web 应用程序给 Docker 化 +layout: docs.hbs +--- + +# 把一个 Node.js web 应用程序给 Docker 化 + +本示例的目标是给你演示如何将一个 Node.js 的应用装入到 Docker 容器中。本教程旨在针对于开发人员,而 *非* 产品发布人员。此教程同样假定你有一个可以正常工作的 [Docker 安装](https://docs.docker.com/engine/installation/),并且对于 Node.js 的应用程序是如何组织的有一个大致的基本了解。 + +在本教程的第一部分我们在 Node.js 中创建一个 Web 的应用程序,然后我们为那个应用构建一个 Docker 镜像;最后我们将把那个镜像作为容器运行之。 + +Docker 允许你以应用程序所有的依赖全部打包成一个标准化的单元,这被成为一个容器。对于应用开发而言,一个容器就是一个蜕化到最基础的 Linux 操作系统。一个镜像是你加载到容器中的软件。 + + +## 创建 Node.js 应用 + +首先,创建一个新文件夹以便于容纳需要的所有文件,并且在此其中创建一个 `package.json` 文件,描述你应用程序以及需要的依赖: + +```json +{ + "name": "docker_web_app", + "version": "1.0.0", + "description": "Node.js on Docker", + "author": "First Last ", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.16.1" + } +} +``` + +配合着你的 `package.json` 请运行 `npm install`。如果你使用的 `npm` 是版本 5 或者之后的版本,这会自动生成一个 `package-lock.json` 文件,它将一起被拷贝进入你的 Docker 镜像中。 + +然后,创建一个 `server.js` 文件,使用 [Express.js](https://expressjs.com/) 框架定义一个 Web 应用: + +```javascript +'use strict'; + +const express = require('express'); + +// Constants +const PORT = 8080; +const HOST = '0.0.0.0'; + +// App +const app = express(); +app.get('/', (req, res) => { + res.send('Hello world\n'); +}); + +app.listen(PORT, HOST); +console.log(`Running on http://${HOST}:${PORT}`); +``` + +在稍后的步骤中我们将看一下借助使用官方的 Docker 镜像,你如何在 Docker 镜像中运行这个应用。首先,你需要一个构建一个应用程序的 Docker 应用。 + +## 创建一个名称为 `Dockerfile` 的文件 + +创建一个空文件,命名为 `Dockerfile`: + +```markup +touch Dockerfile +``` + +用你最喜欢的文本编辑器打开这个 `Dockerfile`。 + +我们要做的第一件事是定义我们需要从哪个镜像进行构建。这里我们将使用最新的 LTS(长期服务器支持版),`Node` 的版本号为 `8`。你可以从 [Docker 站点](https://hub.docker.com/) 获取相关镜像: + +```docker +FROM node:8 +``` + +下一步在镜像中创建一个文件夹存放应用程序代码,这将是你的应用程序工作目录: + +```docker +# Create app directory +WORKDIR /usr/src/app +``` + +此镜像中 Node.js 和 NPM 都已经安装,所以下一件事对于我们而言是使用 `npm` 安装你的应用程序的所有依赖。请注意,如果你的 `npm` 的版本是 4 或者更早的版本,`package-lock.json` 文件将不会自动生成。 + +```docker +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install +# If you are building your code for production +# RUN npm install --only=production +``` + +请注意,我们只是拷贝了 `package.json` 文件而非整个工作目录。这允许我们利用缓存 Docker 层的优势。bitJudo 对此有一个很好的解释,请 [见此](http://bitjudo.com/blog/2014/03/13/building-efficient-dockerfiles-node-dot-js/)。 + +在 Docker 镜像中使用 `COPY` 命令绑定你的应用程序: + +```docker +# Bundle app source +COPY . . +``` + +你的应用程序绑定的端口为 `8080`,所以你可以使用 `EXPOSE` 命令使它与 `docker` 的镜像做映射: + +```docker +EXPOSE 8080 +``` + +最后但同样重要的事是,使用定义运行时的 `CMD` 定义命令来运行应用程序。这里我们使用最简单的 `npm start` 命令,它将运行 `node server.js` 启动你的服务器: + +```docker +CMD [ "npm", "start" ] +``` + +你的 `Dockerfile` 现在看上去是这个样子: + +```docker +FROM node:8 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install +# If you are building your code for production +# RUN npm install --only=production + +# Bundle app source +COPY . . + +EXPOSE 8080 +CMD [ "npm", "start" ] +``` + +## .dockerignore 文件 + +在 `Dockerfile` 的同一个文件夹中创建一个 `.dockerignore` 文件,带有以下内容: + +``` +node_modules +npm-debug.log +``` + +这将避免你的本地模块以及调试日志被拷贝进入到你的 Docker 镜像中,以至于把你镜像原有安装的模块给覆盖了。 + +## 构建你的镜像 + +进入到 `Dockerfile` 所在的那个目录中,运行以下命令构建 Docker 镜像。开关符 `-t` 让你标记你的镜像,以至于让你以后很容易地用 `docker images` 找到它。 + +```bash +$ docker build -t /node-web-app . +``` + +Docker 现在将给出你的镜像列表: + +```bash +$ docker images + +# Example +REPOSITORY TAG ID CREATED +node 8 1934b0b038d1 5 days ago +/node-web-app latest d64d3505b0d2 1 minute ago +``` + +## 运行镜像 + +使用 `-d` 模式运行镜像将以分离模式运行 Docker 容器,使得容器在后台自助运行。开关符 `-p` 在容器中把一个公共端口导向到私有的端口,请用以下命令运行你之前构建的镜像: + +```bash +$ docker run -p 49160:8080 -d /node-web-app +``` + +把你应用程序的输出打印出来: + +```bash +# Get container ID +$ docker ps + +# Print app output +$ docker logs + +# Example +Running on http://localhost:8080 +``` + +如果你需要进入容器中,请运行 `exec` 命令: + +```bash +# Enter the container +$ docker exec -it /bin/bash +``` + +## 测试 + +为测试你的应用程序,给出与 Docker 映射过的端口号: + +```bash +$ docker ps + +# Example +ID IMAGE COMMAND ... PORTS +ecce33b30ebf /node-web-app:latest npm start ... 49160->8080 +``` + +在上面的例子中,在容器中 Docker 把端口号 `8080` 映射到你机器上的 `49160` 。 + +现在你可以使用 `curl`(如果需要的话请通过 `sudo apt-get install curl` 安装)调用你的程序了: + +```bash +$ curl -i localhost:49160 + +HTTP/1.1 200 OK +X-Powered-By: Express +Content-Type: text/html; charset=utf-8 +Content-Length: 12 +ETag: W/"c-M6tWOb/Y57lesdjQuHeB1P/qTV0" +Date: Mon, 13 Nov 2017 20:53:59 GMT +Connection: keep-alive + +Hello world +``` + +我们希望本教程能够帮助你起步,在 Docker 中运行一个简单的 Node.js 应用程序。 + +你也可以在以下一些地方寻觅到更多有关于 Docker 和基于 Docker 的 Node.js 相关内容: + +* [官方 Node.js 的 Docker 镜像](https://registry.hub.docker.com/_/node/) +* [Node.js 基于 Docker 使用的最佳经验](https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md) +* [官方 Docker 文档](https://docs.docker.com/) +* [在 StackOverFlow 上有关 Docker 标记内容](https://stackoverflow.com/questions/tagged/docker) +* [Docker Subreddit](https://reddit.com/r/docker) diff --git a/locale/zh-cn/docs/guides/publishing-napi-modules.md b/locale/zh-cn/docs/guides/publishing-napi-modules.md new file mode 100644 index 0000000000000..22796224fca7e --- /dev/null +++ b/locale/zh-cn/docs/guides/publishing-napi-modules.md @@ -0,0 +1,39 @@ +--- +title: 如何发布 N-API 包 +layout: docs.hbs +--- + +# 发布混合了 N-API 和 non-N-API 包 + +以下步骤基于 `iotivity-node` 进行说明: + - 首先,发布一个 non-N-API 版本: + - 更新 `package.json` 版本。对于 `iotivity-node`,目前版本号是 `1.2.0-2`。 + - 通览所有的清单(确保测试/示例/文档都正常)。 + - `npm publish` + - 其次,发布 N-API 版本: + - 更新 `package.json` 版本。 在这种情况下,`iotivity-node` 的版本变成了 `1.2.0-3`。 + 版本审查, 我们推荐以下预发布的版本架构,由 [semver.org](http://semver.org/#spec-item-9)提供。如: `1.2.0-napi`。 + - 通览所有的清单(确保测试/示例/文档都正常)。 + - `npm publish --tag n-api` + +在这个例子中,标记为 `n-api` 的发布版已经确保尽管其版本 1.2.0-3 比 non-N-API 的版本号要延后一些,但如果有人简单通过运行 `npm install iotivity-node` 安装 `iotivity-node`,它不会被安装而只会默认安装 non-N-API。此人应该运行 `npm install iotivity-node@n-api` 得到 N-API 的版本号,有关使用标签的更多详情请查阅 ["使用目标标签"][]。 + +# 在某个版本的 N-API 包中引入特定的依赖 + +为了将 `iotivity-node` 的 N-API 版本作为依赖进行添加安装,`package.json` 应如下形式呈现: + +```json +"dependencies": { + "iotivity-node": "n-api" +} +``` + +**注意:** 如 ["使用目标标签"][] 的解释,不同于一般的版本号,标记的版本号不能在 `package.json` 中通过版本范围(诸如 `"^2.0.0"` 的形式)进行追踪。所以,版本维护者使用相同的标签但选择了一个稍晚一些的版本, `npm update` 将收到稍晚的版本。这在当前试验环境下的 N-API 是可以接受的。为了依赖于一个 N-API 特定版本而不是最新的发布版,`package.json` 依赖不得不引用具体的版本范围,如下所示: + +```json +"dependencies": { + "iotivity-node": "1.2.0-3" +} +``` + +["使用目标标签"]: https://docs.npmjs.com/getting-started/using-tags diff --git a/locale/zh-cn/docs/guides/simple-profiling.md b/locale/zh-cn/docs/guides/simple-profiling.md new file mode 100644 index 0000000000000..046fc47588cf4 --- /dev/null +++ b/locale/zh-cn/docs/guides/simple-profiling.md @@ -0,0 +1,220 @@ +--- +title: 易于分析的 Node.js 应用程序 +layout: docs.hbs +--- + +# 易于分析的 Node.js 应用程序 + +市面上有很多可以容易地分析 Node.js 应用程序的工具。但是在许多情况下,最简单的选项是使用 Node.js 内置的探查器。创建的探测器使用 [V8 内探测器][],它收集该堆栈在程序执行期间的定期间隔的样本。并且它这些样本的结果,包含诸如 jit 编译的重要的优化事件,如以下刻度系列标示: + +``` +code-creation,LazyCompile,0,0x2d5000a337a0,396,"bp native array.js:1153:16",0x289f644df68,~ +code-creation,LazyCompile,0,0x2d5000a33940,716,"hasOwnProperty native v8natives.js:198:30",0x289f64438d0,~ +code-creation,LazyCompile,0,0x2d5000a33c20,284,"ToName native runtime.js:549:16",0x289f643bb28,~ +code-creation,Stub,2,0x2d5000a33d40,182,"DoubleToIStub" +code-creation,Stub,2,0x2d5000a33e00,507,"NumberToStringStub" +``` + +在以前你需要 V8 源代码去解释这些刻度。幸运的是,工具最近被引入到 Node.js 4.4.0,方便了这些信息的消费,而不另行建立 V8 源。让我们看看内置探查器如何帮助您洞察应用程序性能。 + +为了说明滴答探查器的使用,我们将使用一个简单的快速应用程序。我们的应用程序将有两个处理程序,一个用于向系统中添加新用户: + +```javascript +app.get('/newUser', (req, res) => { + let username = req.query.username || ''; + const password = req.query.password || ''; + + username = username.replace(/[!@#$%^&*]/g, ''); + + if (!username || !password || users.username) { + return res.sendStatus(400); + } + + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync(password, salt, 10000, 512); + + users[username] = { salt, hash }; + + res.sendStatus(200); +}); +``` + +另外一个用于验证用户尝试登陆: + +```javascript +app.get('/auth', (req, res) => { + let username = req.query.username || ''; + const password = req.query.password || ''; + + username = username.replace(/[!@#$%^&*]/g, ''); + + if (!username || !password || !users[username]) { + return res.sendStatus(400); + } + + const hash = crypto.pbkdf2Sync(password, users[username].salt, 10000, 512); + + if (users[username].hash.toString() === hash.toString()) { + res.sendStatus(200); + } else { + res.sendStatus(401); + } +}); +``` + +*请注意,这些不是推荐的处理程序用于对 Node.js 中的用户进行身份验证;它们纯粹用于说明目的。您不应该尝试设计您自己的加密身份验证机制。使用现有的、经过验证的身份验证解决方案要好得多。* + +现在假设我们已经部署了我们的应用程序,并且用户抱怨请求的延迟很大。我们可以轻松地运行应用程序与内置的探查器: + +``` +NODE_ENV=production node --prof app.js +``` + +然后使用 `ab` (ApacheBench) 在服务器上放些负载: + +``` +curl -X GET "http://localhost:8080/newUser?username=matt&password=password" +ab -k -c 20 -n 250 "http://localhost:8080/auth?username=matt&password=password" +``` + +然后你就可以得到 ab 的输出: + +``` +Concurrency Level: 20 +Time taken for tests: 46.932 seconds +Complete requests: 250 +Failed requests: 0 +Keep-Alive requests: 250 +Total transferred: 50250 bytes +HTML transferred: 500 bytes +Requests per second: 5.33 [#/sec] (mean) +Time per request: 3754.556 [ms] (mean) +Time per request: 187.728 [ms] (mean, across all concurrent requests) +Transfer rate: 1.05 [Kbytes/sec] received + +... + +Percentage of the requests served within a certain time (ms) + 50% 3755 + 66% 3804 + 75% 3818 + 80% 3825 + 90% 3845 + 95% 3858 + 98% 3874 + 99% 3875 + 100% 4225 (longest request) +``` + +从这个输出中我们可以看到我们只管理每秒约 5 个请求,并且平均请求只需要 4 秒的往返时间。在一个真实的世界例子中,我们可以代表用户请求在许多函数中做很多工作,但即使在简单的示例中,编译正则表达式、生成随机盐、从用户密码生成唯一哈希或在 Express 框架本身。 + +由于我们使用了 `--prof` 选项运行应用程序,因此在与应用程序的本地运行相同的目录中生成了一个刻度文件。它应该有形式 `isolate-0xnnnnnnnnnnnn-v8.log` (其中 `n` 为数字)。 + +为了使这个文件有意义,我们需要使用与 Node.js 捆绑在一起的刻度处理器。要运行处理器, 请使用 `--prof-process` 标志: + +``` +node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt +``` + +在您最喜欢的文本编辑器中打开 processed.txt 将给您提供一些不同类型的信息。该文件被分解成部分,然后再次被语言分解。首先, 我们看一下摘要部分: + +``` + [Summary]: + ticks total nonlib name + 79 0.2% 0.2% JavaScript + 36703 97.2% 99.2% C++ + 7 0.0% 0.0% GC + 767 2.0% Shared libraries + 215 0.6% Unaccounted +``` + +这告诉我们:收集到的所有样本中有 97% 是在 C++ 代码中进行的。当查看处理的输出的其它部分时,我们应该最注意 C++ 中所做的工作(而不是 Javascript)。考虑到这一点,我们接下来会找到 [C++] 部分, 其中包含有关 C++ 函数占用最多 CPU 时间的信息,然后查看一下: + +``` + [C++]: + ticks total nonlib name + 19557 51.8% 52.9% node::crypto::PBKDF2(v8::FunctionCallbackInfo const&) + 4510 11.9% 12.2% _sha1_block_data_order + 3165 8.4% 8.6% _malloc_zone_malloc +``` + +我们看到,前 3 个条目占了程序占用的 CPU 时间的 72.1%。从这个输出中,我们立即看到至少 51.8% 的 CPU 时间被称为 PBKDF2 的函数占用。它与用户密码中的哈希生成相对应。然而,较低的两个条目的因素是如何进入我们的应用程序(或者我们为了例子而假装如此)不会立即明显得看出来。为了更好地理解这些函数之间的关系,接下来我们将查看 [自下而上(重)配置文件] 部分,该节提供有关每个函数的主要调用方的信息。检查此部分,我们会发现: + +``` + ticks parent name + 19557 51.8% node::crypto::PBKDF2(v8::FunctionCallbackInfo const&) + 19557 100.0% v8::internal::Builtins::~Builtins() + 19557 100.0% LazyCompile: ~pbkdf2 crypto.js:557:16 + + 4510 11.9% _sha1_block_data_order + 4510 100.0% LazyCompile: *pbkdf2 crypto.js:557:16 + 4510 100.0% LazyCompile: *exports.pbkdf2Sync crypto.js:552:30 + + 3165 8.4% _malloc_zone_malloc + 3161 99.9% LazyCompile: *pbkdf2 crypto.js:557:16 + 3161 100.0% LazyCompile: *exports.pbkdf2Sync crypto.js:552:30 +``` + +分析此节需要的工作量比上面的原始刻度计数多一点。 +在上面的每个“调用栈”中,父列中的百分比将告诉您在当前行中函数调用了上面行中的函数所占的样本百分比。例如,在中间“呼叫堆栈”以上为 _sha1_block_data_order,我们看到 _sha1_block_data_order 发生在 11.9% 样品,我们知道从上面的原始计数。然而,在这里我们也可以说,它总是由 Node.js 内部的 pbkdf2 函数调用加密模块。我们看到,同样 _malloc_zone_malloc 被称为几乎完全相同的 pbkdf2 功能。因此,使用中的信息这种观点,我们可以说,我们从用户的密码帐户计算的哈希不仅为上面所述的 51.8%,但也是前 3 的 CPU 时间采样函数,因为调用 _sha1_block_data_order 和_malloc_zone_malloc 是代表 pbkdf2 的功能而制作的。 + +在这一点上,很明显,,基于密码的哈希生成应该是我们优化的目标。谢天谢地,您已经完全了解了 [异步编程的好处][],并且您认识到从用户密码生成哈希的工作正在以同步方式进行,从而绑定了事件循环。这将阻止我们在计算哈希时处理其它传入请求。 + +要解决此问题,请对上述处理程序进行小修改,以使用 pbkdf2 函数的异步版本: + +```javascript +app.get('/auth', (req, res) => { + let username = req.query.username || ''; + const password = req.query.password || ''; + + username = username.replace(/[!@#$%^&*]/g, ''); + + if (!username || !password || !users[username]) { + return res.sendStatus(400); + } + + crypto.pbkdf2(password, users[username].salt, 10000, 512, (err, hash) => { + if (users[username].hash.toString() === hash.toString()) { + res.sendStatus(200); + } else { + res.sendStatus(401); + } + }); +}); +``` + +一次新的基于 ab ,关于以上异步版本的应用基准测试情况如下: + +``` +Concurrency Level: 20 +Time taken for tests: 12.846 seconds +Complete requests: 250 +Failed requests: 0 +Keep-Alive requests: 250 +Total transferred: 50250 bytes +HTML transferred: 500 bytes +Requests per second: 19.46 [#/sec] (mean) +Time per request: 1027.689 [ms] (mean) +Time per request: 51.384 [ms] (mean, across all concurrent requests) +Transfer rate: 3.82 [Kbytes/sec] received + +... + +Percentage of the requests served within a certain time (ms) + 50% 1018 + 66% 1035 + 75% 1041 + 80% 1043 + 90% 1049 + 95% 1063 + 98% 1070 + 99% 1071 + 100% 1079 (longest request) +``` + +耶!您的应用程序现在每秒服务约 20 个请求,大约是同步哈希生成的4倍。此外,平均滞后时间从 4 秒前下降到仅 1 秒。 + +希望通过对此(诚然是做作的)示例的性能调查,您已经看到了 V8 刻度处理器如何帮助您更好地了解 Node.js 应用程序的性能。 + +[V8 内探测器]: https://developers.google.com/v8/profiler_example +[异步编程的好处]: https://nodesource.com/blog/why-asynchronous diff --git a/locale/zh-cn/docs/guides/timers-in-node.md b/locale/zh-cn/docs/guides/timers-in-node.md new file mode 100644 index 0000000000000..a7bb4dba43553 --- /dev/null +++ b/locale/zh-cn/docs/guides/timers-in-node.md @@ -0,0 +1,125 @@ +--- +title: Node.js 中的定时器 +layout: docs.hbs +--- + +# Node.js 中的定时器 + +Node.js 中的定时器模块包含了隔一定时间执行一遍代码的函数。定时器不必通过 `require()` 的方式引入,因为所有这些方法都是模拟浏览器中 JavaScript 函数,是全局的。为了更好的全面理解何时这些函数将执行,阅读 Node.js 中 [事件轮询](/en/docs/guides/event-loop-timers-and-nexttick/) 是一个好主意。 + +## 用 Node.js 控制时间的连续 + +Node.js 中的 API 函数提供了几种方式,允许代码从现在时间之后的某个时刻开始执行。以下给出的函数看上去很相似,因为它们在大多数浏览器中都可用。但是 Node.js 实际上提供了它自己的实现。定时器与系统非常紧密地结合在一起,尽管这些 API 是浏览器中函数的翻版,但是仍然在实现上有所不同。 + +### “当我这样说的时候” 的执行方式 ~ *`setTimeout()`* + +`setTimeout()` 可被用来在一段指定时间之后执行某个代码任务。此函数与浏览器 JavaScript 函数 [`window.setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout) 很相似,但是你不能把一串字符串传入执行。 + +`setTimeout()` 接受一个可执行的函数作为其第一个参数,然后有一个毫秒为单位的延时时间作为第二个参数。其余的参数也可纳入其中,作为传给此函数的参数使用。以下是一个例子: + +```js +function myFunc(arg) { + console.log(`arg was => ${arg}`); +} + +setTimeout(myFunc, 1500, 'funky'); +``` + +因为使用了 `setTimeout()`,以上函数 `myFunc()` 将在 1500 毫秒(或者1.5秒)左右时执行。 + +设置的定时间隔不能保证每次都是以 *精准* 的毫秒间隔数执行代码,这是因为其它阻塞或者正在事件轮询上处理的代码将推迟此定时的执行。*唯一* 保证的是定时器不会比声明的时间间隔 *提早* 执行。 + +`setTimeout()` 返回一个 `Timeout` 对象,可通过该对象引用到设置的定时器。这个返回的对象可以被用来取消定时(见下面的 `clearTimeout()` 部分),同时还可以改变执行行为(见下面的 `unref()` 部分)。 + +### "在此之后立即执行" *`setImmediate()`* + +`setImmediate()` 将在当前事件轮询的末尾处执行。 +本代码将在当前事件轮询中的任何 I/O 操作 *后*,在任何下一轮定时器 *前* 执行。代码执行可以被认为是“在此之后立即执行”,这意味着任何紧跟着 `setImmediate()` 函数调用将在 `setImmediate()` 函数参数前执行。 + +`setImmediate()` 的第一个参数是要执行的函数,当执行时,后面的参数将作为参数传入这个函数中。这是一个例子: + +```js +console.log('before immediate'); + +setImmediate((arg) => { + console.log(`executing immediate: ${arg}`); +}, 'so immediate'); + +console.log('after immediate'); +``` + +传入 `setImmediate()` 的上述函数将在任何可执行的代码执行完后执行,所以输出结果是: + +``` +before immediate +after immediate +executing immediate: so immediate +``` + +`setImmediate()` 返回一个 `Immediate` 对象,它可以被用于取消安排的定时任务(见下面的 `clearImmediate()` )。 + +注意:不要把 `setImmediate()` 和 `process.nextTick()` 相混淆。它们有一些主要的差别:第一, `process.nextTick()` 将在任何设置好的 `Immediate` 以及任何安排好的 I/O *前* 执行。第二, `process.nextTick()` 是不可擦除的,换句话说,一旦有代码使用 `process.nextTick()` 执行,执行无法中断,这就像一个普通函数一样,具体可以参考 [此教程](/en/docs/guides/event-loop-timers-and-nexttick/#process-nexttick) 来更好地理解 `process.nextTick()` 的操作。 + +### "永远的轮询" 执行 ~ *`setInterval()`* + +如果存在一块函数,它需要多次执行,`setInterval()` 可以派上用场。`setInterval()` 接受一个函数作为其参数,该函数将被运行无限次,第二个参数便是一个给定的延时毫秒数。就像 `setTimeout()`,其余参数可以在这之后添加,作为传递给函数调用的参数使用。另外一个和 `setTimeout()` 想象的地方是,延时不保证精确,因为有些操作可能在事件轮询上挂住,因此可以被认为是大致的延时。如以下例子: + +```js +function intervalFunc() { + console.log('Cant stop me now!'); +} + +setInterval(intervalFunc, 1500); +``` + +在上面的例子中,`intervalFunc()` 每 1500 毫秒执行一次(或每 1.5 秒执行一次),直到它被终止(见下)。 + +就像 `setTimeout()`,`setInterval()` 同样返回一个 `Timeout` 对象,它可以被引用并且改变设定的定时器。 + +## 清除定时器 + +如果 `Timeout` 或 `Immediate` 需要被取消,我们该怎么办?`setTimeout()`,`setImmediate()` 和 `setInterval()` 返回一个定时器对象,它可以被用来引用设置过的 `Timeout` 或 `Immediate` 对象。通过把该对象传入 `clear` 函数中,那个对象就会被终止执行。这些可接受的函数是 `clearTimeout()`,`clearImmediate()` 和 `clearInterval()`。看下面例子: + +```js +const timeoutObj = setTimeout(() => { + console.log('timeout beyond time'); +}, 1500); + +const immediateObj = setImmediate(() => { + console.log('immediately executing immediate'); +}); + +const intervalObj = setInterval(() => { + console.log('interviewing the interval'); +}, 500); + +clearTimeout(timeoutObj); +clearImmediate(immediateObj); +clearInterval(intervalObj); +``` + +## 让定时器在背后运行 + +你已经记住了 `Timeout` 对象是通过 `setTimeout` 和 `setInterval` 返回的。 `Timeout` 对象提供了两个针对 `Timeout` 行为的函数: `unref()` 和 `ref()`。如果有一个 `Timeout` 对象是通过 `set` 函数设定排程的,`unref()` 就可以用于那个对象。这会对它产生一些轻微的改变,那就是 *如果它是最后的执行代码* ,将不会调用 `Timeout` 对象。 + +相似的是,`Timeout` 对象有 `unref()` 方法可以被调用从而移除通过在相同的 `Timeout` 对象上调用了 `ref()` 之后所施加的行为,从而保证它的执行。小心一点,它并不会 *准确地* 恢复到初始化的行为状态。请见下面的例子: + +```js +const timerObj = setTimeout(() => { + console.log('will i run?'); +}); + +// if left alone, this statement will keep the above +// timeout from running, since the timeout will be the only +// thing keeping the program from exiting +timerObj.unref(); + +// we can bring it back to life by calling ref() inside +// an immediate +setImmediate(() => { + timerObj.ref(); +}); +``` +## 进一步向下的事件循环 + +还有远比本篇多得多的,关于时间轮询和定时器的东西,本文未涉及到。你可以通过 [Node.js 的时间轮询,定时器以及 process.nextTick()](/en/docs/guides/event-loop-timers-and-nexttick/) 学习到更多有关于 Node.js 内部事件轮询机制,以及定时器在执行时如何操作的。 diff --git a/locale/zh-cn/docs/guides/working-with-different-filesystems.md b/locale/zh-cn/docs/guides/working-with-different-filesystems.md new file mode 100644 index 0000000000000..f2582cd368a70 --- /dev/null +++ b/locale/zh-cn/docs/guides/working-with-different-filesystems.md @@ -0,0 +1,92 @@ +--- +title: 与不同的文件系统协同工作 +layout: docs.hbs +--- + +# 与不同的文件系统协同工作 + +Node 公开了许多文件系统的特性。但并不是所有的文件系统都相同。以下都是给你的最佳建议,它们能够让你的代码在不同的文件系统之间保持简单又安全。 + +## 文件系统的行为 + +在你开始与文件系统工作前,你需要知道它是如何工作的。不同的文件系统行为表现差异,而且相互之间有或多或少的特性:如大小写敏感、大小写不敏感、大小写保存、编码格式保存、时间戳解析、扩展特性、节点表、Unix 权限、备用数据流等。 + +你应该小心翼翼地通过 `process.platform` 推断文件系统类型。举一个例子,不要假设因为您的程序在 Darwin 系统上运行,因此您正在处理一个不区分大小写的文件系统(HFS+),因为用户可能使用一个区分大小写的文件系统(HFSX)。类似地,不要假设因为您的程序在 Linux 上运行,因此您正在处理支持 UNIX 权限和 INODE 的文件系统,因为您可能在特定的外部驱动器上,USB或网络驱动器不支持。 + +操作系统可能不容易推断文件系统行为,但都不会丢失。不用保存所有已知文件系统和行为的列表(这总是不完整的),您可以探测文件系统来查看它实际上是如何运行的。容易探测的某些特征的存在或不存在常常足以推断更难探测的其它特征的行为。 + +请记住,一些用户可能在工作树中的不同路径上安装了不同的文件系统。 + +## 避免最低公分母方法 + +您可能会试图使程序像最低公分母文件系统一样,通过将所有文件名规范为大写,将所有文件名规范为NFC Unicode 格式,并将所有文件时间戳归一化以表示 1 秒分辨率。这将是最低的公分母方法。 + +不要这样做。您只能安全地与文件系统进行交互,该文件系统在各个方面具有完全相同的最低公分母特性。您将无法以用户期望的方式使用更高级的文件系统,并且您会遇到文件名或时间戳冲突。您肯定会通过一系列复杂的依赖事件造成用户数据丢失和损坏。你会不断给你自己制造缺陷,而且解决起来很困难。 + +当您稍后需要支持只有 2 秒或 24 小时时间戳分辨率的文件系统时会发生什么?当 Unicode 标准前进到包含稍微不同的归一化算法(如以前发生)时会发生什么? + +最常用的分母方法倾向于通过只使用“便携”系统调用来创建一个可移植的程序。这导致了程序泄漏,而实际上不是便携式的。 + +## 采用超集方法 + +充分利用你支持的每一个平台,采用超集方法。例如,一个可移植的备份程序应该正确地在Windows系统之间同步文件或文件夹,即使在 Linux 系统上不支持 btime,也不应该破坏或更改它。相同的便携式备份程序应在 Linux 之间正确同步 UNIX 权限系统且不应破坏或更改 UNIX 权限,即便 UNIX 权限在 Windows 系统上不被支持。 + +通过使程序像更高级的文件系统一样处理不同的文件系统。支持所有可能功能的超集: 区分大小写、保存大小写、Unicode 敏感编码、Unicode 格式保存、Unix 权限、高分辨率纳秒时间戳、扩展属性等。 + +一旦你在程序中拥有了大小写保存设置后,如果需要与不区分大小写的文件系统进行交互,则始终可以实现不区分大小写。但是如果您放弃程序中的大小写保存,则无法与拥有大小写保存的文件系统安全地进行交互。对于 Unicode 编码保存和时间戳解析保存也是如此。 + +如果文件系统为您提供小写和大写混合的文件名,请将文件名保留在给定的大小写状态下。如果文件系统为您提供混合 Unicode 编码或 NFC 或 NFD (或 NFKC 或 NFKD),然后将文件名保留为给定的精确字节序列。如果文件系统为您提供毫秒时间戳,则将时间戳保留为毫秒分辨率。 + +当您使用较小的文件系统时,您总是可以适当地向下采样,并根据运行程序的文件系统的行为要求进行比较。如果您知道文件系统不支持 UNIX 权限,则不应期望读取与您编写的相同的 UNIX 权限。如果您知道文件系统不保留大小写,那么当你的程序创建 `ABC` 时,你应该准备在目录列表中看到 `ABC` 。但是,如果您知道文件系统确实保留了大小写区分,那么当检测到文件名重命名或文件系统区分大小写时,您应该考虑将 `ABC` 与 `abc` 作为不同的文件名进行区分。 + +## 大小写保留 + +您可以创建一个名为 `test/abc` 的目录,并惊讶地看到有时 `fs.readdir('test')` 返回 `['ABC']` 。这不是 Node 的 bug。Node 返回文件名, 因为文件系统存储它;而不是所有文件系统 +支持大小写保存。某些文件系统将所有文件名转换为大写(或小写)。 + +## Unicode 编码格式保存 + +*大小写保存和 Unicode 编码保存是相似的概念。要了解为什么要保留 Unicode 编码,请确保首先了解为什么应保留大小写。在正确理解时,Unicode 形式保存也同样简单。* + +Unicode 可以使用多个不同的字节序列对相同的字符进行编码。多个字符串可能看起来相同,但有不同的字节序列。使用 UTF-8 字符串时请注意,您的期望与 Unicode 的工作方式一致。正如您不希望所有 UTF-8 字符都编码到单个字节一样, 您不应该期望用人的肉眼看上去是是一样的几个 UTF-8 字符串一定具有相同的字节表示形式。这可能只是一个期望,你可以有 ASCII,但不是 UTF-8。 + +您可以创建一个名为 `test/café` 的目录(带有字节序列的 `<63 61 66 c3 a9>` 和 `string.length === 5` 的 NFC Unicode 编码)。然后你会惊讶地看到,有时 `fs.readdir('test')` 会返回 `['café']`(带有字节序列的 `<63 61 66 65 cc 81>` 和 `string.length === 6` 的 NFD Unicode 编码)。这不是 Node 的缺陷。Node 返回文件名因为文件系统存储它,而不是所有文件系统支持 Unicode 形式保存。 + +例如 HFS+ 将所有文件名正常化为一种格式,几乎总是与 NFD 格式相同。不要期望 HFS+ 与 NTFS 或 EXT4 的行为相同,反之亦然。不要尝试通过规范化来永久更改数据。这会对对文件系统间的 Unicode 差异进行了漏抽象总结。反而造成问题而不是解决它们。相反地,我们应该保留 Unicode 格式并仅使用正常化作为比较函数。 + +## 敏感的 Unicode 编码 + +Unicode 不敏感编码和 Unicode 编码保存是两种不同的文件系统行为,经常相互误解。正如在存储和传输文件名时将文件名永久规范化为大写时,有时会不正确地实现了不区分大小写;因此 Unicode 不敏感编码有时会被永久地错误地实现。在存储和传输文件名时,将文件名规范化为特定的 Unicode 形式(NFD 的情况下为 HFS+)。在不牺牲 Unicode 编码保存的情况下实现 Unicode 不敏感编码是可能的,而且最好使用 Unicode 规范化进行比较。 + +## 比较不同的 Unicode 编码 + +节点提供了 `string.normalize('NFC' / 'NFD')`,你可以使用它正常化一个 UTF-8 字符串, 无论是 NFC 或 NFD。您不应将输出从该函数中存储,而只将其用作比较函数的一部分以测试两个 UTF-8 字符串是否与用户看起来相同。 + +你可以使用 `string1.normalize('NFC') === string2.normalize('NFC')` 或者 `string1.normalize('NFD') === string2.normalize('NFD')` 作为你的比较函数,至于使用哪种编码格式并不重要。 + +规范化速度很快,但您可能希望使用缓存作为比较函数的输入以避免多次对同一字符串进行规范化。如果该字符串不存在于缓存中,则将其规范化并缓存它。注意不要存储或保留缓存,仅将其用作缓存。 + +请注意,使用 `normalize()` 要求您的 Node 版本包括 ICU(否则 `normalize()` 函数将只返回原始字符串)。如果您从网站下载最新版本的 Node,那么它将包括 ICU。 + + +## 时间戳解析 + +您可以将文件的 `mtime`(修改后的时间)设置为 `1444291759414`(毫秒分辨率),有时会惊讶地看到 `fs.stat` 返回新的 mtime 为 `1444291759000`(1 秒分辨率)或 `1444291758000`(2 秒分辨率)。这不是 Node 的 bug。Node 在文件系统存储时返回时间戳,而不是所有文件系统都支持纳秒、毫秒或 1 秒的时间戳解析。某些文件系统甚至对 atime 有非常粗略的分辨率。例如,某些 FAT 文件系统的 24 小时。 + +## 不要通过规范化来损坏文件名和时间戳 + +文件名和时间戳是用户数据。正如您永远不会自动将用户文件数据重写为大写的数据,或把 `CRLF` 转化为 `LF` 进行规范化一样,您永远不应更改、干扰或损坏文件名或时间戳。方式是大小写 / Unicode 格式 / 时戳规范化。规范化只应用于比较,而不应用于更改数据。 + +规范化实际上是一个有损的哈希代码。您可以使用它来测试某些类型的等价性(例如,即使它们有不同的字节序列,也可以使用多个字符串看起来相同)。但您永远不能将其用作实际数据的替代。您的程序应根据需要传递文件名和时间戳数据。 + +您的程序可以在 NFC(或它喜欢的任何 Unicode 形式组合)中创建新数据,或使用小写或大写的文件名,或者使用 2 秒的分辨率时间戳。但程序不应通过强制大小写 / Unicode 编码 / 时间戳规范化来损坏现有用户数据。相反,在程序中采用超集方法并保留大小写、Unicode 窗体和时间戳解析。这样, 您就能够安全地与执行同样操作的文件系统进行交互。 + +## 适当地使用规范化比较函数 + +请确保适当地使用大小写敏感 / Unicode 编码 / 时间戳比较函数。如果正在处理区分大小写的文件系统,请不要使用不区分大小写的文件名比较函数。如果正在处理 Unicode 编码敏感的文件系统(例如 NTFS 和大多数 Linux 文件系统,同时保留 NFC 和 NFD 或混合 Unicode 编码),则不要使用 Unicode 表单不敏感的比较函数。如果您正在使用纳秒时间戳解析文件系统,则不要将时间戳与 2 秒分辨率进行比较。 + +## 为比较函数的细微差别做好准备 + +请注意,您的比较函数与文件系统的功能匹配(或者,如果可能的话,可以探测文件系统,以查看它如何实际进行比较的)。例如,不区分大小写比简单的 `toLowerCase()` 复杂。事实上 `toUpperCase()` 通常比 `toLowerCase()` 好(因为它处理某些外语字符的方式不同)。但是, 最好还是探测文件系统,因为每个文件系统都有自己的编码比较表。 + +例如,苹果的 HFS+ 将文件名规范化为 NFD 形式,但此 NFD 表单实际上是当前 NFD 表单的较旧版本,有时可能与最新的 Unicode 标准的 NFD 形式稍有不同。不要期望 HFS+ NFD 和 Unicode NFD 总是完全相同的。 diff --git a/locale/zh-cn/docs/index.md b/locale/zh-cn/docs/index.md new file mode 100644 index 0000000000000..e70d39593e229 --- /dev/null +++ b/locale/zh-cn/docs/index.md @@ -0,0 +1,44 @@ +--- +title: Docs +layout: docs.hbs +labels: + lts: LTS +--- + +# 关于文档说明 + +本网站上有以下几类文档类型: + +* API 函数接口文档 +* ES6 特性 +* 常见问题 +* 指南 + +### API函数接口文档 + +[API 函数接口文档](/api/) 提供了在 Node.js 中的函数或者对象的详细信息。这个文档告诉你什么方法该用什么参数,方法返回值是什么以及使用那个方法会有什么错误异常。它同时也告知你不同版本中的 Node.js 有哪些方法是可用的。 + +此文档描述了 Node.js 内建的模块,社区提供的模块并不记载在内。 + +
+

在寻找先前发布的 API 函数接口文档吗?

+ + +
+ +### ES6 特性 + +[ES6 专区](/en/docs/es6/) 描述了三个 ES6 特性组介绍,同时详细介绍了哪些特性在 Node.js 中是默认的(附上了解释性连接)。同时也告知如何寻找某个特定 Node.js 发布版本是使用哪个版本的 V8 引擎。 + +### 指南 + +关于 Node.js 技术特性和能力的长篇具有深度的文章。 diff --git a/locale/zh-cn/docs/meta/topics/dependencies.md b/locale/zh-cn/docs/meta/topics/dependencies.md new file mode 100644 index 0000000000000..43da4d8fca12e --- /dev/null +++ b/locale/zh-cn/docs/meta/topics/dependencies.md @@ -0,0 +1,78 @@ +--- +title: 所有依赖项 +layout: docs.hbs +--- + +# 所有依赖项 + +Node.js 依赖于以下一些依赖项,这样它才能正常工作。 + +- [Libraries](#Libraries) + - [V8](#V8) + - [libuv](#libuv) + - [http-parser](#http-parser) + - [c-ares](#c-ares) + - [OpenSSL](#OpenSSL) + - [zlib](#zlib) +- [Tools](#Tools) + - [npm](#npm) + - [gyp](#gyp) + - [gtest](#gtest) + +## 类库 + +### V8 + +V8 类库为 Node.js 提供了 JavaScript引擎,Node.js 通过 V8 C++ 的 API 函数接口进行操控, V8 由谷歌公司维护,用于谷歌浏览器中。 + +- [相关文档](https://v8docs.nodesource.com/) + +### libuv + +另外一个重要的依赖项是 libuv,它是一个 C 写成的类库,用于非阻塞型的 I/O 操作,同时在所有支持的操作系统上保持一致的接口。它提供了一些机制来处理诸如文件系统、DNS、网络、子进程、管道、信号量控制、轮询机制和数据流。它同样也提供了一个线程池,用于无法在操作系统层面进行异步操作的任务卸载。 + +- [相关文档](http://docs.libuv.org/) + +### http-parser + +HTTP 解析是通过一个由 C 语言编写、轻量级称作 http-parser 的类库进行的。由于它的设计不会引发系统调用和系统资源分配,因而它的预请求内存痕迹极小。 + +- [相关文档](https://github.com/joyent/http-parser/) + +### c-ares + +对于某些异步的 DNS 请求,Node.js 使用由 C 编写,称作 c-areas 的类库。它是通过 JavaScript 的 DNS 模块,以 resolve() 家族函数的形式发布。lookup() 函数,核心剩余部分使用它,并利用了在 libuv 中可跨越getaddrinfo(3) 的调用。那是因为 c-areas 支持 /etc/hosts,/etc/resolv.conf 以及 /etc/svc.conf。但不是像 mDNS 一样的东西。 + +- [相关文档](http://c-ares.haxx.se/docs.html) + +### OpenSSL + +OpenSSL 广泛地在 `tls` 和 `crypto` 模块中使用。它提供了战争环境下,许多现代网络为安全而依赖的密码函数。 + +- [相关文档](https://www.openssl.org/docs/) + +### zlib + +为了快速压缩解压,Node.js 依赖与工业标准的 zlib 类库。同名可知的还有 gzip 和 libpng。Node.js 使用 zlib 创建同步、异步和数据流压缩、解压缩接口。 + +- [相关文档](http://www.zlib.net/manual.html) + +## 工具 + +### npm + +Node.js 完全是基于模块化构建的,因此需要一个高质量的包管理器;有鉴于此 npm 产生了。随着 npm 的产生史上最大的社区创建的编程生态圈诞生,它们使得构建 Node.js 快而容易。 + +- [相关文档](https://docs.npmjs.com/) + +### gyp + +构建系统通过 gyp 来处理。这是一个从 V8 拷贝而来、基于 python 的项目生成工具。它可以生成项目文件用以跨不同平台中使用。Node.js 需要一个构建系统,因为它自身的大部分,以及它的依赖项,是用需要编译的语言写成的。 + +- [相关文档](https://gyp.gsrc.io/docs/UserDocumentation.md) + +### gtest + +本地代码可以通过 gtest 进行测试,它是从 Chromium 收录的。它不需要一个真实可启动的节点环境下,直接测试 C/C++ 程序。 + +- [相关文档](https://code.google.com/p/googletest/wiki/V1_7_Documentation) From 63cde47c8d74d15963b6a0f609df548d7742a3f4 Mon Sep 17 00:00:00 2001 From: MaleDong Date: Thu, 19 Jul 2018 11:13:39 +0800 Subject: [PATCH 2/4] Modified Traslations 1 --- locale/zh-cn/docs/es6.md | 2 +- .../guides/anatomy-of-an-http-transaction.md | 3 +- .../docs/guides/backpressuring-in-streams.md | 100 +++++++++--------- .../docs/guides/blocking-vs-non-blocking.md | 2 +- locale/zh-cn/docs/meta/topics/dependencies.md | 2 +- 5 files changed, 54 insertions(+), 55 deletions(-) diff --git a/locale/zh-cn/docs/es6.md b/locale/zh-cn/docs/es6.md index 9757703823f4c..2c5e663871c00 100644 --- a/locale/zh-cn/docs/es6.md +++ b/locale/zh-cn/docs/es6.md @@ -32,7 +32,7 @@ V8 团队现正在努力持续工作,他们借助 EcmaScript 5 或是更早时 特性优化的工作也随着 ES2015 到来,以上计划是通过 [执行计划](https://docs.google.com/document/d/1EA9EbfnydAmmU_lM8R_uEMQ-U_v4l9zulePSBkeYWmY),有一个 V8 团队在那儿收集整理相关信息,并协调需要提高性能、设计追踪问题的文稿等部门。 -## 我有我自己的基本框架,可以平衡 --harmony,所以我可以移除这个标记吗? +## 我有我自己的基本框架,可以利用 --harmony,所以我可以移除这个标记吗? 目前来说,`--harmony` 在 Node.js 的作用是让 **staged** 特性起作用。它本质上等同于 `--es_staging`。如上所述,有些特性尚未完全确认是稳定的,所以如果你希望一个安全的环境(尤其是在发布环境),你应该考虑移除这个运行时的环境标记,直到它在 V8 中以默认形式发布,或者在 Node.js 中落地。如果你开启了这个开关,你应该有对未来 Node.js 升级而造成代码破坏(无法正常工作)的准备,比如 V8 引擎做了更改,它的语法变得更接近标准。 diff --git a/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md b/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md index ade359fc9f9ea..c316cec219a08 100644 --- a/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md +++ b/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md @@ -28,8 +28,7 @@ server.on('request', (request, response) => { }); ``` -当一个 HTTP 到达服务端,node 调用 request 处理程序,并产生一些唾手可得的对象用以处理传输, -这些对象就是 `request` 和 `response`。我们马上会讲到。 +当一个 HTTP 到达服务端,node 调用 request 处理程序,并产生一些唾手可得的对象用以处理传输,这些对象就是 `request` 和 `response`。我们马上会讲到。 实际上,为了处理请求,[`listen`][] 方法需要在 `server` 对象上被显式调用。在大多数情况下,你只要把端口号作为参数传入 `listen` 方法中,作为监听端口即可。当然也有一些其它选项,具体可以参考 [API 参考文档][]。 diff --git a/locale/zh-cn/docs/guides/backpressuring-in-streams.md b/locale/zh-cn/docs/guides/backpressuring-in-streams.md index 664c7425dfa7a..e1eae153f4e4f 100644 --- a/locale/zh-cn/docs/guides/backpressuring-in-streams.md +++ b/locale/zh-cn/docs/guides/backpressuring-in-streams.md @@ -3,19 +3,19 @@ title: 数据流中的积压问题 layout: docs.hbs --- -# 数据流中的背压问题 +# 数据流中的积压问题 -通常在数据处理的时候我们会遇到一个普遍的问题:[`背压`][],意思是在数据传输过程中有一大堆数据在缓存之后积压着。每次当数据到达结尾又遇到复杂的运算,又或者无论什么原因它比预期的慢,这样累积下来,从源头来的数据就会变得很庞大,像一个塞子一样堵塞住。 +通常在数据处理的时候我们会遇到一个普遍的问题:[`backpressure`][],意思是在数据传输过程中有一大堆数据在缓存之后积压着。每次当数据到达结尾又遇到复杂的运算,又或者无论什么原因它比预期的慢,这样累积下来,从源头来的数据就会变得很庞大,像一个塞子一样堵塞住。 为解决这个问题,必须存在一种适当的代理机制,确保流从一个源流入另外一个的时候是平滑顺畅的。不同的社区组织针对他们各自的问题单独做了解决,好例子比如 Unix 的管道和 TCP 的 Socket。此问题经常与 _流控制_ 相关。在 Node.js 中,流已经是被采纳的解决方案。 -此文的目的在于详细深入介绍什么是背压,并从代码角度详细解释在 Node.js 中,流是如何针对此问题进行处理的。本文的第二部分将对你在实现流的功能时给予最佳实战经验,以确保你的程序既安全又精简。 +此文的目的在于详细深入介绍什么是积压,并从代码角度详细解释在 Node.js 中,流是如何针对此问题进行处理的。本文的第二部分将给予你实现流的功能时最佳实践,以确保你的程序既安全又精简。 -我们假定你对 Node.js 中的 [`背压`][],[`缓存`][],[`事件发射器`][] 和 [`流`][] 的基本概念有一点了解。如果你尚未完整阅读过 API 文档,那么最好是先看一下相关 API 说明,它也会有助于你扩展理解本文的主旨。 +我们假定你对 Node.js 中的 [`backpressure`][],[`Buffer`][],[`EventEmitter`][] 和 [`Stream`][] 的基本概念有一点了解。如果你尚未完整阅读过 API 文档,那么最好是先看一下相关 API 说明,它也会有助于你扩展理解本文的主旨。 ## 处理数据中遇到的问题 -在一个计算机系统中,通过管道,socket 和 信号量将数据从一个进程传到另外一个进程中。在 Node.js 中,我们发明了一个类似的机制,它称为 [`流`][]。流太棒了!它们为 Node.js 做了太多的事情,而且内部代码库的每个角落都用到了那个模块。作为一个开发者,你也应该鼓励自己多去使用这个模块! +在一个计算机系统中,通过管道,socket 和 信号量将数据从一个进程传到另外一个进程中。在 Node.js 中,我们发明了一个类似的机制,它称为 [`Stream`][]。流太棒了!它们为 Node.js 做了太多的事情,而且内部代码库的每个角落都用到了那个模块。作为一个开发者,你也应该鼓励自己多去使用这个模块! ```javascript const readline = require('readline'); @@ -33,7 +33,7 @@ rl.question('Why should you use streams? ', (answer) => { }); ``` -通过流实现背压机制的一个很好的例子是通过比较内部系统工具可以证明一个很大的优化。它通过 Node.js 的[`流`][]实现。 +通过流实现积压机制的一个很好的例子是通过比较内部系统工具可以证明一个很大的优化。它通过 Node.js 的 [`Stream`][]实现。 在以下场景中,我们将拿一个巨大的文件(大概有 9gb 那么大),然后用熟悉的 [`zip(1)`][] 的工具压缩。 @@ -53,16 +53,16 @@ const out = fs.createWriteStream('The.Matrix.1080p.mkv.gz'); inp.pipe(gzip).pipe(out); ``` -现在尝试打开每个压缩的文件来测试结果。由 [`zip(1)`][] 压缩的文件会提醒你文件中断了,但通过 [`流`][] 的压缩在解压时无任何错误。 +现在尝试打开每个压缩的文件来测试结果。由 [`zip(1)`][] 压缩的文件会提醒你文件中断了,但通过 [`Stream`][] 的压缩在解压时无任何错误。 请注意:这个例子中我们使用 `.pipe()` 从一个数据源终端到另外一个终端,不过没有使用任何出错处理机制。如果一大堆数据出错了但是又要被接收, `可读` 和 `gzip` 流不会被销毁。 [`pump`][] 是一个工具类,如果有某个流发生错误或者关闭,它会自动销毁相关所有的流,在这个情况下是必须使用的! ## 数据太多,速度太快 -有太多的例子证明有时 [`可读`][] 传输给 [`可写`][] 的速度远大于它接受和处理的速度! +有太多的例子证明有时 [`Readable`][] 传输给 [`Writable`][] 的速度远大于它接受和处理的速度! 如果发生了这种情况,消费者开始为后面的消费而将数据列队形式积压起来。写入队列的时间越来越长,也正因为如此,更多的数据不得不保存在内存中知道整个流程全部处理完毕。 -写入磁盘的速度远比从磁盘读取数据慢得多,因此,当我们试图压缩一个文件并写入磁盘时,背压的问题也就出现了。因为写磁盘的速度不能跟上读磁盘的速度。 +写入磁盘的速度远比从磁盘读取数据慢得多,因此,当我们试图压缩一个文件并写入磁盘时,积压的问题也就出现了。因为写磁盘的速度不能跟上读磁盘的速度。 ```javascript // Secretly the stream is saying: "whoa, whoa! hang on, this is way too much!" @@ -70,7 +70,7 @@ inp.pipe(gzip).pipe(out); // `write` tries to keep up with the incoming data flow. inp.pipe(gzip).pipe(outputFile); ``` -这就是为什么说背压机制很重要——如果背压机制不存在,进程将用完你全部的系统内存,从而对其它进程产生显著影响,它独占系统大量资源直到任务完成为止。 +这就是为什么说积压机制很重要——如果积压机制不存在,进程将用完你全部的系统内存,从而对其它进程产生显著影响,它独占系统大量资源直到任务完成为止。 这最终导致一些问题: @@ -78,7 +78,7 @@ inp.pipe(gzip).pipe(outputFile); * 太多繁重的垃圾回收 * 内存耗尽 -以下例子中我们把 `.write()` 函数的 [返回值][] 值取出,改成 `true`,这样明显地禁止了 Node.js 核心的背压的支持。在任何引用了 'modified' 二进制库的地方,我们探讨在不适用 `return ret;` 的情况下运行 `node` 二进制代码,并用 `return true;` 取代它。 +以下例子中我们把 `.write()` 函数的 [返回值][] 值取出,改成 `true`,这样明显地禁止了 Node.js 核心的积压的支持。在任何引用了 'modified' 二进制库的地方,我们探讨在不适用 `return ret;` 的情况下运行 `node` 二进制代码,并用 `return true;` 取代它。 ## 过度的垃圾收集 @@ -122,11 +122,11 @@ approx. time (ms) | GC (ms) | modified GC (ms) 54000 | 6 | 35 ``` -当两个进程同时运行,并似乎以同样的效率开始工作,在若干秒随着适当的背压开始变得有效率起来。它将 GC 负载扩展到每隔一定的 4-8 毫秒的间隔,直到数据传输结束。 +当两个进程同时运行,并似乎以同样的效率开始工作,在若干秒随着适当的积压开始变得有效率起来。它将 GC 负载扩展到每隔一定的 4-8 毫秒的间隔,直到数据传输结束。 -但是,当背压机制处理不恰当,V8 垃圾回收机制开始变慢。一般情况下 GC 一分钟内进行 75 次回收,但是修改过的二进制库仅 36 次。 +但是,当积压机制处理不恰当,V8 垃圾回收机制开始变慢。一般情况下 GC 一分钟内进行 75 次回收,但是修改过的二进制库仅 36 次。 -随着内存占用越来越多,缓慢和渐进的欠债也不断积累。随着数据的传输,在没有背压系统的情况下,每个块传输都使用更多的内存。 +随着内存占用越来越多,缓慢和渐进的欠债也不断积累。随着数据的传输,在没有积压系统的情况下,每个块传输都使用更多的内存。 内存分配使用越多,GC 就越要照顾内存交换。内存交换得越多,GC 就需要考虑决定哪些内存可以被释放,并且要一直在大块内存中扫描独立区块,而这又要消耗更多的计算功率。 @@ -188,37 +188,37 @@ sys 7.43 虚拟内存占用的最大的字节块达到了 1.52 gb。 -没有合适的流来处理背压,就会产生一个内存占用的震级顺序——与同样的进程处理有着天壤之别! +没有合适的流来处理积压,就会产生一个内存占用的震级顺序——与同样的进程处理有着天壤之别! 这个实验展示了如何精简以对你的计算系统进行精简,以及有效的资源消耗。现在,我们故意弄出一个故障看看它又是怎么工作的。 -## 背压是怎么处理这些问题的? +## 积压是怎么处理这些问题的? 我们有不同的函数将数据从一个进程传入另外一个进程。在 Node.js 中,有一个内置函数称为 [`.pipe()`][],同样地,你们也可以使用 [其它工具包][]。最终,在这个进程的基本层面上我们有二个互不相关的组件:数据的 _源头_,和 _消费者_。 当 [`.pipe()`][] 被源调用之后,它通知消费者有数据需要传输。管道函数为事件触发建立了合适的积压封装。 -在 Node.js 中,源头是一个 [`可读`][] 的流,消费者是 [`可写`][] 流(它们两者可能通过 [`Duplex`][] 或 [`Transform`][] 进行交互)。只不过这超出我们本文讨论范围了。 +在 Node.js 中,源头是一个 [`Readable`][] 的流,消费者是 [`Writable`][] 流(它们两者可能通过 [`Duplex`][] 或 [`Transform`][] 进行交互)。只不过这超出我们本文讨论范围了。 -当背压被触发的一刹那,它可以被缩略成 [`可写`][] 的 [`.write()`][] 方法。返回函数值当然是根据一些条件所决定的。 +当积压被触发的一刹那,它可以被缩略成 [`Writable`][] 的 [`.write()`][] 方法。返回函数值当然是根据一些条件所决定的。 -在数据缓存超出了 [`水准值`][] 或者写入的列队处于繁忙状态,[`.write()`][] 会返回 `false`。 +在数据缓存超出了 [`highWaterMark`][] 或者写入的列队处于繁忙状态,[`.write()`][] 会返回 `false`。 -当 `false` 返回之后,背压系统介入了。它将暂停从任何发送数据的数据流中进入的 [`可读`][]。一旦数据流清空了, [`.drain()`][] 事件将被触发,消耗进来的数据流。 +当 `false` 返回之后,积压系统介入了。它将暂停从任何发送数据的数据流中进入的 [`Readable`][]。一旦数据流清空了, [`.drain()`][] 事件将被触发,消耗进来的数据流。 -一旦队列全部处理完毕,背压机制将允许允许数据再次发送。在使用中的内存空间将自我释放,同时准备接收下一次的批量数据。 +一旦队列全部处理完毕,积压机制将允许允许数据再次发送。在使用中的内存空间将自我释放,同时准备接收下一次的批量数据。 这个有效的举措允许一大堆锁住的内存可以为 [`.pipe()`][] 函数随时使用而并没有内存泄露、无限扩大的内存缓冲。同时垃圾回收器仅需要处理一处地方。 -所以,背压既然如此重要,为什么还有理由说你没有听说过呢?显然答案很明显:Node.js 为你做了一切。 +所以,积压既然如此重要,为什么还有理由说你没有听说过呢?显然答案很明显:Node.js 为你做了一切。 -这太好了!不过当我们试图去理解如何实现我们自己的背压流,这却并不太好。 +这太好了!不过当我们试图去理解如何实现我们自己的积压流,这却并不太好。 -注意:对于大部分机器,存在着一个字节的大小用以决定一个缓存是否已经满了(不同机器此值有变化)。Node.js 将允许你设置你自己的 [`水准值`][]。但是通常来说,默认是设置为 16kb(16384,对于对象模型流而言是 16)。在某些实例中你或许想提高那个值,尽管去提高吧,但是也要小心使用! +注意:对于大部分机器,存在着一个字节的大小用以决定一个缓存是否已经满了(不同机器此值有变化)。Node.js 将允许你设置你自己的 [`highWaterMark`][]。但是通常来说,默认是设置为 16kb(16384,对于对象模型流而言是 16)。在某些实例中你或许想提高那个值,尽管去提高吧,但是也要小心使用! ## `.pipe()` 的生命周期 -为了对背压有一个更好的理解,这里有一副 [`可读`][] 流正通过 [管道][] 流入 [`可写`][] 流的整个生命周期图: +为了对积压有一个更好的理解,这里有一副 [`Readable`][] 流正通过 [piped][] 流入 [`Writable`][] 流的整个生命周期图: ```javascript @@ -267,23 +267,23 @@ sys 7.43 注意:如果你创建一些管道准备把一些流串联起来从而操纵数据,你应该实现 [`Transform`][] 流。 -在这种情况下,从 [`可读`][] 流中的输出进入 [`Transform`][],并且会被管道输送进入 [`可写`][]。 +在这种情况下,从 [`Readable`][] 流中的输出进入 [`Transform`][],并且会被管道输送进入 [`Writable`][]。 ```javascript Readable.pipe(Transformable).pipe(Writable); ``` -背压将被自动应用,但是同时请注意输入和输出 [`Transform`][] 的 `水准值` 可以手动控制,并且会影响到背压系统。 +积压将被自动应用,但是同时请注意输入和输出 [`Transform`][] 的 `水准值` 可以手动控制,并且会影响到积压系统。 -## 背压行为的准则 +## 积压行为的准则 -从 [Node.js v0.10][] 开始,[`流`][] 类借助带有下划线一些相关函数([`._read()`][] 和 [`._write()`][]),提供了访问 [`.read()`][] 或[`.write()`][] 的能力。 +从 [Node.js v0.10][] 开始,[`Stream`][] 类借助带有下划线一些相关函数([`._read()`][] 和 [`._write()`][]),提供了访问 [`.read()`][] 或[`.write()`][] 的能力。 -这里有一些准则文档可供参考: [实现可读的流][] 和 [实现可写的流][]。我们假设你可以把这些文章已经读过了,下个章节将做稍许的深入讲解。 +这里有一些准则文档可供参考:[实现可读的流][] 和 [实现可写的流][]。我们假设你可以把这些文章已经读过了,下个章节将做稍许的深入讲解。 ## 实现用户自定义流须知 -流的黄金法则是 __总是接受背压__。作为最佳实践的构成是不矛盾的实践。只要你小心避免与内部背压支持冲突的行为,你可以确信你在遵循良好的实践。 +流的黄金法则是 __总是接受积压__。作为最佳实践的构成是不矛盾的实践。只要你小心避免与内部积压支持冲突的行为,你可以确信你在遵循良好的实践。 一般而说。 @@ -291,13 +291,13 @@ Readable.pipe(Transformable).pipe(Writable); 2. 在流返回 `false` 后不要调用 `.write()` 方法,而是等待 'drain'。 3. 流在不同的 Node.js 版本和库中是有变化的。小心你的测试。 -注意:关于第三点,构建浏览器流的一个难以置信的方法是使用 [`可读流`][]。Rodd Vagg 曾经写过一篇 [大作][] 详细描述这个工具库。简而言之,它为 [`可读`][] 流提供了自动可销毁降解的类型,并且支持旧版的 Node.js 和 浏览器。 +注意:关于第三点,构建浏览器流的一个难以置信的方法是使用 [`readable-stream`][]。Rodd Vagg 曾经写过一篇 [大作][] 详细描述这个工具库。简而言之,它为 [`Readable`][] 流提供了自动可销毁降解的类型,并且支持旧版的 Node.js 和 浏览器。 ## 对于可读流的规则 -迄今为止,我们已经看了 [`.write()`][] 方法对于背压的影响,并且过多关注在 [`可写`][] 流上,因为 Node.js 的功能,数据从 [`可读`][] 流到 [`可写`][] 流。但是正如我们在数据流传输过程中我们观察到,源和 [`可读`][] 目标一样重要, [`可读`][] 流对于背压是如何处理的至关重要。 +迄今为止,我们已经看了 [`.write()`][] 方法对于积压的影响,并且过多关注在 [`Writable`][] 流上,因为 Node.js 的功能,数据从 [`Readable`][] 流到 [`Writable`][] 流。但是正如我们在数据流传输过程中我们观察到,源和 [`Readable`][] 目标一样重要, [`Readable`][] 流对于积压是如何处理的至关重要。 -这两个过程相互依赖地进行有效沟通,如果 [`可读`][] 流在 [`可写`][] 流需要它停止发送数据的时候忽略了,那么当 [`.write()`][] 方法返回时,会产生问题。 +这两个过程相互依赖地进行有效沟通,如果 [`Readable`][] 流在 [`Writable`][] 流需要它停止发送数据的时候忽略了,那么当 [`.write()`][] 方法返回时,会产生问题。 所以,除了谨慎对待 [`.write()`][] 方法,我们同样要小心在 [`._read()`][] 方法中使用 [`.push()`][] 方法的返回值。如果 [`.push()`][] 方法返回一个 `false`,流就会停止从源读数据。否则,它就不会停止而继续读下去。 @@ -316,7 +316,7 @@ class MyReadable extends Readable { } ``` -另外,从定制流之外,忽略背压简直可笑至极。在以下反例中,代码仅关注数据是否到达(通过 [`.data` 事件 ][] 订阅): +另外,从定制流之外,忽略积压简直可笑至极。在以下反例中,代码仅关注数据是否到达(通过 [`.data` event][] 订阅): ```javascript // This ignores the backpressure mechanisms Node.js has set in place, @@ -329,12 +329,12 @@ readable.on('data', (data) => ## 关于可写流的规则 -重新调用 [`.write()`][] 方法根据一些条件可能返回 true 或者 false。幸运地是,当我们构建属于自己的 [`可写`][] 流的时候, [`流状态机`][] 会处理我们的回调,并且决定什么时候处理背压并且为我们简化数据流。 +重新调用 [`.write()`][] 方法根据一些条件可能返回 true 或者 false。幸运地是,当我们构建属于自己的 [`Writable`][] 流的时候, [`流状态机`][] 会处理我们的回调,并且决定什么时候处理积压并且为我们简化数据流。 -但是当我们需要直接使用 [`可写`][] 流时,我们必须考虑 [`.write()`][] 方法返回的值,并且注意到以下一些情况: +但是当我们需要直接使用 [`Writable`][] 流时,我们必须考虑 [`.write()`][] 方法返回的值,并且注意到以下一些情况: * 如果写队列确实繁忙,[`.write()`][] 方法将返回 false。 -* 如果数据块太大, [`.write()`][] 方法将返回 false(限定通过 [`水准值`][] 决定)。 +* 如果数据块太大, [`.write()`][] 方法将返回 false(限定通过 [`highWaterMark`][] 决定)。 ```javascript @@ -398,21 +398,21 @@ function doUncork(stream) { 流经常作为一个模块用于 Node.js 中,对于内部的系统结构而言非常重要。对于开发者而言,可以通过 Node.js 扩展连接应答系统。 -现在我们希望你有能力进行故障排除,记住了是如何为你的 [`可写`][] 和 [`可读`][] 流编写背压处理的。并且你还可以把这些知识分享给你的同事和朋友们。 +现在我们希望你有能力进行故障排除,记住了是如何为你的 [`Writable`][] 和 [`Readable`][] 流编写积压处理的。并且你还可以把这些知识分享给你的同事和朋友们。 -在此之后请仔细阅读更多的有关 [`流`][] 其它 API 函数,这样有助于当你在构建 Node.js 的应用程序之时更好地理解关于流的能力。 +在此之后请仔细阅读更多的有关 [`Stream`][] 其它 API 函数,这样有助于当你在构建 Node.js 的应用程序之时更好地理解关于流的能力。 -[`流`]: https://nodejs.org/api/stream.html -[`缓存`]: https://nodejs.org/api/buffer.html -[`事件发射器`]: https://nodejs.org/api/events.html -[`可写`]: https://nodejs.org/api/stream.html#stream_writable_streams -[`可读`]: https://nodejs.org/api/stream.html#stream_readable_streams +[`Stream`]: https://nodejs.org/api/stream.html +[`Buffer`]: https://nodejs.org/api/buffer.html +[`EventEmitter`]: https://nodejs.org/api/events.html +[`Writable`]: https://nodejs.org/api/stream.html#stream_writable_streams +[`Readable`]: https://nodejs.org/api/stream.html#stream_readable_streams [`Duplex`]: https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams [`Transform`]: https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams [`zlib`]: https://nodejs.org/api/zlib.html [`.drain()`]: https://nodejs.org/api/stream.html#stream_event_drain -[`.data` 事件 ]: https://nodejs.org/api/stream.html#stream_event_data +[`.data` event]: https://nodejs.org/api/stream.html#stream_event_data [`.read()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_read_size [`.write()`]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback [`._read()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_read_size_1 @@ -427,12 +427,12 @@ function doUncork(stream) { [实现可读的流]: https://nodejs.org/docs/latest/api/stream.html#stream_implementing_a_readable_stream [其它工具包]: https://github.com/sindresorhus/awesome-nodejs#streams -[`背压`]: https://en.wikipedia.org/wiki/Back_pressure#Backpressure_in_information_technology +[`backpressure`]: https://en.wikipedia.org/wiki/Back_pressure#Backpressure_in_information_technology [Node.js v0.10]: https://nodejs.org/docs/v0.10.0/ -[`水准值`]: https://nodejs.org/api/stream.html#stream_buffering +[`highWaterMark`]: https://nodejs.org/api/stream.html#stream_buffering [返回值]: https://github.com/nodejs/node/blob/55c42bc6e5602e5a47fb774009cfe9289cb88e71/lib/_stream_writable.js#L239 -[`可读流`]: https://github.com/nodejs/可读流 +[`readable-stream`]: https://github.com/nodejs/readable-stream [大作]:https://r.va.gg/2014/06/why-i-dont-use-nodes-core-stream-module.html [`dtrace`]: http://dtrace.org/blogs/about/ @@ -441,5 +441,5 @@ function doUncork(stream) { [`流状态机`]: https://en.wikipedia.org/wiki/Finite-state_machine [`.pipe()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options -[管道]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options +[piped]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options [`pump`]: https://github.com/mafintosh/pump diff --git a/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md b/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md index b85587e97de3c..ac476f4048108 100644 --- a/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md +++ b/locale/zh-cn/docs/guides/blocking-vs-non-blocking.md @@ -11,7 +11,7 @@ layout: docs.hbs ## 阻塞 -**阻塞** 是说 Node.js 中其它的 JavaScript 命令必须等到一个非 JavaScript 操作完成之后才可以执行。这是因为当 **阻塞** 发生时,事件机制无法继续运行JavaSCript。 +**阻塞** 是说 Node.js 中其它的 JavaScript 命令必须等到一个非 JavaScript 操作完成之后才可以执行。这是因为当 **阻塞** 发生时,事件机制无法继续运行JavaScript。 在 Node.js 中,JavaScript由于 CPU 密集操作而表现不佳。而不是等待非 JavaScript操作 (例如I/O)。这被称为 **阻塞**。在 Node.js 基本类库中,使用 libuv 的同步方法大多数都是 **阻塞** 的。原生方法也可能是 **阻塞** 的。 diff --git a/locale/zh-cn/docs/meta/topics/dependencies.md b/locale/zh-cn/docs/meta/topics/dependencies.md index 43da4d8fca12e..e271ba56eaed7 100644 --- a/locale/zh-cn/docs/meta/topics/dependencies.md +++ b/locale/zh-cn/docs/meta/topics/dependencies.md @@ -53,7 +53,7 @@ OpenSSL 广泛地在 `tls` 和 `crypto` 模块中使用。它提供了战争环 ### zlib -为了快速压缩解压,Node.js 依赖与工业标准的 zlib 类库。同名可知的还有 gzip 和 libpng。Node.js 使用 zlib 创建同步、异步和数据流压缩、解压缩接口。 +为了快速压缩解压,Node.js 依赖于工业标准的 zlib 类库。同名可知的还有 gzip 和 libpng。Node.js 使用 zlib 创建同步、异步和数据流压缩、解压缩接口。 - [相关文档](http://www.zlib.net/manual.html) From 98b5048a4fbb504a69b3a607df7315d0e2165ef3 Mon Sep 17 00:00:00 2001 From: MaleDong Date: Thu, 2 Aug 2018 17:09:10 +0800 Subject: [PATCH 3/4] [Fix] Update translation according to willin's suggestion --- locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md b/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md index c316cec219a08..167c7744eda83 100644 --- a/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md +++ b/locale/zh-cn/docs/guides/anatomy-of-an-http-transaction.md @@ -77,10 +77,8 @@ request.on('data', (chunk) => { }); ``` -> **注意:** 或许你感到这样做有些枯燥乏味,不过大多数情况下你算是幸运的。 -因为 [`npm`][] 上实在有太多的诸如 [`concat-stream`][] 和 [`body`][] 一类类库屏蔽了部分 -细节逻辑而替你做了这些事情。当然,对于你而言在使用这些类库前知道它们到底干了什么非常重要,这 -就是你为什么需要读这篇文章! +> **注意:** 这看起来有些单调乏味,大多数情况下也确实是这样。 +不过庆幸的是因为 [`npm`][] 上实在有太多的诸如 [`concat-stream`][] 和 [`body`][] 一类类库屏蔽了部分细节逻辑而替你做了这些事情。当然,对于你而言在使用这些类库前知道它们到底干了什么非常重要,这就是你为什么需要读这篇文章! ## 一笔带过关于错误的一些信息 From 18927fb0e8a2ef19743506a568fda5b8346e83a7 Mon Sep 17 00:00:00 2001 From: MaleDong Date: Sun, 5 Aug 2018 14:37:23 +0800 Subject: [PATCH 4/4] Update for `pipeline` Ref: https://github.com/nodejs/nodejs.org/pull/1758 --- .../docs/guides/backpressuring-in-streams.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/locale/zh-cn/docs/guides/backpressuring-in-streams.md b/locale/zh-cn/docs/guides/backpressuring-in-streams.md index e1eae153f4e4f..e3363b659ce12 100644 --- a/locale/zh-cn/docs/guides/backpressuring-in-streams.md +++ b/locale/zh-cn/docs/guides/backpressuring-in-streams.md @@ -57,6 +57,55 @@ inp.pipe(gzip).pipe(out); 请注意:这个例子中我们使用 `.pipe()` 从一个数据源终端到另外一个终端,不过没有使用任何出错处理机制。如果一大堆数据出错了但是又要被接收, `可读` 和 `gzip` 流不会被销毁。 [`pump`][] 是一个工具类,如果有某个流发生错误或者关闭,它会自动销毁相关所有的流,在这个情况下是必须使用的! +[`pump`][] 对于 Nodejs 8.x 以及先前版本是必须的。但对于 10.x 和之后的版本而言,我们引入了 [`pipeline`][] 来取而代之它。这是一个模块化函数,用于对接不同的数据流,可以处理异常错误并善后清理释放资源。它同时也提供了一个回调函数——当整个 pipeline 任务完成时将触发。 + +这里给出一个例子,告诉你如何使用 pipeline: + +```javascript +const { pipeline } = require('stream'); +const fs = require('fs'); +const zlib = require('zlib'); + +// Use the pipeline API to easily pipe a series of streams +// together and get notified when the pipeline is fully done. +// A pipeline to gzip a potentially huge video file efficiently: + +pipeline( + fs.createReadStream('The.Matrix.1080p.mkv'), + zlib.createGzip(), + fs.createWriteStream('The.Matrix.1080p.mkv.gz'), + (err) => { + if (err) { + console.error('Pipeline failed', err); + } else { + console.log('Pipeline succeeded'); + } + } +); +``` +你也可以使用 [`promisify`][] 包装 pipeline,配合 `async` / `await` 进行使用: + +```javascript +const stream = require('stream'); +const fs = require('fs'); +const zlib = require('zlib'); + +const pipeline = util.promisify(stream.pipeline); + +async function run() { + try { + await pipeline( + fs.createReadStream('The.Matrix.1080p.mkv'), + zlib.createGzip(), + fs.createWriteStream('The.Matrix.1080p.mkv.gz'), + ); + console.log('Pipeline succeeded'); + } catch (err) { + console.error('Pipeline failed', err); + } +} +``` + ## 数据太多,速度太快 有太多的例子证明有时 [`Readable`][] 传输给 [`Writable`][] 的速度远大于它接受和处理的速度! @@ -443,3 +492,5 @@ function doUncork(stream) { [`.pipe()`]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options [piped]: https://nodejs.org/docs/latest/api/stream.html#stream_readable_pipe_destination_options [`pump`]: https://github.com/mafintosh/pump +[`pipeline`]: https://nodejs.org/api/stream.html#stream_stream_pipeline_streams_callback +[`promisify`]: https://nodejs.org/api/util.html#util_util_promisify_original