Freud's Blog

Stay hungry, stay foolish. 少年辛苦终身事,莫向光阴惰寸功。

Node.js-(二)-进阶

Posted on By Freud Kang

Connect插件

Connect自身十分简单, 是Node的一个中间件框架,其作用是基于Web服务器做中间件管理。至于如何如何处理网络请求,这些任务通过路由分派给管理的中间件们进行处理。它的处理模型仅仅只是一个中间队列,进行流式处理而已,流式处理可能性能不是最优,但是却是最易于被理解和接受。基于中间件可以自由组合和插拔的情况,优化它十分容易。其中Express就是依赖Connect创建而成的。

  • 中间件

Connect的中间件扮演的是跟http模块一样的角色,处理请求,然后响应客户端或是让下一个中间件继续处理。如下是一个中间件最朴素的原型:

function (req, res, next) {
	// 中间件
}

在中间件的上下文中,有着三个变量。分别代表请求对象、响应对象、下一个中间件。如果当前中间件调用了res.end()结束了响应,执行下一个中间件就显得没有必要。

  • 流式处理

为了演示中间件的流式处理,我们可以看看中间件的使用形式:

var app = require(connect)();

// Middleware
app.use(connect.staticCache());
app.use(connect.static(__dirname + '/public'));
app.use(connect.session());
app.use(connect.bodyParser());
app.use(function (req, res, next) { 
	// 中间件
});

app.listen(3001);

Conncet提供use方法用于注册中间件到一个Connect对象的队列中,我们称该队列叫做中间件队列。

Connector

Connect通过use方法来维护一个中间件队列。然后在请求来临的时候,依次调用队列中的中间件,直到某个中间件不再调用下一个中间件为止。

值得注意的是,必须要有一个中间件调用res.end()方法来告知客户端请求已被处理完成,否则客户端将一直处于等待状态。

  • 路由

其实app.use()方法接受两个参数,route和fn,既路由信息和中间件函数,一个完整的中间件,其实包含路由信息和中间件函数。路由信息的作用是过滤不匹配的URL。请求在遇见路由信息不匹配时,直接传递给下一个中间件处理。

通常在调用app.use()注册中间件时,只需要传递一个中间件函数即可。实际上这个过程中,Connect会将/作为该中间件的默认路由,它表示所有的请求都会被该中间件处理。

中间件的优势类似于Java中的过滤器,能够全局性地处理一些事务,使得业务逻辑保持简单。

任何事物均有两面性,当你调用app.use()添加中间件的时候,需要考虑的是中间件队列是否太长,因为每一层中间件的调用都是会降低性能的。为了提高性能,在添加中间件的时候,如非全局需求的,尽量附带上精确的路由信息。

app.stack = [];
app.use = function(route, fn){
	// …
	// add the middleware
	debug('use %s %s', route || '/', fn.name || 'anonymous');
	this.stack.push({ route: route, handle: fn });
	return this;
};
  • 静态文件中间件
var connect = require('connect');  
var app = connect(); 
app.use(connect.static(__dirname + '/public')); 
  • 缓存策略
app.use('/public', connect.static(__dirname + '/public', {maxAge: 86400000})); 

maxAge默认是0,即不缓存,单位为毫秒,以上设置为10年。

FileSystem的问题Buffer

在计算机内部,一个中文占2个字节,一个英文占1个字节。

JavaScript中对字符串长度的判断,这个length属性对调用者而言是相当友好的。

console.log("0123456789".length); // 10
console.log("零一二三四五六七八九".length); /10
console.log("\u00bd".length); // 1

但是当读取文本文档的时候

var fs = require('fs');
var rs = fs.createReadStream('testdata.text',{bufferSize: 11}); 
var data = '';
rs.on("data", function (trunk){
    data += trunk;
});
rs.on("end", function () {
    console.log(data);
});

如果这个文件读取流读取的是一个纯英文的文件,这段代码是能够正常输出的。但是如果我们读取的是中文,由于BufferSize是奇数,那就会出现一个字符被分配在两个trunk中的场景。输出就会变为乱码。

解决办法是设置编码格式为UTF-8,使中文字符自动补齐。

var rs = fs.createReadStream('testdata.text', {encoding: 'utf-8', bufferSize: 11}); 

在NodeJs中仅支持如下编码:utf8, ucs2, ascii, binary, base64, hex,并不支持中文GBK或GB2312之类的编码,如果需要读取GBK等编码,需要用到其他插件,比如iconv-lite

NodeJs中子进程管理

当想在Node中简单地执行一个外部命令,并让Node获取命令的返回值时。比如,执行一个UNIX命令、脚本或者其他那些不能在Node里直接执行的命令。可以使用Node的子进程

var child_process = require(child_process')
var exec = child_process.exec;

var options ={
	timeout: 1000,
	killSignal: ‘SIGKILL' 
};

exec(command,options,callback);

exec的第一个参数是准备执行的shell命令字符串,第二个参数是一个回调函数。这个回调函数将会在exec执行完外部命令或者有错误发生时被调用。回调函数有三个参数:error,stdout,stderr,看下面的例子:

exec(ls',function(err,stdout,stderr){
	//execute
});

如果有错误发生,第一个参数将会是一个Error类的实例,如果第一个参数不包含错误,那么第二个参数stdout将会包含命令的标准输出。最后一个参数包含命令相关的错误输出。

Options 包含的内容:

  • cwd —— 当前目录,可以指定当前工作目录。
  • Timeout以毫秒为单位的命令执行超时时间,默认是0,即无限制,一直等到子进程结束。
  • Encoding子进程输出内容的编码格式,默认值是”utf8”,也就是UTF-8编码。如果子进程的输出不是utf8,你可以用这个参数来设置,支持的编码格式有:
  • maxBuffer指定stdout流和stderr流允许输出的最大字节数,如果达到最大值,子进程会被杀死。默认值是200*1024。
  • killSignal当超时或者输出缓存达到最大值时发送给子进程的终结信号。默认值是“SIGTERM”,它将给子进程发送一个终结信号。通常都会使用这种有序的方式来结束 进程。当用SIGTERM信号时,进程接收到以后还可以进行处理或者重写信号处理器的默认行为。如果目标进程需要,你可以同时向他传递其它的信号(比如 SIGUSR1)。你也可以选择发送一个SIGKILL信号,它会被操作系统处理并强制立刻结束子进程,这样的话,子进程的任何清理操作都不会被执行。

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

  • Javascript的垃圾回收
    • JavaScript使用垃圾回收机制来自动管理内存,ECMAScript没有暴露任何垃圾回收器的接口。我们无法强迫其进行垃圾回收,更无法干预内存管理
  • V8限制
    • Node基于V8设计,与其他语言不同的一个地方,就是其限制了JavaScript所能使用的内存(64位为1.4GB,32位为0.7GB),这也就意味着将无法直接操作一些大内存对象。
    • 当然这个限制是可以打开的,类似于JVM,我们通过在启动node时可以传递–max-old-space-size或–max-new- space-size来调整内存限制的大小,前者确定老生代的大小,单位为MB,后者确定新生代的大小,单位为KB。这些配置只在V8初始化时生效,一旦 生效不能再改变
  • V8堆构成
    • 新生代内存区:大多数的对象被分配在这里,这个区域很小但是垃圾回特别频繁 老生代指针区:属于老生代,这里包含了大多数可能存在指向其他对象的指针的对象,大多数从新生代晋升的对象会被移动到这里
    • 老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针*
    • 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象*
    • 代码区:代码对象,也就是包含JIT之后指令的对象,会被分配在这里。唯一拥有执行权限的内存区*
    • Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每个区域都是存放相同大小的元素,结构简单
  • 垃圾回收机制
    • 自动垃圾回收算法的演变过程中出现了很多算法,但是由于不同对象的生存周期不同,没有一种算法适用于所有的情况。所以V8采用了一种分代回收的策略,将内 存分为两个生代:新生代和老生代。新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用不同的垃 圾回收算法来提升垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象满足某些条件(后面会有介绍)时,会被移动到老生代(晋升)

NodeJS内存泄漏监控插件

Express

基于Connector中间件的Web框架,做网站用

官网http://www.expressjs.com.cn/

Socket IO

消息推送框架,通过建立长连接进行消息推送。

官网http://socket.io/

Express和ejs使用特别注意:

  • BodyParser已经被移出Express单独成立一个项目,需要单独安装

通常 POST 内容的格式是 application/x-www-form-urlencoded, 因此要用下面的方式来使用:

app.use(require('body-parser').urlencoded({extended: true}))
  • 与ejs结合的时候,Layout被移出Express,需要单独安装
npm install express-partials
var partials = require('express-partials');

app.set('view engine', 'ejs'); 下面添加 app.use(partials()); 在需要引用模板的地方调用 layout:’模版名称’ 示例

app.get('/reg', function (req, res) {
	res.render('reg', {
		title: '用户注册',
		layout: 'template'
	});
});

ejs文件中定义<%-body%>就可以了



参考资料

W3cschool: http://www.w3cschool.cc/nodejs/nodejs-tutorial.html

InfoQ: http://www.infoq.com/cn/articles/what-is-nodejs

NodeJs官网:http://nodejs.org/

ByVoid: 《node.js开发指南》

田永强:《深入浅出nodejs》

浅谈V8引擎中的垃圾回收机制: http://www.html-js.com/blog/2514