Node Cookbook(英文)读后感
发布于 2年前 作者 DoubleSpout 4789 次浏览

最近读了几本node.js的教程,有些段落还是写的不错的,想写篇文章总结下。《Node Cookbook》这本node.js教程是我读到目前为止最好的系统教程,很多实践经验以及一些对于安全和扩展性的讨论,真心不错。

1、eventloop 在node中很多异步的操作都需要回调来执行,他们有些处于相同的eventloop中,有些处于不同的eventloop中,我们需要把这些事情搞清楚才能更好的控制程序运行的流程。我们先看如下简单的代码:

EE = require('events').EventEmitter;
ee = new EE();
die = false;
ee.on('die', function() {
die = true;
});
setTimeout(function() {
ee.emit('die');
}, 100);
while(!die) {
}
console.log('done');

执行的结果就是 done 永远不会被打印出来,因为node在同一时刻只能做一件事情,所以settimeout永远没有机会执行。所以我们在事件驱动的项目中尽量的使用无阻塞的I/O操作。 我们再来看下一段代码:

var http = require('http')
var opts = {
host: 'sfnsdkfjdsnk.com',
port: 80,
path: '/'
}
try {
http.get(opts, function(res) {
console.log('Will this get called?')
})
}
catch (e) {
console.log('Will we catch an error?')
}

我们向一个不存在的域名发送了一条http的get请求,当然肯定会抛出异常,这时try和catch并没有能帮助我们捕获这个异常,因为http.get返回的不是一个error对象,而是http.ClientRequest对象。所以异步的错误不能用try和catch来捕获。 关于emit触发器,我们可以通过 emitter.addListener 来添加事件监听器,当我们为某一个事件添加了多个事件监听器后node是这样触发他们的.

EventEmitter.prototype.emit = function(type) {
...
var handler = this._events[type];
...
} else if (isArray(handler)) {
var args = Array.prototype.slice.call(arguments, 1);
var listeners = handler.slice();
for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].apply(this, args);
}
return true;
...
};

所以一旦我们在某一个监听器中throw了error,则接下来的监听器将不会再执行了,这个需要我们注意一下。

2、response.finished 在cookbook上用到了这个api,后来看了node源码发现这个属性是用来是否执行过response.end()方法的,个人不建议使用它,因为node官方api没有这个属性,可能以后随时改掉而不通知开发者,这样会使你的程序随着node版本的升级可能无法使用

3、Optimizing performance with streaming 利用streaming优化文件的传输,传统的做法是 服务端将文件整个读取到内存中,然后再发送到客户端,而现在我们可以利用streaming来优化它,使它性能更高。简单的理解就是利用node做一个管道,客户端假设为一个空盆而我们的file文件为一个池塘,池塘通过管道慢慢的注满整个脸盆,而不用node先读取再发送,我们看它是如何做到的:

var s = fs.createReadStream(filepath).once('open', function () {
response.writeHead(200, headers);
this.pipe(response);
}).once('error', function (e) {
console.log(e);
response.writeHead(500);
response.end('Server Error!');
});

核心代码就是上面的这段,利用pipe方法将2个stream的实例连接起来,这样一边从file读取文件一边就可以响应给客户端了。但是这样真能提升效率吗?我在本地的虚拟机做了一个小小的压力测试,输出一张5mb的单反照片,我们先看测试代码: A、没有用stream

var http = require('http');
var fs = require('fs');
http.createServer(function (request, response) {
 
 fs.readFile('./DSC_0004.JPG', function(err,data){
  if(err){
    console.log(e);
    response.writeHead(500);
    response.end('Server Error!');
    return;
  }
  response.writeHead(200, {'Content-Type': 'image/jpeg'});
  response.end(data);
 }) 
}).listen(3000);

B、利用stream

var http = require('http');
var fs = require('fs');
http.createServer(function (request, response) {
 
var s = fs.createReadStream('./DSC_0004.JPG').once('open', function () {
 response.writeHead(200, {'Content-Type': 'image/jpeg'});
 this.pipe(response);
}).once('error', function (e) {
  console.log(e);
  response.writeHead(500);
  response.end('Server Error!');
});
}).listen(3000);

压测环境,虚拟机linux2.6.8,2cpu,256MB内存,测试工具ab,语句 ab -c 10 -n 50 http://192.168.11.66:3000/ 测试结果让我大跌眼镜: A、不用stream:Requests per second: 33.53 [#/sec] (mean) B、利用stream:Requests per second: 1.53 [#/sec] (mean) 所以尽信书不如无书,当只有传输超大文件,并且并发量较小的时候stream才有优势,所以如果你的服务是做web的供很多人访问的,还是不建议去利用stream.pipe()来直接输出。

4、相对路径漏洞 如果不用express等开发框架,自己建立web服务器很容易遗漏掉“相对路径”这个漏洞。比如我们规定"./static"是静态文件列表,所有访问hostname/static/xxx.jpg等的请求都将直接去static文件夹寻找,如果找到则直接输出这个文件,如果没有找到则返回404,所以这里我们的代码可能如下,伪代码:

var path = require('url').parse(request.url).pathname;
fs.readFile(__dirname+path, function (err, data) {
  if (err) throw err;
  response.end(data);
});

这样存在着一个漏洞,如果用户请求了/static/…/app.js,我们的拼接后的路径就成了:/usr/local/node/app/static/…/app.js,这样我们就把我们的启动文件app.js发送给了用户,用户可以通过这样的方法直接拿到我们项目的所有文件,而且如果我们用 root 权限启动node.js的话整个系统的文件恶意用户都可以看到了。 所以我们必须处理一下path变量,利用这个方法 path.normalize§ ;将…/等等一些不正常的字符过滤掉,这样就可以保证我们的系统安全了。 注意:这里不要用浏览器测试,有些浏览器会自动帮你做 normalize ,可以下载一个发包软件进行测试。

5、post数据安全 对于post的接受数据的触发器,应该增加一个length判断,防止恶意用户伪造一个小的请求头的content-length,而发送很大的数据。同时也需要在end触发器时判断下post的内容是否为空,防止击溃服务器。

6、关于csrf 书中利用表单和session的_csrf随机串来抵御一般的csrf攻击,其实这样是没多大用处的,先简单说一下_csrf隐藏文本域的原理吧。服务端为每一个session生成一个唯一随机的加密串,然后当有post提交时服务端都会去验证提交上来的加密串和session中的加密串是否相同,这个加密串就是_csrf。我们可以在很多php框架中看到有这类的隐藏文本框。 但是_csrf并不能够真正完全的抵御csrf攻击,因为用户拿到了session或者有权限去注入脚本的话,就也可以拿到这个_csrf加密串了,所以从根本上抵御csrf攻击就只剩下验证码了。

7、利用buffer接受文件的技巧 我们习惯用buff直接相加,比如:

var buf;
data.on('data', function(err, buffer){
 buf += buffer;
})

然后再end的时候拿到我们的buf,其实这样做每次都会进行一次buffer.toString()的操作,会影响效率,下面我们用另外的方式实现,测试一下2者的性能到底差多少。 测试机器还是我的虚拟机,测试代码1,利用buffer.copy方法直接连接不通过tostring();

var fs = require('fs');
var f = './word.txt';
function streamtread(end){
 var buf;
 var s = fs.createReadStream(f).once('open', function () {
 }).once('end', function(){
  if(!end) return console.timeEnd('streamtest');
  streamtread(--end);
 });
 fs.stat(f, function(err, stats) {
  var bufferOffset = 0;
  buf = new Buffer(stats.size);
  s.on('data', function (chunk) {
   chunk.copy(buf, bufferOffset);
   bufferOffset += chunk.length;
  });
 });
}
 console.time('streamtest');
 streamtread(1000);

以上代码利用buffer.copy获得word.txt文本内的数据,word.txt是一个大小为3.16MB的文件。执行1000次读取后所花时间为: streamtest: 33479ms 下面我们将直接buf相加,我们看下代码:

var fs = require('fs');
var f = './word.txt';
function streamtread(end){
 var buf='';
 var s = fs.createReadStream(f).once('open', function () {
 }).once('end', function(){
  if(!end) return console.timeEnd('streamtest');
  streamtread(--end);
 });
 fs.stat(f, function(err, stats) {
  s.on('data', function (chunk) {
   buf += chunk;
  });
 });
}
 console.time('streamtest');
 streamtread(1000);

streamtest: 90536ms 差别不多接近3倍的差距啊,而且这样也可以解决什么utf-8转gb2312神马的bug。

先写这么多吧,以后想到再完善吧

原文地址:http://snoopyxdy.blog.163.com/blog/static/60117440201283112858425/

3 回复

第一段代码后面这一句: “因为node在同一时刻只能做一件事情,所以settimeout永远没有机会执行。”

我想知道是setTimeout没有机会执行还是setTimeout的回调函数没有机会执行?

还有,如果把console.log写在setTimeout回调函数里面console.log会执行吗?

应该是 回调函数没机会执行

@saber @justfly 是的一下代码不会执行,我写的有点歧义啊:

ee.emit('die');
回到顶部