[书籍精读]《深入浅出Node.js》精读笔记分享

414 阅读20分钟

写在前面

  • 书籍介绍:本书由首章Node介绍为索引,涉及Node的各个方面,主要内容包含模块机制的揭示、异步I/O实现原理的展现、异步编程的探讨、内存控制的介绍、二进制数据Buffer的细节、Node中的网络编程基础、Node中的Web开发、进程间的消息传递、Node测试以及通过Node构建产品需要的注意事项。
  • 我的简评:这是一本难得的好书,这本书理论和实践结合的很好。如果你是一个纯前端的开发者,这本书可以读读开拓些视野,如果你是一个全栈的开发者,这本书作为入门和深入后端也很不错,推荐拜读。
  • !!文末有pdf书籍、笔记思维导图、随书代码打包下载地址,需要请自取!阅读[书籍精读系列]所有文章,请移步:推荐收藏-JavaScript书籍精读笔记系列导航

第一章 Node简介

1.1.Node的诞生历程

  • 2009年3月, Ryan Dahl

1.2.Node的命名与起源

  • 别名 Nodejs、 NodeJS、 Node.js
  • 找到了设计高性能, Web服务器的几个要点: 事件驱动、非阻塞I/O
  • JavaScript 高性能、符合事件驱动、没有历史包袱
  • 构建网络应用的一个基础框架

1.3.Node给JavaScript带来的意义

  • 浏览器中除了V8作为JavaScript引擎外,还有一个WebKit布局引擎
  • 浏览器通过事件驱动来服务界面上的交互, Node通过过事件驱动来服务I/O

1.4.Node的特点

  • 异步I/O、事件与回调函数、单线程、跨平台
  • 单线程:弱点1:无法利用多核CPU;弱点2:错误会引起整个应用退出,应用的健壮性值得考验;弱点3:大量计算占用CPU导致无法继续调用异步I/O;Node采用与Web Workers相同的思路来解决单线程中大计算量的问题:child_process;
  • 跨平台:在操作系统与Node上层模块系统之间构建了一层平台层架构,即libuv;Node的第三方C++模块也可以借助libuv实现跨平台;

1.5.Node的应用场景

  • I/O密集型:面向网络且擅长并行I/O
  • 是否不擅长CPU密集型业务:采用使用多线程的方式进行计算;通过编写C++扩展的方式更高效利用CPU;
  • 与遗留系统和平共处
  • 分布式应用:数据平台、数据库集群

1.6.Node的使用者

  • 前后端编程语言环境统一
  • Node带来的高性能I/O用于实时应用
  • 并行I/O使得使用者可以更高效的利用分布式环境
  • 并行I/O,有效利用稳定接口提升Web渲染能力
  • 云计算平台提供Node支持
  • 游戏开发领域
  • 工具类应用

第二章 模块机制

  • 大致经历了工具类库、组件库、前端框架、前端应用的变迁

2.1.CommonJS规范

  • CommonJS的出发点:规范薄弱,以下缺陷(没有模块系统、标准库较少、没有标准接口、缺乏包管理系统)
  • CommonJS的模块规范:主要分为模块引用、模块定义和模块标识3个部分

2.2.Node的模块实现

  • 优先从缓存加载:浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象
  • 路径分析和文件定位:Node 会按.js、.json、.node的次序补足扩展名
  • 模块编译:每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上;在编译的过程中,Node对获取的JavaScript文件内容进行头尾包装;(function(exports, require, module, __filename, __dirname) {\n, 在尾部添加了\n});C/C++模块,Node调用process.dlopen()方法进行加载和执行;

2.3.核心模块

  • JavaScript核心模块的编译过程:C/C++文件放在Node项目的src目录下,JavaScript文件存放在lib目录下;编译程序需要将所有的JavaScript模块文件编译为C/C++代码;
  • C/C++核心模块的编译过程:Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助内建模块

2.4.C/C++扩展模块

  • 说明:JavaScript的一个典型弱点就是位运算;*nix下通过g++/gcc等编译器编译为动态链接共享对象文件.so,Windows下则需要通过VisualC++的编译器编译为动态链接库文件.dll;
  • 前提条件:GYP项目生成工具、V8引擎C++库、libuv库、Node内部库、等等
  • C/C++扩展模块的编写:普通的扩展模块与内建模块的区别在于无需将源代码编译进Node,而是通过dlopen()方法动态加载
  • C/C++扩展模块的编译:写好.gyp项目文件是除编码外的头等大事;编译过程会根据平台不同,分别通过make或者vcbuild进行编译;
  • C/C++扩展模块的加载:require()方法通过解析标识符、路径分析、文件定位,然后加载执行即可;加载.node文件实际上经历了两个步骤,第一个步骤是掉用uv_dlopen方法去打开动态链接库,第二个步骤调用uv_dlsym()方法找到动态链接库中通过NODE_MODULE宏定义的方法地址;

2.5.模块调用栈

  • JavaScript核心模块主要扮演的职责有两类:一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用;一类是纯粹的功能模块,不需要跟底层打交道

2.6.包和NPM

  • 包描述文件与NPM:包规范的定义可以帮助Node解决依赖包安装的问题;NPM实际需要的字段主要有name、version、description、keywords、repositories、author、bin、main、scripts、engines、dependencies、devDependencies;
  • NPM常用功能:NPM帮助完成了第三方模块的发布、安装和依赖;查看帮助npm、分析包npm ls;
  • 局域NPM:能够享受到NPM上众多的包,同时对自己的包进行保密和限制
  • NPM潜在问题:开发人员水平不一,包质量也良莠不齐;NPM模块首页上的依赖榜可以说明模块的质量和可靠性;GitHub上项目的观察者数量和分支数量从侧面反映模块的可靠性和流行度;计划引入CPAN社区中的Kwalitee风格来让模块进行自然排序;

2.7.前后端共用模块

  • 模块的侧重点:前端通过网络加载代码,瓶颈在于带宽,后端从磁盘加载,瓶颈在于CPU和内存等资源
  • AMD规范:AMD模块需要用define来明确定义一个模块,而在Node实现中是隐式包装的
  • CMD规范:CMD与AMD规范的主要区别在于定义模块和依赖引入的部分
  • 兼容多种模块规范:包装兼容Node、AMD、CMD以及常见的浏览器环境中

第三章 异步IO

  • 伴随着异步I/O的还有事件驱动和单线程

3.1.为什么要异步I/O

  • 用户体验:I/O是昂贵的,分布式I/O是更昂贵的
  • 资源分配:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好的使用CPU

3.2.异步I/O与实现现状

  • 异步I/O在Node中应用最为广泛,但是它并非Node的原创
  • 异步I/O与非阻塞I/O:操作系统内核对于I/O只有两种方式:阻塞与非阻塞;现存的轮询技术主要有:read,select,poll,epoll,kequeue(read:重复调用来检查I/O的状态来完成完整数据的读取;select:通过对文件描述符上的事件状态来进行判断;poll:采用链表的方式避免数组长度的限制,其次能避免不需要的检查;epoll:Linux下效率最高的I/O事件通知机制;kqueue:与epoll类似,不过仅在FreeBSD系统下存在)
  • 理想的非阻塞异步I/O
  • 现实的异步I/O:在Node中,无论是*nix还是Windows平台,内部完成I/O任务的另有线程池

3.3.Node的异步I/O

  • 事件循环:Node自身的执行模型:事件循环
  • 观察者:在Node中,事件主要来源于网络请求、文件I/O等;在Windows下,这个循环基于IOCP创建,而在*nix下则基于多线程创建;
  • 请求对象:从JavaScript发起调用到内核执行完I/O操作的过渡过程中存在的一种中间产物
  • 执行回调:事件循环、观察者、请求对象、I/O线程池这四者共同构成Node异步I/O模型的基本要素;完成异步I/O的过程(Windows下通过IOCP向系统内核发送I/O调用和从内核获取已完成的I/O操作,配以事件循环;Linux下通过epoll实现;FreeBSD下通过kqueue实现;Solaris下通过Event ports实现)

3.4.非I/O的异步API

  • 与I/O无关的异步API:setTimeout()、setInterval()、setImmedate()、process.nextTick()
  • 定时器:实现原理与异步I/O比较类似,只是不需要I/O线程池的参与;问题在于并非精确的;
  • process.nextTick():采用定时器需要动用红黑树,创建定时器对象和迭代等操作;定时器中采用红黑树的操作时间复杂度为O(lg(n)),nextTick()的时间复杂度为O(1);
  • setImmediate():process.nextTick()中的回调函数执行的优先级要高于setImmediate();process.nextTick()属于idle观察者,setImmediate()属于check观察者;

3.5.事件驱动与高性能服务器

  • Node无需为每一个请求创建额外的对应线程
  • 不受线程上下文切换开销的影响
  • 一些知名的基于事件驱动的实现:Ruby的Event Machine;Perl的AnyEvent;Python的Twisted;

第四章 异步编程

  • Node是首个将异步大规模带到应用层面的平台

4.1.函数式编程

  • 高阶函数:高阶函数是可以把函数作为参数,或是将函数作为返回值的函数
  • 偏函数用法:偏函数用法是指创建一个调用另外一个部分参数或变量已经预置的函数的函数的用法;通过指定部分参数来产生一个新的定制函数的形式就是偏函数;

4.2.异步编程的优势与难点

  • 优势:Node的异步模型和V8的高性能
  • 难点:异常处理、函数嵌套、阻塞代码、多线程编程、异步转同步

4.3.异步编程解决方案

  • 事件发布/订阅模式:如果一个事件添加了超过10个侦听器将会得到一条警告,使用emitter.setMaxListeners(0)去掉限制
  • Promise/Deferred模式:Deferred主要是用于内部,用于维护异步模型的状态;Promise则作用与外部,通过then()方法暴露给外部以添加自定义逻辑;Promise/Deferred模式将业务中不可变的部分封装在了Deferred,将可变的部分交给Promise;
  • 流程控制库:尾触发与Next,目前应用最多的地方是Connect的中间件;async,长期占据NPM依赖榜的前三名,series实现异步的串行执行,parallel实现异步的并行执行,waterfall实现异步调用的依赖处理;step,更轻量;wind,思路完全不同的异步编程方案;

4.4.异步并发控制

  • 同步I/O,每个I/O都是彼此阻塞的,不会出现耗用文件描述符太多的情况
  • bagpip的解决方案:bagpipe模块的解决思路(通过一个队列来控制并发量;调用发起但未执行的异步调用量小于限定值,从队列中取出执行;如果活跃调用达到限定值,调用暂时存放在队列中;每一个异步调用结束时,从队列中取出新的异步调用执行;);拒绝访问;超时控制;
  • async的解决方案:async中parallelLimit()用于处理异步调用的限制

第五章 内存控制

  • 基于无阻塞、事件驱动建立的Node服务,具有内存消耗低的优点,非常适合处理海量的网络请求

5.1.V8的垃圾回收机制与内存限制

  • Node与V8
  • V8的内存限制:只能使用部分内存(64位系统下约为1.4G,32位系统下约为0.7G)
  • V8的对象分配:V8依然提供选项让我们使用更多的内存;--max-old-space-size设置老生代内存空间的最大值;--max-new-space-size设置新生代内存空间大小;
  • V8的垃圾回收机制:垃圾回收策略主要基于分代式垃圾回收机制;在分代的基础上,新生代的对象主要通过Scavenge算法进行垃圾回收;V8在老生代中主要采用Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收;
  • 查看垃圾回收日志:在启动时添加--trace-gc参数;node启动时使用--prof参数,可以得到V8执行时的性能分析数据;提供linux-tick-processor工具用于统计日志信息;

5.2.高效使用内存

  • 作用域:能形成作用域的有函数调用、with以及全局作用域
  • 闭包:实现外部作用域访问内部作用域中变量的方法
  • 无法立即回收的内存有闭包和全局变量引用这两种情况

5.3.内存指标

  • 查看内存使用情况:process.memoryUsage()可以看到Node进程的内存占用情况;os模块的totalmem()和freemem()查看系统的总内存和闲置内存;
  • 堆外内存:不通过V8分配的内存称为堆外内存;利用堆外内存可以突破内存限制的问题;

5.4.内存泄露

  • 造成内存泄露的原因几个:缓存、队列消费不及时、作用域未释放
  • 慎将内存当作缓存:在Node中任何试图拿内存当缓存的行为都应当被限制
  • 关注队列状态:使任何异步调用的回调都具备可控的响应时间

5.5.内存泄露排查

  • 常见的用于定位Node应用内存泄露的工具:v8-profiler、node-headpdump、node-mtrace、dtrace、node-memwatch
  • node-headdump
  • node-memwatch

5.6.大内存应用

  • Node提供了stream模块用于处理大文件
  • 要小心,即使V8不限制堆内存的大小,物理内存依然有限制

第六章 理解Buffer

6.1.Buffer结构

  • 模块结构:Buffer是一个像Array的对象,但它主要用于操作对象
  • Buffer对象:buf[10]的元素值是一个0到255的随机值
  • Buffer内存分配:Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请的;Node以8KB为界限来区分Buffer是大对象还是小对象;真正的内存是在Node的C++层面提供的,JavaScript层面只是使用它;

6.2.Buffer的转换

  • 目前支持的字符串编码类型:ASCII、UTF-8、UTF-16LE/UCS-2、Base64、Binary、Hex
  • 字符串转Buffer
  • Buffer转字符串
  • Buffer不支持的编程类型:Buffer提供一个isEncoding()函数来判断编码是否支持转换;iconv和iconv-lite两个模块可以支持更多的编码类型转换;

6.3.Buffer的拼接

  • 乱码是如何产生的
  • setEncode()与string_decoder()
  • 正确拼接Buffer:调用Buffer.concat()方法生成一个合并的Buffer对象

6.4.Buffer与性能

  • Buffer在文件I/O和网络I/O中运用广泛
  • Buffer是二进制数据,字符串与Buffer之间存在编码关系

第七章 网络编程

  • Node提供了net、dgram、http、https这4个模块,分别用于处理TCP、UDP、HTTP、HTTPS

7.1.构建TCP服务

  • TCP
  • 创建TCP服务器端
  • TCP服务的事件:TCP套接字是可写可读的Stream对象,可以利用pipe()方法巧妙的实现管道操作

7.2.构建UDP服务

  • 创建UDP套接字
  • 创建UDP服务器端
  • 创建UDP客户端
  • UDP套接字事件:UDP套接字只是一个EventEmitter的实例,而非Stream的实例

7.3.构建HTTP服务

  • HTTP
  • http模块:TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务
  • HTTP客户端

7.4.构建WebSocket服务

  • WebSocket握手
  • WebSocket的数据传输

7.5.网络服务与安全

  • Node在网络安全上提供了3个模块,分别为crypto、tls、https
  • TLS/SSL
  • TLS服务
  • HTTPS服务

第八章 构建Web应用

8.1.基础功能

  • 请求方法
  • 路径解析
  • 查询字符串
  • Cookie:Cookie处理的几步(服务器向客户端发送cookie;浏览器将Cookie保存;之后每次浏览器都会将Cookie发向服务器端)
  • Session:常见的两种实现方式(基于Cookie来实现用户和数据的映射;通过查询字符串来实现浏览器端和服务器端数据的对应);Connect默认采用connect_uid,Tomcat会采用jsessionid等;
  • 缓存:提高性能,YSlow中提到的几条关于缓存的规则(添加Expires或Cache-Control到报文中;配置ETags;让Ajax可缓存)
  • Basic认证:通过Base64加密后在网络中传送,有太多的缺点

8.2.数据上传

  • 表单数据:通过报头的Transfer-Encoding或Content-Length即可判断请求中是否带有内容
  • 附件上传:浏览器在遇到multipart/form-data表单提交时,构造的请求报文与普通表单完全不同;formidable,基于流式处理解析报文,将接收到的文件写入到系统的临时文件夹中,并返回对应的路径;
  • 数据上传与安全

8.3.路由解析

  • 文件路径型
  • MVC
  • RESTful

8.4.中间件

  • 异常处理
  • 中间件与性能
  • 从凌乱的发散状态收敛成很规整的组织方式

8.5.页面渲染

  • 内容响应:不同的文件类型具有不同的Mime值;Content-Disposition字段影响的行为是客户端会根据它的值判断是应该将报文数据当作即时浏览的内容,还是可下载的附件;
  • 视图渲染
  • 模板:实质就是将模板文件和数据通过模板引擎生成最终的HTML代码;形成模板技术4个要素(模板语言;包含模板语言的模板文件;拥有动态数据的数据对象;模板引擎);mustache,弱逻辑的模板;最知名的有EJS、Jade等;
  • Bigpipe:用于调用限流,解决重数据页面的加载速度问题;解决思路是将页面分割成多个部分,先向用户输出没有数据的布局,将每个部分逐步输出到前端,再最渲染填充框架,完成整个网页的渲染;

第九章 玩转进程

9.1.服务模型的变迁

  • 石器时代:同步 - 只在一些无并发要求的应用中存在
  • 青铜时代:复制进程 - 要复制较多的数据,启动是较为缓慢的
  • 白银时代:多进程 - 时间将会被耗用在上下文切换中
  • 黄金时代:事件驱动 - 内存耗用的问题著名的C10k问题;单线程避免了不必要的内存开销和上下文切换开销;

9.2.多进程架构

  • child_process.fork()复制的都是一个独立的进程,独立而全新的V8实例
  • 启动多个进程只是为了充分将CPU资源利用起来,而不是为了解决并发问题
  • 创建子进程
  • 进程间通信:JavaScript主线程与UI渲染共用同一个线程;实现进程间通信的技术有很多,如命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket等;操作系统的文件描述符是有限的;
  • 句柄传递:句柄是一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符;文件描述符实际上是一个整数值;Node进程之间只有消息传递,不会真正的传递对象;

9.3.集群稳定之路

  • 进程事件
  • 自动重启:创建新工作进程在前,退出异常进程在后
  • 负载均衡:轮叫调度的工作方式是由主进程接受连接,将其依次分发给工作进程
  • 状态共享:解决数据共享最简单、直接的方式就是通过第三方进行数据存储;主动通知;

9.4.Cluster模块

  • Cluster工作原理:事实上是child_process和net模块的组合应用
  • Cluster事件

第十章 测试

10.1.单元测试

  • 编写可测试代码的几个原则:单一职责、接口抽象、层次分离;
  • 单元测试主要包含断言、测试框架、测试用例、测试覆盖率、mock、持续继承等,由于Node的特殊性,还会加入异步代码测试和私有方法的测试;
  • JavaScript的断言规范最早来自于CommonJS的单元测试规范;
  • 单元测试风格主要有TDD(测试驱动开发)和BDD(行为驱动开发)两种;
  • BDD对测试用例的组织主要采用describe和it进行组织;
  • TDD对测试用例的组织主要采用suite和test完成;
  • 单元测试覆盖率方便我们定位没有测试到的代码行;
  • 私有方法的测试(Java一类的语言,私有方法访问可以通过反射的方式实现;巧妙利用闭包的诀窍,在eval()执行时,实现对模块内部局部变量的访问,从而可以将局部变量导出给测试用例调用执行;)

10.2.性能测试

  • 单元测试主要用于检测代码的行为是否符合预期,性能测试的范畴比较广泛,包括负载测试、压力测试和基准测试
  • 基准测试
  • 压力测试:最常用的工具是ab、siege、http_load等
  • 基准测试驱动开发
  • 测试数据与业务数据的转换

第十一章 产品化

11.1.项目工程化

  • 目录结构
  • 构建工具:在Web应用中通常会在Makefile文件中编写一些构建任务来帮助提升效率;合并编译、应用打包、运行测试、清理目录、扫描代码等;
  • 编码规范:一种是文档式的约定,一种是代码提交时的强制检查
  • 代码审查

11.2.部署流程

  • 部署环境
  • 部署操作

11.3.性能

  • 几个拆分原则:做专一的事;让擅长的工具做擅长的事情;将模型简化;将风险分离;
  • 动静分离
  • 启动缓存
  • 多进程架构
  • 读写分离:进行数据库的读写分离,将数据库进行主从设计

11.4.日志

  • 访问日志
  • 异常日志:log与info方法都将信息输出给标准输出process.stdout,warn与error方法则将信息输出到标准错误process.stderr;console对象上有个Console属性,它是console对象的构造函数;回调函数中产生的异常,交给全局的uncaughtException事件去捕获;
  • 日志与数据库
  • 分割日志

11.5.监控报警

  • 监控:一种是业务逻辑型的监控,一种是硬件型的监控;主要指标:日志监控、响应时间、进程监控、磁盘监控、内存监控、CPU占用监控、CPU load监控、I/O负载、网络监控、应用状态监控、DNS监控;
  • 报警的实现
  • 监控系统的稳定性

11.6.稳定性

  • 典型的水平扩展方式就是多进程、多机器、多机房

写在后面