《深入浅出NodeJS》读书笔记

来源:互联网 发布:js窗口高度 编辑:程序博客网 时间:2024/05/20 07:50


NodeJS

NodeJS 4个特点:异步I/O,事件驱动与回调,单线程事件轮询,跨平台。

NodeJS 5个大坑:异常处理,嵌套太深,没有Sleep,多线程编程,异步转同步。

NodeJS 4种提升性能的方法:动静分离,缓存(Redis,多进程,数据库读写分离。

NodeJS简介   模块机制   异步I/O   异步编程   内存控制    Buffer     网络编程     构建Web    多进程    测试   产品化   调试NodeJS    编程规范

 

NodeJS简介

高性能,符合时间驱动,没有历史包袱这三个主要原因,JavaScript成为了Node的实现语言。

NodeJS基于Google V8引擎。Node优秀的运算能力主要来自V8的深度优化。

NodeJS特点:异步I/O,事件驱动与回调函数,单线程事件轮询,跨平台

NodeJS单线程的缺点:

无法利用多核CPU.

错误会引起整个应用进程退出。

大量计算占用CPU导致无法继续调用异步I/O.

 

解决单线程缺点的方法是引入子进程方法(Cluster,见后边)和C/C++模块扩展(利用它们的多线程机制)。

 

模块机制

Node出现之前,服务器端的JS基本没有市场的。

CommonJS主要是为了弥补当前JS没有标准的缺陷,希望JS能够在任何地方运行。

模块引用

var xxx = require(‘模块标识’);

例如:var math = require(‘math’);

模块定义

模块中module对象代表模块自身exports对象是module的属性,用于导出当前模块的方法或者变量,它是唯一导出的出口moduleexportsnode在编译过程中给加上去的。

NodeJS中,一个文件就是一个模块将方法或者变量挂在在exports对象上作为属性即可定义导出的方式:

exports.xxx = ……

例如: exports.add = function(){

var sum = 0;

var i = 0;

….

return sum;

}

exportsmodule.exports区别见后)

模块标识

就是传给require()的参数。它必须符合小驼峰命名的字符串,或者以。,。。开头的相对路径,或者绝对路径。可以有或者没有文件后缀名(最好有后缀,p/17.

模块标识分类:

·        核心模块,如http, fs, path等。

·        .或者..开始的相对路径文件模块。

·        /开始的绝对路径文件模块。

·        非路径形式的文件模块,如自定义的connect模块。

Node中引入模块步骤

(找文件->找文件扩展名->编译执行)

1.      路径分析:定位文件位置,标识符中有路径,甚至没有。

2.      文件定位:标识符中没有文件扩展名,所以需要确定类型。

3.      编译执行:不同文件类型,载入方式不一样。

Node中模块分类

1.      核心模块(Node提供):核心模块在Node源代码的编译过程中,编译进了二进制文件,在Node进程启动时,部分核心模块就被加载进内存中,所以这部分核心模块引入时,文件定位和编译执行两步省略掉,且在路径分析时优先判断,所以核心模块加载速度是最快的。

2.      文件模块(用户编写):在运行时动态加载,三步骤都需要。

Node对引用过的模块都会进行缓存,以减少二次引入时的开销。

模块路径分析策略

node在定位文件模块的具体文件时指定的查找策略。其策略是从当前node-modules开始一级一级向根的node-modules查找,核心模块最快,相对、绝对路径模块次之,自定义文件模块最慢。

所以引用模块时,最好加上路径以及扩展名!

文件定位策略

因为模块标识符中可以不包含文件扩展名,在这种情况下,Node会按照.js, .json, .node的次序补足扩展名,依次尝试。在尝试的过程中,需要调fs模块的同步阻塞式的判断文件是否存在,所以最好传入文件扩展名。

require()通过分析文件扩展之后,可能没找到对应的文件,但却得到了一个目录,此时node会将目录当做一个包来出来(node对包处理会遵守CommonJS包规范)。

Node中,每个文件模块都是一个对象,定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。不同的文件扩展名,其载入和编译的方法也不一样

.js文件:通过fs模块同步读取文件后编译执行。

.node文件:这是C/C++编写的扩展文件,通过process.dlopen()方法加载和执行。它不需要编译,因为它是编写C/C++模块之后编译生成的. C/C++带来的优势主要是执行效率方面的。

.json文件:通过fs模块同步读取文件后,用JSON.parse()解析返回结果。

.其余扩展名文件:当做.js文件处理。

JS模块中require, exports, module这三个变量在模块中并没有定义,是node在编译过程中给加上的,这样每个模块中这三个变量的作用域是隔离的。

exportsmodule.exports区别:

exports指向module.exports的引用, require()返回的是module.exports而不是exports.如果module.exports指向了一个新对象则exports则断开了对module.exports的引用。刚开始module.exports为空对象,所以exports收集的属性和方法都赋给module.exports,而一旦module.exports有了属性,方法,则exports收集的信息将被忽略,所以在不调用module.exports时,采用exports.

Node的核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块分为C/C++编写的和JS编写的两部分。而在编译所有的C/C++文件之前,编译程序需要将所有的JS模块文件编译为C/C++代码。

內建模块

C++模块完成主内完成核心,JS主外实现封装的模式是node能够提高性能的常见方式。由纯C/C++编写的部分统一称为內建模块,因为他们通常不被用户直接调用。

GYP – Generate your projects

项目生成工具,可用于生成各种平台下的项目文件,node中安装npm install –g node-gyp

实现C/C++扩展模块(p/29-31)

第一步:编写C/C++源文件

第二步:创建.gyp项目文件,通过GYP命令进行build生成xxx.node文件。

第三步:通过require加载生成的.node文件,调用模块中的方法。

Node中文件模块,核心模块,內建模块,C/C++扩展模块之间的关系

包和NPM是将模块联系起来的一种机制

包组织模块示意图:

包实际上是一个存档文件,即一个目录直接打包为.zip,安装解压还原为目录。

包结构:

package.json:包描述文件。(详细说明p/35

bin:用于存放可执行二进制文件的目录。

Lib:用于存放js代码的目录。

Doc:存放文档的目录。

Test:存放单元测试用例的代码。

NPM

帮助完成第三方模块的发布,安装和依赖等。

NPM版本: npm –v

NPM帮助: npm

 

使用NPM安装依赖包:

全局安装: npm install –g express –generator,然后再用–g安装。

本地安装: npm install package.json or npm install xxx (会自动上网上搜)

非官方源安装。

 

NPM钩子命令:即在package.jsonscripts字段定义安装前,中,后执行的脚本,完后在npm install package.json时候会调用。

scripts”:{

“preinstall”: “preinstall.js”,

“install”: ”install.js”,

“uninstall”: “uninstall.js”,

“test”: “test.js” //当执行npm test时会执行该脚本

}

 

使用NPM发布包:略。

 

使用npm ls分析包

可以分析出当前路径下能够通过模块路径找到的所有包,并生成依赖树。

 

为了同时能够共享npm上总多的包的,同时对自己的包进行保密和限制,现有的解决方案就是企业搭建自己的npm仓库。

 

Npm平台上,每个人都可以分享包到平台上,但是开发人员水平不一,上面的包质量良莠不齐。另一个问题是node代码可以运行在服务器端,需考虑安全问题。

 

异步I/O

单线程同步编程模型会阻塞I/O导致硬件资源得不到更优的使用。多线程编程模型也因为编程中的死锁,状态同步等问题让开发人员头疼。Node在两者之间给出了答案:利用单线程,远离多线程死锁,状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地使用CPU

异步I/O调用示意图3-1 p/50

阻塞I/O造成CPU等待资源浪费,非阻塞带来的麻烦却是需要轮询去确认是否完全完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。

Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中罢了。在node中,无论Linux还是Windows平台,内部完成I/O任务的另有线程池。+3.10 p/55

事件循环

在进程启动时,Node便会创建一个类似于whiletrue)的循环,每执行一次循环体的过程我们成为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行他们。然后进入下一个循环,如果不再有事件处理,就退出进程。3-11 p/56

 

每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。事件可能来自用户的点击或者加载某些文件时产生,而这些产生的文件都有对应的观察者。

请求对象

JS把要调用的函数,参数,回调函数这些东西封装在一个请求对象中,传递给底层的C++ I/O线程池,完后立即返回。

事件循环,观察者,请求对象,I/O线程池这四者共同构成了Node异步I/O模型的基本要素。

setTimeout()setInterval()用于单次和多次执行任务,不需要I/O线程池参与。非精确定时。

process.nextTick(函数)–即下一个Tick(事件循环)异步执行一个任务,它比setTimeout(xxx,0)更高效。

function foo(){…}

process.nextTick(foo)

setImmediate()process.nextTick()类似,都是将回调函数延迟执行。

二者区别:

a.      process.nextTick()中的回调函数执行的优先级高于setImmediate().

b.     process.nextTick()回调函数存于数组,而setImmediate()回调函数存于链表。每个Tick(即事件轮询)process.nextTick()会全部执行数组中的回调,而setImmediate()只会执行链表中的一个回调。

Node通过事件驱动的方式处理请求,使得服务器能够有条不紊的处理请求,即使在大量连接的情况下,也不受线程上下文切换开销的影响,这是node高性能的一个原因。

 

异步编程

JS中,函数作为一等公民,使用上非常自由,无论调用它,或者作为参数,或者作为返回值均可。

高阶函数

可以把函数作为参数,或者把函数作为返回值的函数。

偏函数

偏函数用法是指创建一个调用另外一个部分—--参数或变量已经预置的函数的函数的用法。

Var isType = function(type){

return function(obj){

 return toString.call(obj) == ‘[object’ + type +’]’;

  };

};

Var isString = isType(‘String’);

Var isFunction = isType(‘Function’);

这种通过指定部分参数来产生个新的定制函数的形式就是偏函数。

Node带来的最大特性莫过于基于事件驱动的非阻塞I/O模型,这是他的灵魂所在。

由于事件循环模型需要应对海量请求,海量请求同时作用在单线程上,就需要防止任何一个计算耗费过多的CPU时间片。建议CPU的好用不要超过10ms,或者将大量的计算分解为诸多的小量计算,通过setTimeout()进行调度。

异步编程的五大难点(坑)

1.      异常处理

异步I/O的实现主要包含两个阶段:提交请求和处理结果。两个阶段中间有事件循环地调度,两者彼此不关联。异步方法则通常在第一个阶段提交请求后立即返回,因为异常并不一定发生在这个阶段,try/catch的功效在此处不会发挥任何作用。

解决方法:通常约定将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常抛出。

另外注意不要对用户传递的回调函数进行异常捕获:

try{

 req.body = JSON.parse(…);

 callback();

}

catch(err){

  ….

  callback(err);

}

如果回调中有异常,将会进入catch,于是回调函数被执行两次。这显然不是预期的情况,可能导致业务混乱。

2.      函数嵌套太深

解决方案:事件,Promise/Deferred模式,流程控制库(async, step等库)。参见后文。

3.      阻塞代码没有sleep

JS没有sleep()这样的线程沉睡功能。

唯独能用于延时操作的只有setInterval()setTimeout()这两函数。但这俩函数不能阻塞后续代码的持续执行。

4.      多线程编程

Node是单线程。

解决方法:通过child_process(API)cluster模块来解决多核CPU利用的问题,见后文。

5.      异步转同步

解决方案:事件,Promise/Deferred模式,流程控制库(async, step等库)。参见后文。

 

异步编程解决方案

1.      事件发布/订阅模式

2.      Promise/Deferred模式

3.   流程控制库,async, step

事件发布/订阅模式

Node自身提供的events模块是发布/订阅模式的简单实现,Node中部分模块都继承自它。

示例代码:

emitter.on(‘event1’ , function(message){

 console.log(message);

});

 

emitter.emit(‘event1’, “I am a message!”);

 

1.也可以实现一个事件,多个侦听器(默认不要超过10个,如果超过10个将会有条警告,因为设计者认为侦听器太多可能导致内存泄漏,同时侦听器过多,可能存在过多占用CPU的情景。可以使用emitter.setMaxListeners(0)将这个限制去掉)。

 

2. events中的EventEmitter对象对error事件进行了特殊的对待。如果运行期间的错误出发了error事件,EventEmitter会检查是否对error事件添加过侦听器,如果添加了会将错误交给该侦听器处理,否则这个错误将作为异常抛出。如果外部没有捕获这个异常,将引起线程退出。

 

3.事件模式常用于解耦业务逻辑,将不变的部分封装在组件内部,将容易变化需自定义的部分通过事件暴露给外部处理,类似于接口的设计。同时事件也是一种钩子机制,利用钩子导出内部数据或状态给外部调用者,比如我们常用的http请求:

res.on(‘data’, function(chunk){

 console.log(‘Body: ’+ chunk);

});

 

4.继承自events模块中的EventEmitter

var events = require(‘events’);

function Stream(){

 events.EventEmitter.call(this);

}

Util.inherts(Stream, events.EventEmitter);

 

5.利用EventEmitter类的once方法实现事件队列,解决雪崩的问题。

  雪崩问题:在高访问量,大并发量的情况下缓存失效的情景,此时大量的请求同时涌入数据库中,数据库无法同时承受如此大量的查询请求,进而影响网站的整体响应速度。

 EventEmitteronce()方法:通过它添加的侦听器只执行一次,在执行之后就会将它与事件的关联移除。

 解决雪崩问题:即每次只有一个查询,其他查询进入事件队列,当查询结束时,会将查询结果通知所有的事件队列中的等待者。

Var proxy = new events.EventEmitter();

Var status = “ready”;

Var select = function(callback){

  Proxy.once(“selected”,callback);

  If(status ===”ready”){

       Status =”pending”;

      Db.select(sqlcommand, function(results){

            Proxy.emit(“selected”, results);

            Status = “ready”;

     } );

 }

};

解释p/77

 

多个异步之间的协作方案

1.      多对一:利用哨兵变量(用于检测次数的变量)来实现多个异步调用之间的协作(即调用多少次后,才干什么事)

var after = function(times, callback){

 var count = 0, results={};

 return function (key ,value){

    results[key] = value;

    count++;

    if(count === times){

         callback(results);

    }

}

}

 

Var done = after(times, render);

2.      多对多:p/78

 

EventProxy模块作者自己写的

all() –所有事件都被触发才执行一次侦听器;

tail() –所有事件都被触发后,执行一次回调,以后触发其中之一的事件,都会回调。

after() –事件触发多少次之后,执行回调。

fail() –绑定错误处理。

done() –触发给定事件,如果有异常触发error事件。   

Promise/Deferred模式

该模式是为了解决JS回调层次太深代码难懂的问题,变成xxx().then().then()的形式。Q模块对这种模式进行封装,会用这个模块就行。(通过npm install q来安装)

Promise/Deferred是一种先执行异步调用,延迟传递处理的方式。包括PromiseDeferred两部分。

 

Promise/A

1.      对单个异步操作的定义:Promise操作只会处在3种状态中的一种:未完成态,完成态和失败态;只能从未完成态向完成态或失败态转化,不能逆反。完成态和失败态之间不能互相转化。

2.      then()方法的要求:一个Promise对象只要具备then()方法即可。p/83

 

实现Promise/A例子 p/84-86.

 

解决多层嵌套的问题

问题:

Obj.api1(function(value1){

    Obj.api2(function(value2){

          Obj.api3(function(value3){

                Obj.api4(function(value4){

                          Callback(value4);

                   });

           });

      });

});

 

·        事件方法:

var emitter = new event.Emitter();

emitter.on(“step1”, function(){

    obj.api1(function(value1){

          emitter.emit(“step2”, value1);

     });

 });

 

emitter.on(“step2”, function(value1){

    obj.api2(function(value2){

          emitter.emit(“step3”, value2);

     });

 });

……

 

emitter.emit(“step1”); // start

 

·        Promise/Deferred

Promise().then(obj.api1).then(obj.api2) .then(obj.api3).then(obj.api4).then(function(value4){ // do something},function(error){//handle error from step1 to step4}).done(); //Q库。

·        async模块的waterfall (前后有依赖关系,无依赖用series)

async.waterfall([

      function(callback){

               obj.api1(function(value1){

               callback(err, value1);

               }

 

      },

 function(arg1, callback){

          obj.api2(function(arg1){

               callback(err, value2);

         }

  },

 function(arg1, callback){

         obj.api3(function(arg1){

               callback(err, value3);

         }

 

 },

 function(arg1, callback){

         obj.api3(function(arg1){

               callback(err, value4);

         }

 }

], function(err, result){

});

 

 

流程控制库– async

1.      async.series()方法来实现异步的串行执行(前后无依赖关系)

async.series( [

function(callback){

 fs.readFile(‘file1.txt’, ‘utf-8’,callback);

},

function(callback){

 fs.readFile(‘file2.txt’, ‘utf-8’,callback);

}

], function(err, results) {

 //results =>[file1.txt,file2.txt]

});

每个callback()执行时会将结果保存起来,然后执行下一个调用,直到结束所有调用。最终的回调函数执行时,队列里的异步调用保存的结果以数组的方式传入。这里的异常处理规则是一旦出现异常,就结束所有调用,并将异常传递给最终的回调函数的第一个参数。

2.      async.parallel()实现异步操作的并行执行,仅当所有异步操作都正常完成时,才会将结果以数组的方式传入

async.parallel( [

function(callback){

 fs.readFile(‘file1.txt’, ‘utf-8’,callback);

},

function(callback){

 fs.readFile(‘file2.txt’, ‘utf-8’,callback);

}

], function(err, results) {

 //results =>[file1.txt,file2.txt]

});

对于异常的判断依然是一旦某个异步调用产生了异常,就会将异常作为第一个参数传入给最终的回调函数。

3.      async.waterfall()实现异步操作的串行调用,且前一个的结果是后一个的输入(区别于series

async.waterfall( [

function(callback){

fs.readFile(‘file1.txt’, ‘utf-8’,function(err, content){

     callback(err,content);

});

},

function(arg1, callback){

 fs.readFile(‘arg1, ‘utf-8’, function(err,content){

         callback(err, content);

});

},

function(arg1, callback){

 fs.readFile(‘arg1, ‘utf-8’, function(err,content){

         callback(err, content);

});

}

], function(err, results) {

 //results =>[file4.txt]

});

4.      async.auto(业务逻辑依赖关系)方法实现复杂的业务处理

 

流程控制库– step

1.      Step实现异步调用的串行化

Step(

function readFile1(){

    fs.readFile(‘file1.txt’, ‘utf-8’, this);

},

function readFile2(){

    fs.readFile(‘file2.txt’, ‘utf-8’, this);

},

function done(err, content){

}

);

2.      Step实现异步调用的并行执行。所有任务(this.parallel)都执行完才调用done.

Step(

    Function readFile1(){

    fs.readFile(‘file1.txt’, ‘utf-8’, this.parallel());

    fs.readFile(‘file2.txt’, ‘utf-8’, this.parallel());

},

Function done(err, content1, content2){

  //content1 => file1,

 // content2 => file2

}

);

如果异步方法的结果传回的是多个参数,Step将只会取前两个参数。

3.      Stepgroup()方法跟parallel方法差不多,只是parallel传给下一个任务的结果是以单个形式传的而group是以数组形式传的。p/101

 

异步并发控制

如果并发量大,下层服务器会吃不消,尽管要压榨底层系统的性能,但是还要给予一定的过载保护,以防止过犹不及。

 

1.      bagpipe模块

使用作者的bagpipe模块实现并发控制(主要暴露push()方法和full事件):

Var Bagpipe = require(‘bagpipe’);

//设定最大并发数

Var bagpipe = new Bagpupe(10);

for(var I = 0; i < 100; i++){

 bagpipe.push( async, function(){

       //异步回调执行

  });

}

 

bagpipe的拒绝模式

Var bagpipe = new Bagpipe(10, { refuse: true })

在拒绝模式下,等待的调用队列满了,新来的调用就直接返回拒绝异常。

 

bagpipe的超时控制

Var bagpipe = new Bagpipe(10, { timeout: 3000 })

如果异步调用没有在规定事件内完成,则会执行用户传入的回调,返回一个超时异常。

 

 

2.      async的异步并发控制

async通过paralleLimit()queue()方法实现并发量控制 p/109

 

内存控制

Javascript由垃圾回收机制来进行自动内存管理,对性能敏感的服务器端程序,内存管理的好坏,垃圾回收状况是否优良,都会对服务构成影响。

Node的内存限制 64bit - 1.4G, 32 bit – 0.7G.

·        这个限制主要是因为Node基于V8构建,所以在Node中使用的JS对象基本上都是通过V8自己的方式来进行分配和管理。而V8最初是为浏览器设计的,对于浏览器而言不太可能遇到用大量内存的场景。更深层的原因是因为V8的垃圾回收机制的限制。比如1.5G的垃圾回收堆内存为例,一次垃圾回收的时间可能需要50毫秒以上,这是垃圾回收引起线程暂停执行的时间。

·        V8中,所有的JS对象都是通过堆来进行分配的。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制。

·        在代码里面可以通过console.log(process.memoryUsage())来查看内存使用情况。

·        Node修改内存限制:在启动Node时传递–max-old-space-size–max-new-space-size来调整内存限制:

node --max-old-space-size = 1700 XXX.js //修改老生代内存限制,单位MB

node –max-new-space-size = 1024 XXX.js //修改新生代内存限制

V8中内存分为新生代和老生代、新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

·        老生代内存最大值在在64位系统下为1400M32位系统下为700M;新生代内存最大值在64位系统下为32M,在32位系统上为16M

·        新生代垃圾回收主要采用Scavenge算法,简而言之,垃圾回收过程中,通过将存活对象在两个semispace空间(From空间和To空间)之间进行复制(p/115.

·        新生代移动到老生代的过程叫晋升,晋升两个条件:

1.      对象是否经历过scavenge回收。2. To 空间的内存占用比超过限制。

·        老生代垃圾回收主要采用Mark-SweepMark-Compact相结合的方式。Mark-Sweep方式在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。经历标记清除回收后,内存空间出现不连续的状态,所以采用Mark-Compact方式将内存碎片在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

·        为了降低垃圾回收带来的停顿时间,V8先从标记阶段入手,将本来要一口气停顿完成的动作改为增量标记,也就是拆分为许多小步,每做完一步,就让JS应用逻辑执行一会,垃圾回收和应用逻辑交替执行直到标记阶段完成。

·        查看垃圾回收日志: node –trace_gc

高效使用内存

1.      标识符(即变量名)查找方向是向上的,所以变量只能向外访问,而不能向内访问。

2.      主动释放内存中的对象,可以通过delete操作来删除引用关系。或者将变量重新赋值为undefined或者null,让旧的对象脱离引用关系.(最好使用后者,因为V8通过delete删除对象的属性可能会干扰V8的优化)

3.      闭包:Javascript中实现外部作用域访问内部作用域中变量的方法叫做闭包

var bar = function(){

var local = “local”;

return function () {

return local;

};

} //这样外部就可以调用此方法来访问内部的local变量。

 

Var ba = bar();

Console.log(ba);

一旦有变量引用这个中间函数,这个中间函数的原始作用域中产生的内存占用不会得到释放。除非不再有引用,才会逐步释放。

 

查看内存

1.      process.memoryUsage() –查看进程的内存使用

2.      使用os模块的totalmem()freemem()来看操作系统的内存使用情况。

3.      堆外内存:不是通过V8进行分配的内存,比如Buffer.

内存泄漏

1.      内存泄漏的实质只有一个,那就是应用回收的对象出现意外而没有被回收,变成了常驻老生代中的对象。

2.      一旦一个对象被当做缓存来使用,就意味着它将会常驻老生代。缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。

3.      对于大量缓存目前比较好的解决方案是采用进程外缓存(Redis, Memcached)。这样减少常驻内存的对象的数量,让垃圾回收更高效,而且进程之间可以共享缓存。

4.      内存泄漏检查工具: V8-profiler, node-heapdump, node-mtrace, dtrace, node-memwatch.

5.      使用node-heapdump查看node内存泄漏(内存快照) p/131 – 132.

6.      使用node-memwatch查看内存泄漏(内存快照)p/133 -135.

大内存应用

Node提供了Stream模块用于处理大文件(stream是原生模块,直接用就行)。fscreateReadStream()createWriteStream()就是对stream的应用,分别用于创建文件的可读流和可写流,这种方式不受V8的内存限制

Var reader = fs.createReadStream(‘in.txt’);

Var writer = fs.createWriteStream(‘out.txt’);

reader.pipe(writer);

 

Buffer

Buffer是一个典型的JavaScriptC++结合的模块,它将性能相关部分用C++实现,将非性能相关的部分用JavaScript实现。

Node在进程启动时就已经加载了Buffer,并将其放在全局对象上,所以不需要require().

Buffer对象类似于数组,它的元素为16进制的两位数,即0-255的数值。可以访问length属性得到长度,也可以通过下标访问。

有趣的是给元素的赋值如果小于0,就将该值逐次加256,直到得到一个0-255之间的数。如果得到的数值大于255,就逐次减256,直到0-255区间的数值。如果有小数则舍弃小数部分。----竟然不报异常!!!

Buffer对象内存申请是在C++中(类似于memory pool,而不是在V8堆上申请,所以不受V8内存限制,但分配是在Javascript中,是被V8垃圾回收标记回收的。

Buffer对象可以与字符串之间相互转化,目前支持的字符串编码类型有:

ASCII, UTF-8, UTF-16LE/UCS-2, Base64, Binary, Hex.

1字符串转Buffer: new Buffer(str [,encoding]) //默认是UTF-8

2 Buffer转字符串 buf.toString([encoding],[start],[end]) //默认UTF-8

3使用Buffer.isEncoding(encoding)来判断编码是否支持转换,若不支持,可考虑换别的模块。

4 Buffer.concat()方法把一系列的Buffer合成一个Buffer.

5 Buffer还有很多功能,比如从字节流中取出各种数据类型的值,buf.readUInt8()等。

 

网络编程

Node提供模块用于服务器和客户端:

net模块 ---TCP

dgram ---UDP

http ---HTTP

https --- HTTPS

net模块 + TCP

1.      net模块来创建TCP服务器端

var net = require(‘net’);

var server = net.createServer(function(socket){

 socket.on(‘data’, function(data){

       socket.write(‘hi’);

});

socket.on(‘end’, function(data){

       console.log(‘disconnected’);

});

Socket.write(“hello world!”);

});

 

Server.listen(8124, function(){

     Console.log(‘server found!’);

});

 

2.      TCP服务器的客户端: Telnet, Domain Socket,使用net模块自行构造客户端。

3.      TCP服务的服务器事件和连接事件 p/153

4.      TCP服务的Nagle算法:要求缓冲区的数据达到一定数量或者一定时间后才能将其发出,所以小数据包会被Nagle算法合并,以此来优化网络。在NodeTCP默认启用Nagle算法,可以使用socket.setNoDelay(true)去掉Nagle算法。

另外尽管在网络一端调用write()会触发另一端的data事件,但是并不意味着每次write()都会触发一次data事件,在关闭掉Nagle算法后,另一端可能会将接收到的多个小数据包合并,然后触发一次data事件。

dgram模块+UDP

1.      创建UDP服务器 p/154

var dgram = require(‘dgram’);

var server = dgram.createSocket(“udp4”);

server.on(“message”, ……)

……

Server.bind(41234);

2.      创建UDP客户端

var dgram = require(‘dgram’);

var client = dgram.createSocket(“udp4”);

client.send(……);

3.      UDP套接字事件p/155

http模块+HTTP

1.创建HTTP服务器

var http = require(“http”);

http.createServer(function(req,res){

  res.writeHead(200,{‘Content-Type’:’text/plain’});

  res.end(‘Hello world!’);

}).listen(1337,’127.0.0.1’);

 

2. HTTP服务与TCP服务模型有区别的地方在于,在开启keepalive后,一个TCP会话可以用于多次请求和响应。TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务。http模块即是将connectionrequest的过程进行了封装。

 

3.无论服务器在处理业务时是否发生异常,务必在结束时调用res.end()结束请求,否则客户端将一直处于等待状态。res.send()res.end()有封装,所以其后边不用再调用res.end().

4.HTTP服务器事件p/160

5.http模块实现HTTP客户端p/161

6.HTTP客户端事件p/163

WebSocket服务

WebSocket与传统的HTTP比有如下好处:

1.      客户端与服务器端只建立一个TCP连接,可以使用更少的连接。

2.      WebSocket服务器可以推送数据到客户端。

3.      有更轻量级的协议头,减少数据传送量。

 

尽管Node没有内置WebSocket库,但是社区的ws模块封装了WebSocket的底层实现。

Node在网络安全上提供了3个模块:crypto(主要用于加密解密, SHAI,MD5等算法都在其中有体现),tls(与net模块功能类似,只能用于创建TCP服务)和httpshttps就是工作在TLS/SSL上的HTTP)。

 

TLS/SSL

1.      TLS/SSL只用于传输层即TCP,不能用于应用层

2.      TLS/SSL的公钥/私钥结构:每个服务器和客户端都有自己的公私钥。公钥用来加密要传输的数据,私钥用来解密接收到的数据。公钥私钥是配对的,通过公钥加密的数据只有通过私钥才能解密,所以在建立安全传输之前,客户端和服务器端之间需要互换公钥。Node在底层采用的是openssl实现TLS/SSL,为此要生成公钥私钥可以通过openssl完成。

3.      TLS/SSL通过引入数字证书来确认收到的公钥来自服务器。数字证书需要向CA申请,需要费用。也可以通过openssl生成自签名证书p/171.

4.      通过tls模块来创建一个安全的TCP服务p/172

5.      通过tls模块来创建TCP客户端p/173

 

HTTPS

1.      HTTPS服务就是工作在TLS/SSL上的HTTP

2.      HTTPS的私钥和证书生成p/170-171

3.      通过https模块创建HTTPS服务器

var https = require(‘https’);

var fs = require(‘fs’);

var options = {

     key: fs.readFileSync(‘./keys/server.key’),

    cert: fs.readFileSync(‘./keys/server.crt’)

} ;

 

https.createServer(options, function(req,res){

  res.writeHead(200);

  res.end(“hello world\n”);

}).listen(8000);

4.      通过https模块实现HTTPS客户端p/175

 

构建Web

请求方法

Web请求方法是GETPOST,还有HEAD,DELETE,PUT,CONNECT等方法,存于req.method.

路径解析

req.url存放路径部分,通过url.parse(req.url).pathname来解析路径

查询字符串

即参数(问好后边的),通过url.parse(req.url,true).query来解析成JSON对象。

Cookie

用于标识和认证一个用户

Cookie的处理分为如下几步:

a.      服务器向客户端发送cookiep/183

b.     浏览器将cookie保存

c.      之后每次浏览器都会将cookie发向服务器端

Cookie存于req.headers.cookie.

Cookie的服务器端解析 p/181-182

Cookie的性能影响:由于cookie的实现机制,一旦服务器端向客户端发送了设置cookie的意图,除非cookie过期,否则客户端每次请求都会发送这些cookie到服务器,一旦设置的cookie过多,将会导致报头较大。大多数cookie并不要每次都用上,因为这样会造成带宽的浪费。

Session

Cookie体积过大,而且最严重的是cookie可以在前后端进行修改,因此数据容易被篡改和伪造。为了解决cookie的问题,产生了session. Session 的数据只保留在服务器端,客户端无法修改,这样数据的安全性得到了一定的保障,数据也无需在协议中每次都被传递。

Session的实现:p/185

Session会带来内存的增长,所以建议放在第三方高速缓存里。

缓存

l  条件请求(需要客户端和服务器交互)时间戳If-modified-since(客户端发给服务器)/Last-modified(服务器发给客户端)ETagIf-None-Match(客户端发给服务器)/ETag(服务器发给客户端))

l  响应头里设置(服务器端发给客户端):Expires(时间字符串)Cache-Control(倒计时)

 

POST DELETE PUT这类行为性质的请求操作一般不会做任何缓存,大多数缓存只应用在GET请求中。

条件请求

缓存的流程(p/191)是本地没有文件时,浏览器会请求服务器端的内容,并将这部分内容放置在本地的某个缓存目录中。在第二次请求时,它将对本地文件进行检查,如果不能确定本地这份文件是否可以直接使用,它将发起一次条件请求。

l  If-Modified-Since/Last-Modified:(实现p/191)就是在普通GET请求报文中,附带If-Modified-Since字段(req.headers中),向服务器询问是否有更新的版本,本地文件的最后修改时间。如果服务器端没有新的版本,只需响应304状态码,客户端就使用本地版本;如果服务器端有新的版本,就将新的内容发给客户端,同时通过Last-Modified值来告诉客户端最新更新时间。

缺点:时间戳改但内容不一定改;时间戳只能精确到秒级。

l  If-None-Match/ETag: (实现p/192)即通过一个散列值来看内容是否改变。

响应头里设置

1.      响应头里设置’Expires’(时间字符串)来让浏览器进行缓存,其缺陷是需要让浏览器和服务器时间同步。res.setHeader(“Expires”, expires.toUTCString());

2.      响应头里设定Cache-Control来让浏览器进行缓存:res.setHeader(“Cache-control”, “max-age=”+10*365…);

Basic认证

是当客户端与服务器进行请求时,允许通过用户名和密码实现的一种身份认证方式。该认证太多缺点,虽然经过加密,但是近乎明文,十分危险。一般只有在HTTPS情况下才会使用。

数据上传

指从客户端往服务器上传

1.      通过报头Transfer-EncodingContent-Length来判断请求中是否带有内容

2.      通过网页表单向服务器提交数据,请求头中Content-Type字段值为application/x-www-form-urlencoded.

通过JSON格式提交Content-Typeapplication/json(服务器处理p/196),.

通过XML格式提交为application/xml(服务器处理p/197).

3.      通过附件形式上传p/197.

4.      解决用户提交大数据量的方案

a.      限制上传内容的大小,一旦超过限制,停止接收数据,并响应400状态码。

b.     通过流式解析,将数据流导向到磁盘中,Node中只保留文件路径等小数据。

路由解析之MVC

路由解析,根据URL寻找对应的控制器和行为。

行为调用相关的模型,进行数据操作。

数据操作结束后,调用视图和相关数据进行页面渲染,输出到客户端。

路径解析之RESTful

RESTful的一种实现p/208-209.

中间件

中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。这里提到的中间件就是为我们封装上文提及的所有HTTP请求细节处理的中间件,开发者可以脱离这部分细节,专注在业务上。

页面渲染

浏览器通过不同的Content-Type的值来决定采用不同的渲染方式,这个值我们称为MIME值。

MIME:

JSON文件---application/json

XML文件---application/xml

PDF文件---application/pdf

Html文件---text/html

 

通过mime模块可以获得文件的MIME值。

 

响应报文头中的Content-Disposition字段影响的行为是客户端会根据他的值来判断是应该将报文数据当做即时浏览的内容,还是可附件下载。当内容需要即时查看时,他的值为inline,当数据可以存为附件时,他的值为attachment.

视图渲染

即给客户端返回页面.

在动态页面技术中,最终的视图是由模板和数据共同生成出来的。模板是带有特殊标签的HTML片段,通过与数据的渲染,将数据填充到这些特殊标签中,最后生成普通的带数据的HTML片段。通常我们将渲染方法设计为render(),参数就是模板路径和数据。现有的模板有ejsjade等。

Bigpipe

Facebook公司的前端加载技术,它的提出主要是为了解决重数据页面的加载速度的问题。

它的解决思路是将页面分割成多个部分,先向用户输出没有数据的布局(框架),将每个部分逐步输出到前端,再最终渲染填充框架,完成整个网页的渲染。

*短路原理:opt=opt||{} ---相当于if(!opt)opt ={}, 只要前边的为假,无论||后边是啥,都会返回后边的值;只要前边为真,无论||后边是啥,都不会考虑。

多进程

Node是单线程,只能利用一个核,所以为了充分利用多核CPU,需要添加多个子进程。

实现多进程有两种方式:

1.      使用child_process模块---过于底层。

2.      使用cluster模块。它是对child_process模块和net模块的封装。(推荐使用)

Child_process模块(了解)

1.      通过child_process.fork()os模块在主进程中复制子进程

var fork = require(‘child_process’).fork;

var cpus = require(‘os’).cpus();

for(var i = 0; i < cpus.length;i++){

   fork(‘./work.js’);  

}

注通过fork()复制的子进程需要至少30毫秒的启动时间和至少10M的内存。

2.      这就是主从模式(master-worker:主进程不负责具体的业务处理,而是负责调度或管理工作进程,它是趋于稳定的。工作进程负责具体的业务处理。

3.      child_process模块创建子进程4种方法:

spawn():启动一个子进程来执行命令。

exec():启动一个子进程来执行命令,它有一个回调函数来获知子进程的状况。

execFile():启动一个子进程来执行可执行文件。

fork():它创建一个子进程只需指定要执行的javascript文件模块即可。

4个方法创建子进程之后均会返回子进程对象。

4.      主进程通过调用返回的子进程的.on(‘message’,…)事件来接收子进程发来的数据,通过.send()向子进程发数据。子进程则通过process.on(‘message’,…)process.send()来接收主进程发来的数据和向主进程发数据。

5.      因为端口占用的问题,不能实现多个子进程监听同一个端口的想法,通常做法是让每个进程监听不同的端口,主进程监听主端口,然后将请求代理到不同的端口上。

6.      通过send()直接发送socket给子进程,实现多个子进程监听同一个端口,但这种方式只能用在TCP,UDP服务。

7.      父进程能侦听到的子进程事件p/248.

8.      父进程通过kill()向子进程发送SIGTERM终止信号,而子进程在收到该信号后退出。

9.      如果子进程异常,则可以通过捕获uncaughtException事件来退出。这样会有exit事件发给主进程p/251.

10.  自杀信号:工作进程在得知要退出时向主进程发送一个自杀信号,然后停止接收新的连接,当所有连接都断开后才退出。主进程在接收到自杀信号后,立即创建新的工作进程,以保证异常进程在退出之前已经有新的进程来代替它。

子进程之间通过Redis共享数据

各个子进程之间共享数据可用缓存Redis。实现同步有两种方式:

1.      各个子进程向Redis轮询。

2.      启动一个通知子进程,只负责轮询通知。

Cluster模块

1.      通过cluster模块来创建子进程

var cluster = require(‘cluster’);

var http = require(‘http’);

var numCPUs = require(‘os’).cpus().length;

 

if(cluster.isMaster){

for(var I = 0; I < numCPUs; i++){

       cluster.fork();

}

  cluster.on(‘exit’, function(worker,code,signal){

     console.log(‘worker’ + worker.process.pid + ‘died’);

  });

}else{

     http.createServerfunction(req.res){

           res.writeHead(200);

           res.end(“hello world!”);

     }.listen(8000);

}

2.      通过cluster创建的子进程可以共享监听一个端口!主线程会将请求随机分发给某个子进程。

3.      在进程中判断是主进程还是工作进程,主要取决于环境变量中是否有NODE_UNIQUE_ID

Cluster.isworker = (‘NODE_UNIQUE_ID’ in process.env);

Cluster.isMaster = (cluster.isworker ===false);

4.      Cluster模块实际上是child_process模块和net模块的封装。

5.      Cluster模块暴露出的事件:

fork:复制一个工作进程后触发该事件。

online:复制好一个工作进程后,工作进程主动发送一条online消息给主进程,主进程收到消息后,触发该事件。

listening工作进程中调用listen()后,发一条listening消息给主进程,主进程收到消息后,触发该事件。

disconnect:主进程和工作进程之间IPC通道断开后会触发该事件。

exit:有工作进程退出时触发该事件。

setup: cluster.setupMaster()执行后触发该事件。

 

测试

单元测试

NodeJS中使用assert模块, should.js, expectchain等模块进行断言。

Assert模块的断言方法p/255

mocha框架

使用mocha框架可以对TDDBDD都支持。通过npm install mocha来安装。

1.      BDD(行为驱动开发,关注整体行为是否符合预期)在mocha中的支持

主要采用describeit进行组织。describe可以描述多层级结构,具体到测试用例时用it.另外还有before, after, beforeEach, afterEach4个钩子方法。

       

describe表示一组相关的测试,它是一个函数,参数1是测试套件名称,参数2是实际执行的函数。

it:测试用例,表示一个单独的测试。是测试的最小单位。也是函数,参数1是用例名称,参数2是实际执行的函数。

测试脚本里应该包括一个或者多个describe,每个describe应包括一个或者多个it.

before/after:写在describe内,在所有用例之前/之后执行,是一个函数。

beforeEach/afterEach:写在describe内,是一个函数,在每个测试用例之前/之后执行。

2.      TDD(测试驱动开发,关注所有功能是否被正确实现)在mocha中的支持

Mocha框架TDD测试使用suitetest组织用例;钩子函数包括setupteardown.

 

 

 

 

 

 

 

 

 

    

 

3.      使用mocha –R <reporter>来指定报告格式

4.      执行测试脚本三种方式:

“mocha xxx.js xxx.js” ---进入脚本所在文件夹后执行该命令(一般都在test文件夹下)

“mocha” ---默认运行Test目录里的所有脚本(仅第一层)。

“mocha – recursive” ---test目录下的所有用例,包括深层文件夹下的。

mocha允许在test目录下放置mocha.opts,把命令行参数写在里面,这样只需运行“mocha”即可执行该命令行参数。

5.      异步测试:通过在用例的参数里面传入done()实参,来实现异步测试,即在断言判定完之后调用done()来告知测试框架当前用例完成。p/268例子

6.      对测试用例设定超时:

a.      mocha –t <ms>:设定所有用例的超时时间(默认2000ms

b.     this.timeout(ms):如果在it中设定则为该用例的超时时间;如果在describe中设定则为该describe中所有用例的超时时间。

如果一个用例的执行时间超过了预期时间,则会记录下超时错误,然后执行下一个测试用例。

7.      Mock

a.      before()/after()beforeEach()/afterEach()中使用临时缓存来mock

describe(“getContent”,function(){

var _readFileSync;

 before(function(){

     _readFileSync = fs.readFileSync;

    Fs.readFileSync = function(filename, encoding){

             throw new Error(“mock reafFileSync error”);

    };

});

// it() ….

after( function(){

    fs. readFileSync = _ readFileSync;
    })

});

b.     before()/after()beforeEach()/afterEach()中使用muk模块实现mock p/274

c.      Mock时要注意不要将异步模拟成同步

fs.readFile = function(filename, encoding, callback){

     callback(new Error(“mock reafFile error”));

} ------同步

正确的模拟异步用process.nextTick()

fs.readFile = function(filename, encoding, callback){

  process.nextTick(function(){

      callback(new Error(“mock reafFile error”));

});

} ------异步

8.      通过rewire模块实现对私有方法的测试。

使用jscover模块来检测测试覆盖率(依赖java编译环境)

也可以使用blanket模块统计测试覆盖率(比jscover要好,且方便p/272

性能测试

基准测试:要统计的就是在多少时间内执行了多少次某个方法。为了增强可比性,一般会以次数作为参照物,然后比较时间,以此来判别性能的差距。使用benchmark模块进行基准测试。

 

压力测试工具:ab, siege, http_load, jmeter等。

 

通常在进行实际的功能开发之前,我们需要评估业务量,以便功能开发完成后能够胜任实际的在线业务量。

 

产品化

构建工具

构建工具完成的功能主要是合并静态文件,压缩文件大小,打包应用,编译模块等。Grunt是跨平台的构建工具,它通过Node写成,借助Node的跨平台能力,实现了很好的平台兼容性。

代码检测工具JSLint

可以通过gitlab等开源工具搭建了内部的代码托管平台。

v Node提升性能的4个方法:

1.      动静分离:node处理静态文件的能力不算突出。将图片,脚本,样式表和多媒体等静态文件都导入到专业的静态文件服务器(比如Nginx)上,node只处理动态请求即可。

2.      启用缓存RedisMemcached.

3.      多进程架构– cluster模块。

4.      数据库读写分离:就任意数据库而言,读取的速度远远高于写入的速度。而某些数据库在写入时为了保证数据一致性,会进行锁表操作,这同时会影响到读取的速度。为了提升性能,通常会进行数据库的读写分离,将数据库进行主从设计。这样读数据操作不再受写入的影响了。

日志

node中可用connect提供的日志中间件来记录访问日志,当然我们可以在Nginx反向代理里记录访问日志。

异常日志的实现p/296-298

Node通过nodemailer模块实现邮件报警。

 

调试NodeJS

Node调试可用三种方法:Debugger, console.log()Node-Inspector

Debugger:需要通过debugger;语句在代码中设置断点。使用方法p/310 - 311

Node-Insepector:可在浏览器中进行调试。

我一般用console.log()

 

编程规范

缩进:采用两个空格而不是Tab

变量声明:永远用var声明变量

空格:操作符前后需要加空格

单双引号的使用:尽量使用单引号,这样无需转译

大括号的位置:大括号{无需另起一行

逗号:如果逗号不在行结尾,后面需要一个空格

分号:给表达式结尾添加分号

变量命名:小驼峰式

方法命名:小驼峰式

类命名:大驼峰式

常量命名:所有字母大写,以下划线分割

文件命名:采用下划线分割单词

其它编码规范参见p/315 - 321