Nodejs Node Cookbook(英文)读后感
Nodejs Node Cookbook(英文)读后感
最近读了几本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/
第一段代码后面这一句: “因为node在同一时刻只能做一件事情,所以settimeout永远没有机会执行。”
我想知道是setTimeout没有机会执行还是setTimeout的回调函数没有机会执行?
还有,如果把console.log写在setTimeout回调函数里面console.log会执行吗?
应该是 回调函数没机会执行
《Node.js Node Cookbook》是一本非常实用的书籍,它不仅提供了大量的代码示例,还涵盖了Node.js编程中的一些最佳实践。下面我将根据该书的一些关键点进行总结,并提供相应的示例代码。
1. Event Loop 和 异步编程
Node.js 使用单线程模型,所有的操作都在同一个事件循环中运行。了解这一点对于编写高效的异步代码至关重要。例如,setTimeout
和 setInterval
都是在事件循环的不同阶段执行的。
const EE = require('events').EventEmitter;
const ee = new EE();
let die = false;
ee.on('die', () => {
die = true;
});
setTimeout(() => {
ee.emit('die');
}, 100);
// 此处会阻塞事件循环,导致 setTimeout 无法执行
while (!die) {}
console.log('done'); // 不会被打印
正确的做法应该是使用非阻塞的 I/O 操作:
const http = require('http');
http.get({
host: 'nonexistentdomain.com',
port: 80,
path: '/'
}, (res) => {
console.log('Will this get called?'); // 不会被调用
}).on('error', (e) => {
console.error('Error:', e.message); // 捕获错误
});
2. response.finished 属性
尽管这本书提到了 response.finished
属性,但这是不推荐使用的,因为它不是Node.js的官方API,可能会在未来被移除。更推荐的做法是使用 response.end()
方法来确保响应完成。
3. 利用 Stream 优化性能
流(Stream)是一种高效的数据传输方式,适合处理大量数据。以下是一个简单的示例:
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
const s = fs.createReadStream('./DSC_0004.JPG');
s.once('open', () => {
res.writeHead(200, { 'Content-Type': 'image/jpeg' });
s.pipe(res);
}).once('error', (e) => {
console.error(e);
res.writeHead(500);
res.end('Server Error!');
});
}).listen(3000);
4. 相对路径漏洞
处理相对路径时,应该使用 path.normalize()
方法来避免路径遍历攻击。
const path = require('path');
const fs = require('fs');
const urlPath = '/static/../app.js';
const normalizedPath = path.normalize(__dirname + urlPath);
fs.readFile(normalizedPath, (err, data) => {
if (err) throw err;
// 处理文件内容
});
5. POST 数据安全
接收POST数据时,应该检查数据长度,以防止恶意用户的滥用。
let body = [];
req.on('data', (chunk) => {
if (body.length > 1e6) { // 防止内存溢出
req.connection.destroy();
}
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body);
if (body.length === 0) {
console.error('Empty POST request');
} else {
// 处理数据
}
});
总结
这本书详细地介绍了Node.js的各种编程技巧和最佳实践,非常适合Node.js开发者阅读。通过上述示例代码,你可以更好地理解和应用这些知识。