Node.js 已经迎来了服务器端 JavaScript 的时代,这是客户端 JavaScript 在过去几年经历的复兴的下一个合乎逻辑的步骤。虽然 Node.js 不是第一个服务器端的 JavaScript 实现,但它肯定已经成为最受欢迎的实现。通过利用 JavaScript 作为一种语言的最佳特性并培养一个充满活力的社区,Node.js 已经成为一个非常受欢迎的平台和框架,没有放缓的迹象。在http://nodejs.org/可以找到关于什么是节点的详细描述:
Node.js 是一个基于 Chrome 的 JavaScript 运行时构建的平台,用于轻松构建快速、可扩展的网络应用。Node.js 使用事件驱动、非阻塞的 I/O 模型,使其轻量级且高效,非常适合跨分布式设备运行的数据密集型实时应用。
该项目始于 2009 年瑞安·达尔的大脑之子。那一年在 JSConf.eu(欧洲每年举行的一次会议)上,他做了演讲,改变了 JavaScript 开发的面貌。他的演讲包括一个令人印象深刻的完整的 IRC 服务器演示,它是用大约 400 行 JavaScript 编写的。在他的演讲中,他概述了他为什么开始这个项目,为什么 JavaScript 成为这个项目不可或缺的一部分,以及他在服务器编程领域一直寻求实现的目标——特别是关于我们如何处理输入和输出(I/O)。
那年晚些时候,NPM 项目开始了,目标是管理 Node.js 应用的包,以及创建一个公共可用的注册表,以便在 Node.js 开发人员之间共享代码。从 Node.js 的 0.6.3 版本开始,npm 与 Node.js 一起部署和安装,使其成为事实上的包管理器。
**# node . js 有何不同?
Node.js 与其他平台的不同之处在于它是如何处理 I/O 的,它结合异步 I/O 使用了一个事件循环,这使得它能够以较小的占用空间实现高水平的并发。
通常,当程序需要某种外部输入时,它会以同步的方式进行。任何程序员都应该非常熟悉下面的代码行:
var results = db.query("SELECT * FROM users");
print(results[0].username);
我们在这里所做的就是在一个 SQL 数据库中查询所有用户的列表,然后打印出第一个用户的名字。像这样查询数据库时,需要采取许多中间步骤,例如:
- 打开与数据库服务器的连接。
- 通过网络将请求传输到该服务器。
- 服务器本身需要在收到请求后对其进行处理。
- 服务器必须通过网络将响应传输回我们的应用。
这份清单并没有涵盖所有的细节,因为有太多的因素超出了需要提出的观点。通过查看我们的源代码,这被视为一个瞬时动作,但是我们知道得更清楚。我们经常忽略这种浪费的时间,因为它太快了,以至于我们没有注意到它的发生。请考虑下表:
|输入输出成本
| | --- | | L1 高速缓存 | 3 个周期 | | L2 高速缓存 | 14 个周期 | | 随机存取存储 | 250 次循环 | | 唱片 | 41,000,000 次循环 | | 网络 | 240,000,000 次循环 |
每个输入输出操作都有一个成本,这个成本在使用同步输入输出的程序中直接支付。在程序继续运行之前,很容易出现数百万个时钟周期。
编写应用服务器时,像这样的程序一次只能服务一个用户,在对上一个用户的所有 I/O 和处理完成之前,无法服务下一个用户。这当然是不可接受的,所以最简单的解决方案是为每个传入的请求创建一个新的线程,这样它们就可以并行运行。
Apache web 服务器就是这样工作的,实现起来并不难。但是,随着同时用户数量的增加,使用的内存量也会增加。这些线程中的每一个都需要操作系统级别的开销,而且增加得非常快。此外,这些线程之间的上下文切换开销比预期的更耗时,进一步加剧了问题。
网络服务器在其核心使用一个事件循环来处理流程。通过这样做,它能够用更少的资源同时处理更多的用户。事件循环要求将处理的各个部分分解成小块,并在一个队列中运行。这消除了创建线程、在这些线程之间来回切换的高成本,并且对整个系统的需求更少。同时,它填补了处理空白,尤其是在等待输入/输出完成期间出现的空白。
Node.js 采用了 nginx 使用的事件驱动模型,取得了巨大的成功,它为许多类型的应用提供了相同的功能。在 Node.js 中,所有的 I/O 都是完全异步的,不会阻塞应用线程的其余部分。Node.js API 接受所有输入输出操作的函数参数(通常称为“回调函数”)。然后,Node.js 触发输入/输出操作,并让应用外部的另一个线程进行处理。之后,应用可以继续处理其他请求。一旦请求的操作完成,就会通知事件循环,并调用回调函数来获得结果。
事实证明,就原始处理时间而言,等待输入/输出完成是许多应用中最昂贵的部分。使用 Node.js,等待输入/输出所花费的时间与应用的其余处理时间是完全分离的。您的应用只是使用回调函数将结果作为简单事件进行处理,而 JavaScript 使用闭包的能力保留了函数的上下文,尽管是异步执行的。
如果你要承担编写多线程应用的任务,你必须关心并发问题,比如死锁,这在现实应用中很难重现和调试。使用 Node.js,您的主要应用逻辑运行在一个线程上,没有这种并发问题,而耗时的 I/O 由 Node.js 代表您处理。
像任何其他平台一样,Node.js 有一个开发人员可以用来编写应用的 API。JavaScript 本身缺乏一个标准库,尤其是用于执行 I/O 的库,这实际上是 Ryan Dahl 选择 JavaScript 的原因之一。由于核心 API 可以从头开始构建,不需要担心与标准库产生冲突,以防做错(考虑到 JavaScript 的历史,这不是一个不合理的假设)。
这个核心库是极简的,但它确实包含了基本模块。这包括但不限于:文件系统访问、网络通信、事件、二进制数据结构和流。这些 API 中的许多虽然不难使用,但在实现上非常低级。直接从 Node.js 网站(添加了注释)考虑这个“Hello World”演示:
// one of the core modules
var http = require('http');
// creates an http server, this function is called for each request
http.createServer(function (req, res) {
// these parameters represent the request and response objects
// the response is going to use a HTTP status code 200 (OK)
// the content-type HTTP header is set as well
res.writeHead(200, {'Content-Type': 'text/plain'});
// lastly, the response is concluded with simple text
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
这个服务器使用 http 核心模块来设置一个 web 服务器,只需向任何提出请求的人发送“Hello World”。这是一个简单的例子,但是没有注释,总共只有六行代码。
Node.js 团队选择将核心库的范围限制在有限的范围内,让开发人员社区来创建他们需要的模块,例如数据库驱动程序、单元测试、模板和核心 API 的抽象。为了帮助这个过程,Node.js 有一个名为 npm 的包管理器。
npm 是为 Node.js 应用处理安装依赖关系的工具。它选择本地捆绑的依赖项,而不是使用单一的全局命名空间。这允许不同的项目有自己的依赖关系,即使这些项目之间的版本不同。
下载示例代码
您可以从您在http://www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/supportand注册,将文件直接通过电子邮件发送给您。
除了允许使用第三方模块之外,npm 还将向注册管理机构投稿作为一项公共事务。向注册表中添加一个模块就像一个命令一样简单,使得进入的障碍极低。如今,npm 注册中心已经列出了超过 42,000 个软件包,并且每天都在以更快的速度增长。
随着注册中心的快速发展,很明显它背后有一个充满活力的生态系统。我个人可以证明这样一个事实,Node.js 开发人员社区非常友好,非常多产,并且有巨大的热情。
当涉及到保护您的应用时,有许多因素需要考虑。我们将从检查 JavaScript 本身开始,然后分析作为平台的 Node.js,并揭示一些与讨论相关的内部内容。之后,我们将从整体上研究您的应用的注意事项和模式。最后,我们将在应用的请求和响应级别调查漏洞。到本书结束时,您应该对 Node.js 的内部有了足够的了解,不仅可以解决我们在这里讨论的问题,还可以掌握应用未来可能出现的任何漏洞。
在这一章中,我们探索了 Node.js 项目本身的历史,并给出了一些开发环境和社区的背景。在下一章中,我们将从 JavaScript 语言本身的安全特性开始。**