第一章 Node 简介
1.1 Node 的诞生历程
- 2009 年 3 月,Ryan Dahl 在其博客宣布准备发布基于 V8 创建一个轻量级的 web 服务器并提供一套库。
- 2009 年 5 月,Ryan Dahl 在 github 上发布了最初版本。
- 2009 年 12 月和 2010 年 4 月,两届 JSConf 大会安排了 Node 的讲座。
- 2010 年底,Node 获得硅谷云计算服务商的投资。
- 2011 年 7 月,Node 在微软的支持下发布了其 Windows 版本。
- 2011 年 11 月,Node 超越 Ruby on Rails,称为 github 上关注度最高的项目。
- ...
1.2 Node 的命名与起源
在 Node 官方网站之外,有很多别称:Nodejs、NodeJS、Node.js 等。本书遵循官方说法,将一直使用 Node 这个名字。
1.2.1 为什么是 JavaScript
Ryan Dahl 是一名资深 C/C++程序员程序员,在创造 Node 之前,他的主要工作都是围绕高性能 web 服务器进行的。经历过一些尝试和失败后,他找到了设计高性能 web 服务器的几个要点:事件驱动、非阻塞 I/O。
所以 Ryan Dahl 最初要写一个基于事件驱动、非阻塞 I/O 的 web 服务器,以达到最高的性能,提供 Apache 等服务器之外的选择。大多数人不愿意设计一种更简单和更有效的程序,原因是他们用了阻塞 I/O 的库。
相比之下 JavaScript 比 C 的开发门槛低,比 Lua 的历史包袱低要少。尽管服务端 JavaScript 存在已经很多年了,但是后端部分一直没有市场,可以说历史包袱为零,为其导入非阻塞 I/O 库没有额外阻力。另外 JavaScript 在浏览器中广泛的事件驱动方面的应用,暗合 Ryan Dahl 喜好基于事件驱动的需求。当时,第二次浏览器大战也渐渐分高下,Chrome 浏览器的 JavaScript 引擎 V8 摘得性能第一桂冠,而且其基于新的 BSD 许可证发布,自然受到 Ryan Dahl 的欢迎。考虑到高性能、符合事件驱动、没有历史包袱 3 个主要原因,JavaScript 自然成了 Node 的实现语言。
1.2.2 为什么叫 Node
起初,Ryan Dahl 称他的项目为 web.js,就是一个 Web 服务器,但是项目的发展超过了他最初单纯开发一个 web 服务器的想法,变成了构建网络应用的一个基础框架,这样可以在它的基础上构建更多的东西,诸如服务器、客户端、命令行工具等。Node 发展成为一个强制不共享任何资源的单线程、单进程系统,包含十分适宜网络的库,为构建大型分布式应用程序提供基础设施,其目标也是称为一个构建快速、可伸缩的网络应用平台。它自身非常简单,通过通信协议来组织许多 Node,非常容易通过扩展来达成构建大型网络应用的目的。每一个 Node 进程都构成这个网络应用中的一个节点,这是它名字的真谛。
1.3 Node 给 JavaScript 带来的意义
V8 给 Chrome 浏览器带来了一个强劲的心脏,使得它在浏览器大战中脱颖而出,也使得 Ryan 在语言评估中为选择 Node 增加了一个极大的权重值。
我们知道浏览器除了 V8 作为 JavaScript 引擎之外,还有一个 WebKit 布局引擎。HTML5 在发展过程中定义了更多丰富 API,在实现上,浏览器提供了越来越多的功能暴露给 JavaScript 和 HTML 标签。这个愿景美好,但是对于前端浏览器的发展现状而言,HTML5 标准统一的过程是相对缓慢的。JavaScript 作为一门图灵完备的语言,长久以来却限制在浏览器沙箱中运行,它的能力取决于浏览器中间层提供的支持有多少。
除了 HTML、WebKit 和显卡这些 UI 相关技术没有支持外,Node 的结构与 Chrome 十分相似。他们都是基于事件驱动的异步架构,浏览器通过事件驱动来服务界面上的 UI 交互,Node 通过事件驱动来服务 I/O。在 Node 中,JavaScript 可以随心所欲地访问本地文件,可以搭建 websocket 服务端,可以连接数据库,可以如 Web Workers 一样玩转多进程。如今,JavaScript 可以运行在不同地地方,不在继续限制在浏览器中与 CSS 样式表、DOM 树打交道。如果 HTTP 协议栈是水平面,Node 就是浏览器在协议栈另一边地倒影。Node 不处理 UI,但用与浏览器相同地机制和原理运行。Node 打破了过去 JavaScript 只能运行在浏览器端的局面。前后端编程环境统一,可以大大降低前后端转换所需要的上下文交换代价。
目前社区已经出现了 node-webkit 这样的项目。在 node-webkit 项目中,它将 Node 事件循环和 WebKit 事件循环融合在一起,既可以通过它享受 HTML、CSS 带来的 UI 构建,也能通过它访问本地资源,讲两者的优势整合在一起。桌面应用程序开发可以完全通过 HTML、CSS、JavaScript 完成。
1.4 Node 的特点
作为后端 JavaScript 运行平台,Node 保留了前端浏览器 JavaScript 的那些熟悉的接口,没有改写语言本身的任何特性,依旧基于作用域和原型链,区别在于它将前端中广泛运用的思想迁移到服务器。
1.4.1 异步 I/O
关于异步 I/O,想前端工程师解释起来可能更容易些,因为发起 Ajax 调用对于前端工程师而言再熟悉不过的场景。下面代码是发起 Ajax 请求的:
$.post('/url', { title: '深入浅出Node.js' }, function (data) {
console.log('收到响应');
});
console.log('发送Ajax请求结束');
熟悉异步的用户必然知道,“收到响应”是在“发起 Ajax 请求”之后输出的。在调用$.post()
后,后续代码会立即执行,而“收到响应”的执行时间不被预期。我们只知道它将在这个异步请求结束后执行,但并不知道具体的时间点。异步调用中对于结果值的捕获是符合“Don't Call me, I will call you”的原则,这也是注重结果,不关心过程的一种表现。
在 Node 中,异步 I/O 也是常见的。读取文件为例,我们可以看到它与前端 Ajax 调用的方式是极其类似的:
var fs = require('fs');
fs.readFile('/path', function (err, file) {
console.log('读取文件完成');
});
console.log('发起读取文件');
这里的“发起读取文件”是在“读取文件完成”之前输出的。同样,“读取文件完成”的执行也取决于读取文件的异步调用何时结束。
在 Node 中,绝大多数操作都是异步的方式进行调用。Ryan 排除万难,在底层构建了很多异步 I/O 的 API,从文件读取到网络请求等。这样的意义在于,在 Node 中,我们可以从语言层面很自然的进行并行 I/O 操作。每个调用之间无需等待之前的 I/O 调用结束。在编程模型上,可以极大提升效率。
下面的两个文件读取任务的耗时取决于最慢那个文件读取的耗时:
fs.readFile('/path1', function (err, file) {
console.log('读取文件1完成');
});
fs.readFile('/path2', function (err, file) {
console.log('读取文件2完成');
});
对于同步 I/O 而言,它们的耗时是两个任务的耗时之和。这里异步带来的优势是显而易见的。
1.4.2 事件与回调函数
随着 web 2.0 时代的到来,JavaScript 在前端担任了更多的职责,事件也得到了广泛的应用。Node 不想 Rhino 那样受 Java 的影响很大,而是将前端浏览器中应用广泛且成熟的事件引入后端,配合异步 I/O,将事件点暴露给业务逻辑。
下面例子展示的是 Ajax 异步提交到服务端处理的过程。Node 创建一个 web 服务器,并侦听 8080 端口。对于服务器,我们为其绑定了request
事件,对于请求对象,我们为其绑定了data
事件和end
事件。
var http = require('http');
var querystring = requre('querystring');
// 侦听服务器request事件
http
.createServer(function (req, res) {
var postData = '';
req.setEncoding('utf8');
// 侦听data事件
req.on('request', function (trunk) {
postData += trunk;
});
req.on('end', function () {
res.end(postData);
});
})
.listen(9090);
console.log('服务器启动完成');
响应的我们在前端为 Ajax 请求绑定success
事件,在发出请求后,只需要关心请求成功时执行响应的业务逻辑即可:
$.ajax({
url: '/url',
method: 'POST',
data: {},
success: function (data) {
// success 事件
},
});
相比之下无论是前端还是后端,事件都是常用的。对于其它语言来说,这种俯拾皆是 JavaScript 的熟悉感觉是基本不会出现的。
事件编程的方式具有轻量级、松耦合、只关注事务点灯优势,但是在多个异步任务的场景下,事件与事件之间各自独立,如何协作是一个问题。
从前面可以看出,回调函数无处不在。这时因为在 JavaScript 中,我们将函数作为第一等公民来对待,可以将函数作为对象传递给方法作为实参调用。
与其它 web 后端编程语言相比,Node 除了异步和事件外,回调函数也是一大特色。纵观下来,回调函数也是最好的接受异步调用返回数据的凡是。但是这种编程方式对于很多习惯同步思路编程的人来说,也许是十分不习惯的。代码的编写顺序与执行顺序并无关系,这对他们可能造成阅读上的障碍。在流程控制方面,因为穿插了异步方法和回调函数,与常规的同步方式相比,变得不那么一目了然。
在转变为异步编程思维后,通过对业务的划分和对事件的提炼,在流程控制方面处理业务的复杂度与同步方式实际上是一致的。
1.4.3 单线程
Node 保持了 JavaScript 在浏览器中单线程的特点。而且在 Node 中,JavaScript 与其余线程是无法共享状态的。单线程的最大好处就是不用像多线程编程那样处处在意状态和同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。
同样单线程也有它自身的弱点,这些弱点是学习 Node 的过程中必须要面对的。积极面对这些弱电,可以享受到 Node 带来的好处,也能避免潜在的问题,使其得以高效利用。单线程的弱电具体有以下三个方面:
- 无法利用多核 CPU
- 错误会引起整个应用退出,应用的健壮性值得考验
- 大量计算占用 CPU 导致无法继续调用异步 I/O
像浏览器中 JavaScript 与 UI 共用一个线程一样,JavaScript 长时间执行会导致 UI 的渲染和响应被中断。在 Node 中,长时间的 CPU 占用也会导致后续的异步 I/O 发不出调用,已完成的异步 I/O 的回调函数也会得不到及时执行。
最早解决这个大计算量问题的方案是 Google 公司开发的 Gears。它启用一个完全独立的进程,将需要计算的程序发送给这个进程,在得出结果后,通过事件将结果传递回来。这个模型将计算量分发到其它进程上,以此来降低运算造成阻塞的几率。后来 HTML5 定制了 Web Workers 的标准,Google 放弃了 Gears,权力支持 Web Workers。Web Workers 能够创建工作线程来进行计算,来解决 JavaScript 大计算阻塞 UI 渲染的问题。工作线程为了不阻塞主线程,通过消息传递的方式来传递运行结果,这也使得工作线程不能访问主线程中的 UI。
Node 采用了与 Web Workers 相同的思路来解决单线程中大计算量的问题:child_process
。
子进程的出现,意味着 Node 可以从容地应对单线程在健壮性和无法利用多核 CPU 方面地问题。通过将计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间地事件消息来传递结果,这可以很好地保持应用模型地简单和低依赖。通过 Master-Worker 地管理方式,也可以很好地管理各个工作进程,以达到更高地健壮性。
1.4.4 跨平台
起初,Node 只可以在 Linux 平台上运行。如果想在 Windows 平台学习和使用 Node,则必须通过 Cygwin 或者 MinGW。随着 Node 地发展,微软注意到了它的存在,并投入了一个团队帮助 Node 实现 Windows 平台地兼容,在 v0.6.0 发布时,Node 已经能够直接在 Windows 平台运行了。
兼容 Window 和*nix 平台主要得益于 Node 在架构层面地改动,它在操作系统与 Node 上层模块之间构建了一个平台层架构,即 libuv。目前 libuv 已经成为许多系统实现跨平台地基础组件。
良好地架构,Node 地第三方 C++模块也可以借助 libuv 实现跨平台。目前,除了没有保持更新地 C++模块外,大部分 C++模块都能实现跨平台地兼容。
1.5 Node 的应用场景
在进行技术选型之前,需要了解一项新技术具体适合什么样的场景,毕竟合适的技术用在合适的场景可以起到意想不到的效果。关于 Node,探讨得较多得主要有 I/O 密集型和 CPU 密集型。
1.5.1 I/O 密集型
在 Node 得推广过程中,无数次有人问其 Node 的应用场景是什么。如果将所有的脚本语言拿到一处来评判,那么从单线程的角度来说,Node 处理 I/O 的能力是值得竖起大拇指称赞的。通常,说 Node 擅长 I/O 密集型的应用场景基本没人反对。Node 面向网络且擅长并行 I/O,能够有效地阻止起更多地硬件资源,从而提供更多更好地服务。
I/O 密集型地优势在于 Node 利用事件循环地处理能力,而不是启动每一个线程为每一个请求服务,资源总用极少。
1.5.2 是否不擅长 CPU 密集型业务
换一个角度,在 CPU 密集地应用场景中,Node 是否能够胜任呢?实际上,V8 地执行效率是十分高地。单以执行效率来做评判,V8 地执行效率是毋庸置疑的。
Node 在性能上不俗,从另一个角度来说,CPU 密集型应用其实并不可怕。CPU 密集型应用给 Node 带来的挑战主要是:由于 JavaScript 单线程的原因,如果有长时间运行的计算(如大循环),将会导致 CPU 时间片不能释放,使得后续 I/O 无法发起。但是适当调整和分解大型运算任务为多个小人物,使得运算能够适时释放,非阻塞 I/O 调用的发起,这样既可以同时享受到并行异步 I/O 的好处,又能充分利用 CPU。
关于 CPU 密集型应用,Node 的异步 I/O 已经解决了在单线程上 CPU 与 I/O 之间阻塞无法重叠利用的关系,I/O 阻塞造成的性能浪费远比 CPU 的影响小。对于长时间运行的计算,如果他的耗时超过普通阻塞 I/O 的耗时,那么应用场景需要重新评估,因为这类计算比阻塞 I/O 还影响效率,甚至说就是一个纯计算的场景,根本没有 I/O。此类应用场景或许应当采用多线程的方式进行计算。Node 虽然没有提供多线程用于计算支持,但是还是从以下两个方式来充分利用 CPU。
- Node 可以通过编写 C/C++扩展的方式更高效地利用 CPU,将一些 V8 不能左到性能极致地地方通过 C/C++来实现。C/C++扩展地方式实现,比 Java 速度还快。
- 如果单线程地 Node 不能满足需求,甚至用了 C/C++地扩展还觉得不够,那么通过子进程的方式,将一部分 Node 进程当作常驻服务进程用于计算,然后利用进程间的消息来传递结果,将计算与 I/O 分离,这样还能充分利用 CPU.
1.5.3 与遗留系统和平共处
有人会说:“JavaScript 一统前后端了,将来会不会干掉其它语言?”言语中充满了危机感。
在 Web 端,过去大多数都是同步的方式编写程序,这种串行调用下层应用数据的过程中充斥着穿行的等待时间,如果采用多线程来解决这种串行等待,又或多或少地显得小题大做。在 Node 中,语言层面即可天然并行的特性在这种场景中显得十分有效。对于已有的稳定系统,并非意味着我们要抛弃。
1.5.4 分布式应用
阿里巴巴的数据平台对 Node 的分布式应用算是一个典型的例子。分布式应用意味着对可伸缩性要求非常高。数据平台通常要在一个数据库集群中去寻找需要的数据。阿里巴巴开发了中间层 NodeFox、ITier,将数据库集群做了划分和映射,查询调用依旧是针对单张表进行 SQL 查询,中间件分解查询 SQL,并行地去多台数据库中获取数据并合并。NodeFox 能够实现对多台 MySQL 数据库地查询,如果查询一台 MySQL 一样,而 ITier 更强大,查询多个数据库如果查询单个数据库一样,这里的多个数据库是指不同的数据库,如 MySQL 或其它的数据路。
这个案例其实也是高效利用并行 I/O 的例子。Node 高效利用并行 I/O 的过程,也是高效使用数据库的过程。对于 Node,这个行为只是依次普通的 I/O。对于数据库而言,却是一次复杂的计算,所以也是进而充分利用压榨硬件资源的过程。
1.6 Node 的使用者
- 前后端编程语言环境统一。雅虎开放了 Cocktail 框架,利用自己深厚的前端沉淀,将 YUI3 这个前端框架的能力借助 Node 延伸到服务器端,使得使用者拜托了日常工作中一边写 JavaScript 一边写 PHP 所带来的上下文交换负担。
- Node 带来的高性能 I/O 用于实时应用。Voxer 将 Node 应用在实时语音上。国内腾讯的朋友网将 Node 应用在长连接中,以提供实时服务,花瓣网、蘑菇街等公司通过 socket.io 实现实时通知的功能。
- 并行 I/O 使得使用者可以更高效地利用分布式环境。阿里巴巴和 eBay 是这方面地典型。阿里巴巴地 NodeFox 和 eBay 地 ql.io 都是借用 Node 并行 I/O 地能力,更高效地使用已有的数据。
- 并行 I/O,有效利用稳定接口提升 web 渲染能力。雪球财经和 LinkedIn 的移动版网站均是这种案例,撇弃同步等待 Ⅹ 的顺序请求,大胆采用并行 I/O,加速数据的获取进而提升 web 的渲染速度。
- 云计算平台提供 Node 支持。微软将 Node 引入 Azure 的开发里,阿里云、百度均纷纷在云服务上提供 Node 应用托管服务,Joyent 更是云计算中提供 Node 支持的代表。这类平台看重 javaScript 带来的开发上的优势,以及低资源占用、高性能的特点。
- 游戏开发领域。游戏领域对实时和并发有很高的要求,网易开源了 pemelo 实时框架,可以应用在游戏和高实时应用中。
- 工具类应用。过去依赖 Java 或其它语言构建的前端工具类应用,纷纷被一些前端工程师用 Node 重写,用前端熟悉的语言为前端构建熟悉的工具。