碎碎念:距离上一次写博客已经过去了一个月,说好的半个月写一篇博客(自己打自己脸-。-)没有实现。其实主要这一个月都在看书,感觉没啥好写,但后来灵光一现,直接写读书笔记啊!把书上的一些知识点、重点用博客记下来!说干就干,这次就来写下读完朴灵大大的《深入浅出Node.js》后的一些想法,内容和图片主要来自《深入浅出Node.js》。

第一章 Node简介

Node.js是一个能将JavaScript运行在服务器端的平台,且只包含了JavaScript中最核心的ECMAScript,不包含DOM(文档对象)和BOM(浏览器对象),一开始设计的时候就按照高性能服务器的最重要的两点要求:事件驱动、异步I/O(libuv库实现),简单来说,你可以把Node想象成一个没有HTML和webkit的浏览器,然后在其中添加了OpenSSL等其他必备的组件。

深入浅出python中文第二版pdf 深入浅出nodejs.pdf_node.js

补充:Node当时在选择开发语言时经历了一番波折:用C做开发门槛太高;Ruby的虚拟机性能不太行;Lua本身就含有很多阻塞I/O的库;Haskell略小众,用的人不多;JavaScript在服务器端几乎没什么人用,为其导入异步I/O的库的压力很小,同时JavaScript的入门门槛很低,且当时(2009年)Chrome的V8引擎性能秒杀其他浏览器,创始人Ryan Dahl就选择了JavaScript作为开发语言。

Node的特点

优点:

  1. 高并发(最重要的优点),下面是一个测试用例
  2. 适合I/O密集型应用

缺点:

  1. 不适合CPU密集型应用(例如视频解码、视频压缩);CPU密集型应用给Node带来的挑战主要是由于JavaScript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起。解决方案如下:
  1. 分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用。
  2. 使用C/C++编写第三方的模块来更高效地利用CPU。举例:由于JavaScript中只有double的数据类型,所以它位运算的能力是很弱的,所以在进行诸如转码、编码等操作时,得用C/C++模块。
  1. 只支持单核CPU,不能充分利用CPU。解决方案:启动多进程,且有几个CPU就启动几个进程,且多进程后可以做负载均衡。
  2. 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃。原因:单进程,单线程。
    解决方案:
    (1)Nnigx反向代理,负载均衡,开多个进程,绑定多个端口;
    (2)开多个进程监听同一个端口,使用cluster模块,这样一个进程崩溃后还有另外一个;
  3. 开源组件库质量参差不齐,更新快,向下不兼容(这个貌似就得看良心了-。-)

适合Node.js的场景

  • RESTful API
    这是NodeJS最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。它本质上只是从某个数据库中查找一些值并将它们组成一个响应。由于响应是少量文本,入站请求也是少量的文本,因此流量不高,一台机器甚至也可以处理最繁忙的公司的API需求。
  • 统一Web应用的UI层
    目前MVC的架构,在某种意义上来说,Web开发有两个UI层,一个是在浏览器里面我们最终看到的,另一个在server端,负责生成和拼接页面。不讨论这种架构是好是坏,但是有另外一种实践,面向服务的架构,更好的做前后端的依赖分离。如果所有的关键业务逻辑都封装成RESTful调用,就意味着在上层只需要考虑如何用这些REST接口构建具体的应用。后端程序员们根本不操心具体数据是如何从一个页面传递到另一个页面的,他们也不用管用户数据更新是通过Ajax异步获取的还是通过刷新页面。
  • 深入浅出python中文第二版pdf 深入浅出nodejs.pdf_线程池_02

  • 例子:阿里巴巴开发了中间层应用NodeFox,利用Node并行IO的能力来并行地去多台数据库中查询数据,这个时候可以维持住数万的并发!
  • 大量Ajax请求的应用
    例如个性化应用,每个用户看到的页面都不一样,缓存失效,需要在页面加载的时候发起Ajax请求,NodeJS能响应大量的并发请求。最好的例子是Gmail,前端有大量的异步请求,需要服务后端有极高的响应速度。还有一个例子是实时应用系统,比如聊天系统(两次聊天的间隔可能会很久),以及国内的蘑菇街和科举网都使用socket.io来实现实时通知的功能。

总而言之,NodeJS适合运用在高并发、I/O密集、少量业务逻辑的场景。

第二章 模块机制

commonJS规范

JavaScript先天其实就缺乏模块的功能(ES6已经加入了模块系统,有兴趣的朋友可以自行Google),相比较之下,Java有类文件,Python有import机制,Ruby有require,php有include和require,而JavaScript通过script标签引入代码很是杂乱无章,所以后来就有了commonJS,这样JavaScript可以开发像Java、Python一样开发大型应用。

Node中引入模块经历

  1. 路径分析
  2. 文件定位
  • 首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件缺少扩展名,将会进入扩展名分析的步骤。   
  • 而如果main属性制定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js(JS)、index.json 、index.node(C/C++).   
  • 如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败异常。
  1. 编译执行

Node中的模块分为核心模块(大量C/C++模块 + 少量JavaScript模块)和文件模块(用户编写),其中核心模块在安装时就编译成为二进制文件,所以Node进程启动时会将其直接加载进内存,速度很快,核心模块包括了EventEmitter、Stream、FS、Net。Node对于JavaScript核心模块和JavaScript代码,其实是调用了V8附带的js2c.py工具,将其转换为C/C++代码。

Node全局对象

console、Buffer、exports、process(可用于进程间通信)

NPM

NPM其实就是JavaScipt的包管理器,现在已经是最大的第三方模块管理器了!

第三、四章 异步I/O

Node用libuv库来实现异步I/O

深入浅出python中文第二版pdf 深入浅出nodejs.pdf_深入浅出python中文第二版pdf_03


Js是单线程执行的,运行过程产生的所有异步任务,通过层层调用,最终传送到libuv,libuv是统筹这些异步任务的大管家,且默认使用4个线程来进行异步IO,流程图如下:

深入浅出python中文第二版pdf 深入浅出nodejs.pdf_读书笔记_04


从流程图可知,阻塞任务都会分配至其他线程或其他模块里执行,而并不影响JS执行线程的运作,其他线程或模块执行完毕后会标记请求对象的状态,然后通过 Event Loop (类似于一个 while true 的无限循环)遍历来执行对象里的回调函数。下面的代码包含了三种异步任务:

var http = require('http');
var fs = require('fs');

//网络I/O
http.createServer((res) => {
    console.log('network request occur')
}).listen(8080);

//文件I/O
fs.readFile('path/to/file', (err, data) => {
    if(err) throw err;
    console.log('file data is ready!');
});

//延时任务
setTimeout(() => {
    console.log('delay operation')
}, 1000);

console.log('other operation.')

V8执行上面的代码,代码里有3种不同类型的异步任务,分别是网络IO(epoll实现),文件IO(Windows下用IOCP(其实也是线程池)、*nix下用线程池)以及延时任务(最小堆来实现)。这三种异步类型的接口调用完毕会立即返回,复杂的异步操作交给了libuv处理。

  • http.createServer 执行完后,libuv会监听来自8080端口的网络请求,当有新请求接入时候,会创建对应的FD(File descriptor),接着libuv着手处理这个FD的数据状态。实际上libuv在处理这些网络IO时候并没有另起单独线程,这些操作都是在Event Loop线程里完成,处理过程是使用 epoll 技术(一种高性能的IO复用技术,OSX和BSD是 kqueue )实现。首先调用 epoll_create 监听这个FD(file descriptor)状态,调用完毕后立即返回,不会阻塞主线程继续执行,数据处理完毕后标记这个FD为就绪状态,在Event Loop的循环里会调用 epoll_wait 获取这些已经就绪的FD并执行其回调函数。 epoll 内部也是基于事件回调机制,同时还有很多提升性能的特性,得益于 epoll Node在处理网络IO方面具有极高的并发能力。
  • fs.readFile 的执行会涉及到IO线程池的调用,跟网络IO处理方案不尽相同,libuv处理文件IO是在其他单独的线程里完成的。当调用 fs.readFile 操作时,libuv会生成一个请求对象(包含文件路径,读写参数,回调函数等),这个请求对象会被发送至IO线程池,此时 readFile 调用完毕立即返回,主线程继续执行后面的代码。当线程池处理完毕后会把数据写入到请求对象里,同时把回调函数插入到一个回调队列等待执行,Event Loop每一次循环都会适时执行这个队列里头的回调函数,实现类似异步的效果。下图是书上的一个例子。
  • setTimeout 执行完后,这个延时任务会被插入至最小堆里,Event Loop的每次循环都会从最小堆里获取到期任务,并执行其回调函数。