Node.js 不是 JS 应用、而是 JS 运行平台
看到 Node.js 这个名字,初学者可能会误以为这是一个 Javascript 应用,事实上,Node.js 采用 C++ 语言编写而成,是一个 Javascript 的运行环境。为什么采用 C++ 语言呢?据 Node.js 创始人 Ryan Dahl 回忆,他最初希望采用 Ruby 来写 Node.js,但是后来发现 Ruby 虚拟机的性能不能满足他的要求,后来他尝试采用 V8 引擎,所以选择了 C++ 语言。既然不是 Javascript 应用,为何叫 .js 呢?因为 Node.js 是一个 Javascript 的运行环境。提到 Javascript,大家首先想到的是日常使用的浏览器,现代浏览器包含了各种组件,包括渲染引擎、Javascript 引擎等,其中 Javascript 引擎负责解释执行网页中的 Javascript 代码。作为 Web 前端最重要的语言之一,Javascript 一直是前端工程师的专利。不过,Node.js 是一个后端的 Javascript 运行环境(支持的系统包括 Linux、Windows),这意味着你可以编写系统级或者服务器端的 Javascript 代码,交给 Node.js 来解释执行。
Node.js 采用了 Google Chrome 浏览器的 V8 引擎,性能很好,同时还提供了很多系统级的 API,如文件操作、网络编程等。浏览器端的 Javascript 代码在运行时会受到各种安全性的限制,对客户系统的操作有限。相比之下,Node.js 则是一个全面的后台运行时,为 Javascript 提供了其他语言能够实现的许多功能。
Node.js 的特点
我们先来看看 Node.js 官网上的介绍:
Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.
其特点为:
它是一个 Javascript 运行环境
依赖于 Chrome V8 引擎进行代码解释
事件驱动
非阻塞 I/O
轻量、可伸缩,适于实时数据交互应用
单进程,单线程
Node.js 带来的对系统瓶颈的解决方案
1. 并发连接
举个例子,想象一个场景,我们在银行排队办理业务,我们看看下面两个模型
(1)系统线程模型:
这种模型的问题显而易见,服务端只有一个线程,并发请求(用户)到达只能处理一个,其余的要先等待,这就是阻塞,正在享受服务的请求阻塞后面的请求了
(2)多线程、线程池模型:
这个模型已经比上一个有所进步,它调节服务端线程的数量来提高对并发请求的接收和响应,但并发量高的时候,请求仍然需要等待,它有个更严重的问题:
回到代码层面上来讲,我们看看客户端请求与服务端通讯的过程:
服务端与客户端每建立一个连接,都要为这个连接分配一套配套的资源,主要体现为系统内存资源,以 PHP 为例,维护一个连接可能需要 20M 的内存
这就是为什么一般并发量一大,就需要多开服务器
那么 Node.js 是怎么解决这个问题的呢?
我们来看另外一个模型,想象一下我们在快餐店点餐吃饭的场景
(3)异步、事件驱动模型
我们同样是要发起请求,等待服务器端响应;但是与银行例子不同的是,这次我们点完餐后拿到了一个号码,
拿到号码,我们往往会在位置上等待,而在我们后面的请求会继续得到处理,同样是拿了一个号码然后到一旁等待,接待员能一直进行处理。
等到饭菜做号了,会喊号码,我们拿到了自己的饭菜,进行后续的处理(吃饭)
这个喊号码的动作在 Node.js 中叫做回调(Callback),能在事件(烧菜,I/O)处理完成后继续执行后面的逻辑(吃饭),
这体现了 Node.js 的显著特点,异步机制、事件驱动
整个过程没有阻塞新用户的连接(点餐),也不需要维护已经点餐的用户与厨师的连接
基于这样的机制,理论上陆续有用户请求连接, Node.js 都可以进行响应,因此 Node.js 能支持比Java、PHP程序更高的并发量
虽然维护事件队列也需要成本,再由于 Node.js 是单线程,事件队列越长,得到响应的时间就越长,并发量上去还是会力不从心
总结一下 Node.js 是怎么解决并发连接这个问题的:
更改连接到服务器的方式,每个连接发射(emit)一个在 Node.js 引擎进程中运行的事件(Event),放进事件队列当中,
而不是为每个连接生成一个新的 OS 线程(并为其分配一些配套内存)
2. I/O 阻塞
Node.js 解决的另外一个问题是 I/O 阻塞,看看这样的业务场景:需要从多个数据源拉取数据,然后进行处理
(1)串行获取数据
这是我们一般的解决方案,以 PHP 为例
假如获取 profile 和 timeline 操作各需要 1s,那么串行获取就需要 2s
(2) Node.js 非阻塞 I/O,发射/监听事件来控制执行过程
Node.js 遇到 I/O 事件会创建一个线程去执行,然后主线程会继续往下执行的
因此,拿 profile 的动作触发一个 I/O 事件,马上就会执行拿 timeline 的动作
两个动作并行执行,假如各需要 1s,那么总的时间也就是 1s
它们的I/O操作执行完成后,发射一个事件,profile和timeline
事件代理接收后继续往下执行后面的逻辑,这就是 Node.js 非阻塞I/O的特点
总结一下:
Java、PHP 也有办法实现并行请求(子线程),但 Node.js 通过回调函数(Callback)和异步机制会做得很自然
Node.js 的优缺点
优点:
高并发(最重要的优点)
适合 I/O 密集型应用
缺点:
- 不适合 CPU 密集型应用;CPU 密集型应用给 Node 的挑战主要是:由于JavaScript 单线程的原因,如果有长时间运行的计算(比如大循环),将会导致 CPU 时间片不能释放,使得后续 I/O 发起;
解决方案:分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞 I/O 调用的发起;
只支持单核 CPU,不能充分利用 CPU
可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
原因:单进程,单线程
解决方案:
(1)Nginx 反向代理,负载均衡,开多个进程,绑定多个端口;
(2)开多个进程监听同一个端口,使用 cluster 模块;
开源组件库质量参差不齐,更新快,向下不兼容
Debug 不方便,错误没有 stack trace
适合 Node.js 的场景
1. RESTful API
这是 Node.js 最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。
它本质上只是从某个数据库中查找一些值并将它们组成一个响应。
由于响应是少量文本,入站请求也是少量的文本,因此流量不高,一台机器甚至也可以处理最繁忙的公司的 API 需求。
2. 统一 Web 应用的 UI 层
目前 MVC 的架构,在某种意义上来说,Web 开发有两个 UI 层,一个是在浏览器里面我们最终看到的,另一个在 server 端,负责生成和拼接页面。
不讨论这种架构是好是坏,但是有另外一种实践,面向服务的架构,更好的做前后端的依赖分离。
如果所有的关键业务逻辑都封装成 REST 调用,就意味着在上层只需要考虑如何用这些 REST 接口构建具体的应用。
那些后端程序员们根本不操心具体数据是如何从一个页面传递到另一个页面的,他们也不用管用户数据更新是通过 Ajax 异步获取的还是通过刷新页面
3. 大量 Ajax 请求的应用
例如个性化应用,每个用户看到的页面都不一样,缓存失效,需要在页面加载的时候发起 Ajax 请求
Node.js 能响应大量的并发请求
总而言之, Node.js 适合运用在高并发、I/O 密集、少量业务逻辑的场景
结尾
其实 Node.js 能实现几乎一切的应用
我们考虑的点只是适不适合用它来做