Nodejs Node Cookbook(英文)读后感

发布于 1周前 作者 gougou168 来自 nodejs/Nestjs

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/


4 回复

Nodejs Node Cookbook(英文)读后感

最近读了几本node.js的教程,其中《Node Cookbook》这本书给我留下了深刻的印象。这本书不仅提供了丰富的实践经验,还对安全性和扩展性进行了深入探讨,非常值得一读。下面我将结合书中的一些关键点,分享我的学习体会。

1. eventloop

在Node.js中,很多异步操作依赖于事件循环(event loop)。例如,在下面的代码中,setTimeout函数虽然被调用,但由于事件循环的机制,console.log('done')永远不会被执行:

const EE = require('events').EventEmitter;
const ee = new EE();
let die = false;

ee.on('die', () => {
  die = true;
});

setTimeout(() => {
  ee.emit('die');
}, 100);

while (!die) {}

console.log('done');

这段代码展示了Node.js在同一时刻只能执行一个任务的特点。因此,我们应尽量使用非阻塞的I/O操作,以确保事件循环能够正常工作。

2. 异步错误处理

在处理异步操作时,如HTTP请求,我们需要注意错误处理的方法。例如,在下面的代码中,尝试捕获错误的try-catch块并不会生效,因为http.get返回的是一个http.ClientRequest对象而不是错误对象:

const http = require('http');

const opts = {
  host: 'sfnsdkfjdsnk.com',
  port: 80,
  path: '/'
};

try {
  http.get(opts, (res) => {
    console.log('Will this get called?');
  });
} catch (e) {
  console.log('Will we catch an error?');
}

正确的错误处理应该通过监听error事件来完成:

const req = http.get(opts, (res) => {
  console.log('Response received.');
});

req.on('error', (err) => {
  console.error(`Error: ${err.message}`);
});

3. 使用stream优化性能

在处理文件传输时,利用流(stream)可以显著提高性能。传统方式是将文件全部读入内存后再发送,这种方式在处理大文件时会消耗大量内存。利用流的方式可以边读边发送,从而节省资源:

const fs = require('fs');
const http = require('http');

http.createServer((req, res) => {
  const s = fs.createReadStream('./DSC_0004.JPG');
  s.on('open', () => {
    res.writeHead(200, { 'Content-Type': 'image/jpeg' });
    s.pipe(res);
  });
  s.on('error', (err) => {
    console.error(err);
    res.writeHead(500);
    res.end('Server Error!');
  });
}).listen(3000);

4. 相对路径漏洞

如果不使用Express等开发框架,自行搭建Web服务器时,容易出现相对路径漏洞。例如,如果用户请求了/static/.../app.js,可能会导致敏感文件暴露。因此,我们应该使用path.normalize来处理路径:

const url = require('url');
const path = require('path');
const fs = require('fs');

http.createServer((req, res) => {
  const pathname = url.parse(req.url).pathname;
  const filepath = path.join(__dirname, 'static', path.normalize(pathname));

  fs.readFile(filepath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end('Not Found');
    } else {
      res.writeHead(200);
      res.end(data);
    }
  });
}).listen(3000);

5. POST数据安全

对于POST请求的数据,应该增加长度检查,以防止恶意用户伪造请求头中的Content-Length字段。同时,在数据接收完毕后,也需要检查数据内容,避免服务器被击溃:

const http = require('http');
const querystring = require('querystring');

http.createServer((req, res) => {
  let body = [];
  req.on('data', (chunk) => {
    body.push(chunk);
  });

  req.on('end', () => {
    body = Buffer.concat(body).toString();
    if (body.length > 1024) {
      res.writeHead(400);
      res.end('Bad Request');
      return;
    }

    const postData = querystring.parse(body);
    // 处理postData
    res.end('Data received');
  });
}).listen(3000);

6. CSRF防护

虽然使用_csrf隐藏字段可以提供一定的保护,但并不能完全防御CSRF攻击。更安全的做法是引入验证码机制:

<form action="/submit" method="POST">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <!-- 其他表单字段 -->
  <button type="submit">Submit</button>
</form>

7. 利用Buffer优化性能

在处理文件读取时,直接使用buffer.copy方法比直接拼接字符串更高效。以下是两种方法的性能对比:

// 使用buffer.copy
const fs = require('fs');

function streamtread(end) {
  const f = './word.txt';
  const stats = fs.statSync(f);
  const buf = new Buffer(stats.size);

  const s = fs.createReadStream(f).once('open', () => {
    let offset = 0;
    s.on('data', (chunk) => {
      chunk.copy(buf, offset);
      offset += chunk.length;
    });
  }).once('end', () => {
    if (--end >= 0) streamtread(end);
  });
}

console.time('streamtest');
streamtread(1000);

通过上述示例代码和分析,我们可以更好地理解和应用Node.js的最佳实践,从而编写出更加健壮和高效的代码。


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

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

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

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

《Node.js Node Cookbook》是一本非常实用的书籍,它不仅提供了大量的代码示例,还涵盖了Node.js编程中的一些最佳实践。下面我将根据该书的一些关键点进行总结,并提供相应的示例代码。

1. Event Loop 和 异步编程

Node.js 使用单线程模型,所有的操作都在同一个事件循环中运行。了解这一点对于编写高效的异步代码至关重要。例如,setTimeoutsetInterval 都是在事件循环的不同阶段执行的。

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开发者阅读。通过上述示例代码,你可以更好地理解和应用这些知识。

回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!