登录
注册
node.js 学习社区
Node Cookbook 读后感

糖果花瓣流浪

2014-12-14 09:07

最近读了几本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 & lt; 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(p) ;将../等等一些不正常的字符过滤掉,这样就可以保证我们的系统安全了。
注意:这里不要用浏览器测试,有些浏览器会自动帮你做 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/

回复 · 0

发表回复

你可以在回复中 @ 其他人