- Node简介
- 第一个node程序
- module(模块系统)
- npm包管理器
- 模块系统优先级
- 认识http内置模块
- url内置模块
- path内置模块
- fs内置模块
- http模块服务端进阶
- http报文浅析
- url模块进阶
- path模块进阶
- querystring模块进阶
- 了解Buffer和Stream
- os模块
- Buffer模块
- Stream模块
- http模块客户端
- Cookie浅析
Node简介
1. 为什么选择JavaScript?
Ryan Dahl是一名资深的C/C++程序员,在创造出Node之前,他的主要工作都是围绕高性能Web服务器进行的。经历过一些尝试和失败之后,他找到了设计高性能,Web服务器的几个要点:事件驱动、非阻塞I/O。
所以Ryan Dahl最初的目标是写一个基于事件驱动、非阻塞I/O的Web服务器,以达到更高的性能,提供Apache等服务器之外的选择。他提到,大多数人不设计一种更简单和更有效率的程序的主要原因是他们用到了阻塞I/O的库。写作Node的时候,Ryan Dahl曾经评估过C、Lua、Haskell、Ruby等语言作为备选实现,结论为:C的开发门槛高,可以预见不会有太多的开发者能将它用于日常的业务开发,所以舍弃它;Ryan Dahl觉得自己还不足够玩转Haskell,所以舍弃它;Lua自身已经含有很多阻塞I/O库,为其构建非阻塞I/O库也不能改变人们继续使用阻塞I/O库的习惯,所以也舍弃它;而Ruby的虚拟机由于性能不好而落选。
相比之下,JavaScript比C的开发门槛要低,比Lua的历史包袱要少。尽管服务器端JavaScript存在已经很多年了,但是后端部分一直没有市场,可以说历史包袱为零,为其导入非阻塞I/O库没有额外阻力。另外,JavaScript在浏览器中有广泛的事件驱动方面的应用,暗合Ryan Dahl喜好基于事件驱动的需求。当时,第二次浏览器大战也渐渐分出高下,Chrome浏览器的JavaScript引擎V8摘得性能第一的桂冠,而且其基于新BSD许可证发布,自然受到Ryan Dahl的欢迎。考虑到高性能、符合事件驱动、没有历史包袱这3个主要原因,JavaScript成为了Node的实现语言。
2. Node给JavaScript带来的意义
V8给Chrome浏览器带来了一个强劲的心脏,使得它在浏览器大战中脱颖而出,也使得Ryan Dahl在语言评估中为选择JavaScript增加了一个极大的权重值。这里我们要谈谈Node给JavaScript带来的一个新局面。鉴于Node之前那些不给力的后端JavaScript实现,在性能和编程模型等方面没能达到与其他语言一较高下的程度,这里先撇开不谈,先谈谈Node与浏览器的对比。
Chrome浏览器和Node的组件构成如图1-1所示。我们知道浏览器中除了V8作为JavaScript引擎外,还有一个Webkit布局引擎。 HTML5在发展过程中定义了更多更丰富的API。在实现上,浏览器提供了越来越多的功能暴露给JavaScript和HTML标签。这个愿景美好,但对于前端浏览器的发展现状而言,HTML5标准统一的过程是相对缓慢的。JavaScript作为一门图灵完备的语言,长久以来却限制在浏览器的沙箱中运行,它的能力取决于浏览器中间层提供的支持有多少。
除了HTML、WebKit和显卡这些UI相关技术没有支持外,Node的结构与Chrome十分相似。它们都是基于事件驱动的异步架构,浏览器通过事件驱动来服务界面上的交互,Node通过事件驱动来服务I/O,这个细节将在第3章中详述。在Node中,JavaScript可以随心所欲地访问本地文件,可以搭建WebSocket服务器端,可以连接数据库,可以如Web Workers一样玩转多进程。如今,JavaScript可以运行在不同的地方,不再继续限制在浏览器中与CSS样式表、DOM树打交道。如果HTTP协议栈是水平面,Node就是浏览器在协议栈另一边的倒影。Node不处理UI,但用与浏览器相同的机制和原理运行。Node打破了过去JavaScript只能在浏览器中运行的局面。前后端编程环境统一,可以大大降低前后端转换所需要的上下文交换代价。
对于前端工程师而言,自己所熟悉的JavaScript如今竟然可以在另一个地方放出异彩,不谈其他原因,仅仅因为好奇,就值得去关注和探究它。
3. Node特性
3.1单线程:
Node保持了JavaScript在浏览器中单线程的特点。而且在Node中,JavaScript与其余线程是无法共享任何状态的。单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。
同样,单线程也有它自身的弱点,这些弱点是学习Node的过程中必须要面对的。积极面对这些弱点,可以享受到Node带来的好处,也能避免潜在的问题,使其得以高效利用。单线程的弱点具体有以下3方面。
Ø 无法利用多核CPU。
Ø 错误会引起整个应用退出,应用的健壮性值得考验。
Ø 大量计算占用CPU导致无法继续调用异步I/O。
像浏览器中JavaScript与UI共用一个线程一样,JavaScript长时间执行会导致UI的渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续的异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。
Node采用了与Web Workers相同的思路来解决单线程中大计算量的问题:child_process。子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果,这可以很好地保持应用模型的简单和低依赖。通过Master-Worker的管理方式,也可以很好地管理各个工作进程,以达到更高的健壮性。
3.2 异步I/O(非阻塞I/O)
关于异步I/O,向前端工程师解释起来或许会容易一些,因为发起Ajax调用对于前端工程师而言是再熟悉不过的场景了。下面是一个Ajax异步请求的执行图:
在Node中,绝大多数的操作都以异步的方式进行调用。RyanDahl在底层构建了很多异步I/O的API,从文件读取到网络请求等,均是如此。这样的意义在于,在Node中,我们可以从语言层面很自然地进行并行I/O操作。每个调用之间无须等待之前的I/O调用结束。在编程模型上可以极大提升效率。对于同步I/O而言,它们的耗时是两个任务的耗时之和,异步带来的优势是显而易见的。
3.3 事件驱动
3.3.1 为什么JavaScript是单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
3.3.2 任务队列(事件队列或回调队列)
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
3.3.3 事件和回调函数
“任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列”,就是读取里面有哪些事件。
“任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列”,等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
3.3.4 事件轮询(Event Loop)
"任务队列"中的事件会被不断地读取,这个过程是循环不断的,所以整个的这种运行机制被称为事件轮询(事件循环)。