自己写nodejs也有一段时间,踩过很多坑(而且大部分是自己给自己埋),也见过很多别人踩过的坑,原因其实也很简单,要么是对这个知识点理解不够深入,要么就是编码的习惯不好。这段响应朋春大牛的号召,打算陆陆续续整理下这些坑,算是给自己一个备忘,同时也希望能对大家有所帮助。

    1. callback

    事件回调是nodejs非常常见的一个应用场景,那大家先来看看以下这段代码是否存在什么问题?

get(params, function(err, data) {
  if (err) {
    callback(err);
  }
  //对data进行操作
  var row = data[0];
});

     看出来了吧。对,就是err存在时,callback之后,接下来的代码还是要执行的。而这时,data值是什么,我们往往是没办法控制的。如果data这时返回的是undefined,那么就悲剧了,程序肯定报错。当然解决方法很简单,就是在callback之前加个return即可:

get(params, function(err, data) {
  if (err) {
    return callback(err);
  }
  //对data进行操作
  var row = data[0];
});

    这个知识点并不是很难,但往往是初学者特别容易犯的错,甚至已经写了很久代码的同学也会偶尔犯这种低级错误。

    再来一个更隐蔽的:

db.get(key, function(err, data) {
  if (err) {
    return callback(err);
  }
  try {
    callback(null, JSON.parse(data.toString()))
  } catch(e) {
    callback(e);
  }
});

     看似没有任何问题吧。嘿嘿,揭晓答案,当data为undefined或者不是个json数据时,情况会怎么样?对,被回调两次。callback(null, Error)一次,callback(e),这在一个大项目绝对是坑爹了,排错都需要很久。

 

       2. buffer

      还是老规矩,先看代码:

var data = "";  
res.on('data', function (chunk) {  
  data += chunk;  
})  
.on("end", function () {  
  //对data转码  
}); 

    这段代码在chunk都是ascii码数据或者数据量比较少时是没有问题,但如果你的数据是大量中文的话,恭喜你,中枪了,会出现乱码。其原因是两个chunk(Buffer对象)的拼接并不正常,相当于进行了buffer.toString() + buffer.toString()。如果buffer不是完整的,则toString出来后的string是存在问题的(比如一个中文字被截断)。具体可以参见朴灵写得这篇文章:http://cnodejs.org/topic/4faf65852e8fb5bc65113403

    

    3. 深度嵌套

    很多刚开始写nodejs代码的人,由于思路还停留在同步的思维,所以或多或少写过这样的代码:

func1(err, function(err1, data1) {
  func2(err1, function(err2, data2) {
    func3(err3, function(err3, data3) {
      func4(err4, function(err4, data4) {
        .......
      })
    })
  })
})

    先别说这样的代码是否易于维护,光样子就够难看,代码都“斜”了。不怕大家耻笑,我自己刚开始就写时也写过这样“坑爹”的代码,为此自己当时还写了篇《如何让nodejs同步操作》讲如何解决这个问题。不过从源头上,大家还是要尽量避免采用这种同步的方法,因为nodejs得优势就在于异步,硬生生要做成同步绝对吃力不讨好

 

posted @ 2012-05-31 21:44 lengyuhong 阅读(1160) 评论(7) 编辑

    前几天听了部门内朋春大牛讲分布式缓存的一个技术分享,还是非常有收获。

  PPT如下:

 

    这个分享的副标题是“简单的事情从来不简单”,这句话讲得非常在理。缓存看似简单,但要做“好”一个缓存系统也是很有讲究的。   

    写点自己的心得收获吧:

    1. 分布式缓存面临比较大的三个问题:

       (1) 数据一致性。

               在分布式系统这点显得尤为重要,主要原因有三点:

               缓存系统与底层数据的一致性。这点在底层系统是“可读可写”时,写得尤为重要

               有继承关系的缓存之间的一致性。为了尽量提高缓存命中率,缓存也是分层:全局缓存,二级缓存。他们是存在继承关系的。全局缓存可以有二级缓存来组成。

               多个缓存副本之间的一致性。为了保证系统的高可用性,缓存系统背后往往会接两套存储系统(如memcache,redis等),以上的ppt也主要是讲这方面的内容。

       (2)缓存雪崩

               当缓存系统重启或者所有缓存在同一时刻失效(比如某些系统为了提高速度,会在系统启动是统一将大部分数据刷到缓存中,此时如果设置缓存时间都是24小时,那24小时过后,那就悲剧)时,应用系统由于扛不住压力而直接挂掉。

       (3)缓存穿透

               查询一个必然不存在的数据,查询一个必然不存在的key,每次都会访问DB,如果有人恶意破坏,那么很可能直接对DB造成影响。

        第一点偏重数据的真实性和实时性,而第二点和第三点更多从性能上考虑。同时缓存并不一定是必需的,特别是当写操作特别频繁时。

        

     2. 缓存数据的淘汰

     原先缓存数据的淘汰往往是用设置缓存时间,比如我设置某个数据的缓存时间是24小时,之后的24内这个缓存是不会失效的。优点当然是简单,缺点也很明显就是不都灵活,没做到好的精细化管理。

    我们能利用的资源就是:1. 给缓存加tag,2. 版本号(必须单调递增,时间戳是最好的选择)3. 提供手动清理缓存的接口。

    相关步骤可以参见以上的PPT内容。

    缓存相关接口:

var me = Cache.create(...);
me.set(key, value, ttl, tags);
me.get(key);
me.tagrm(tag, offset, flush);

    缓存的数据结构如下:

var data = {
  ‘i’:now, /** 数据写入时间戳 */
  ‘e’:now + ttl,/** 预期过期时间 */
  ‘k’:key, /** 原始key */
  ‘v’:value, /** 原始值 */
  ‘t’:tags /** tag列表 */
};

    我刚开始的也没弄明白为什么在data中还要存“原始的key”,后来经朋春提醒,才弄明白:原始的key可能过长或者存在特殊字符时,是不能直接作为某些系统的key,因此往往会对原始key做一次hash来作为缓存的新key。

    

    3. 缓存淘汰的策略

    缓存淘汰的策略有两种:

    (1) 定时去清理过期的缓存。

   (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

    两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂,具体用哪种方案,大家可以根据自己的应用场景来权衡。

 

 

 

 

 

 

 

 

 

posted @ 2012-05-26 19:03 lengyuhong 阅读(1617) 评论(2) 编辑

    在讲这个问题之前,先来补充几个知识点,如果对此已经比较了解可以直接跳过

    1. 大多数浏览器的组件构成如图

     

    在最底层的三个组件分别是网络,UI后端和js解释器。作用如下:

    (1)网络- 用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作

    (2)UI 后端- 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口

    (3)JS解释器- 用来解释执行JS代码

 ps:上图和知识点主要来自《HOW BROWSERS WORK: BEHIND THE SCENES OF MODERN WEB BROWSERS》 想深入了解的同学可以重点看下。

 

      2. 大多数浏览器(比如chrome)让一个单线程共用于执行javascrip和更新用户界面。这个线程通常被称为“浏览器UI线程”, 每个时刻只能执行其中一种操作,这意味着当Javascript代码正在执行时用户界面无法响应输入,反之亦然。这样做是因为javascript代码的作用就是操作DOM更新用户界面,用同一个线程来做负责这两件事情可以更高效

 

      3. 浏览器UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。这些任务要么是运行javascript代码,要么执行UI更新,包括重绘和重排。

 

      4. 重点再强调下,javascript是单线程运行,千万别被setTimeout()和setInterVal()这种函数迷惑而误以为它是多线程。

 

     ok,基础点讲解完毕,让我们进入正题,来讲解在浏览器中javascript的执行过程。

     

     一、原理

     一般而言,<script>标签每次出现都会霸道地让页面等待脚本的解析和执行,无论当前的Javascript是内嵌的还是包含了外链文件,页面的下载和渲染都必须停下来等待脚本执行完成。这在页面的生存周期中是必要的,因为脚本执行过程中可能修改页面内容,一个典型的例子就是在页面中使用document.write()。

     当javascript代码是内嵌在html里面时,这点还是比较容易理解,但当javascript是外链文件时稍微有点负载,因为存在一个加载过程,而且浏览器加载好这个js文件之后往往还对其缓存。

     首先,我们用以下这个例子来说明下缓存问题

<html>
<head> 
  <script type='text/javascript' src='js/f2.js'></script>
</head>
<body>
</body>
</html> 

     第一次打开页面时:

    第二次打开页面时:

    从上例中可以明显看出,像chrome之类的高版本浏览器会对js文件进行缓存,作用是不言而喻,减少网络请求。

 

  其次,第二个问题,当一个javascript文件被加载时是否会阻塞其他javascript文件或者其他文件的加载。《高性能Javascript》一书中对这个问题做了较好的解答:各种浏览器的低版本的处理是当一个javascript文件在加载时,会同时阻塞页面其他文件的加载(包括其他javascript文件),但IE8,Firfox3.5,Safari 4和Chrome 2都允许并行下载javascript文件,但遗憾的是,javascript下载过程仍然会组舍其他资源的下载,比如图片。尽管javascript脚本的下载过程不会相互影响,但页面仍然必须等待所有的javascript代码下载并执行完成才能继续。

     这里说句题外话:浏览器对同一域名下的并发链接数也是有限制的,其他一些参数如下:

   

   

     二、技巧

     1. 脚本位置

         由于脚本会阻塞页面其他资源的下载,因此推荐将所有的<script>标签放到<body>标签的底部,已尽量减少对整个页面下载的影响。

     2. 将能合并的js文件合并

     3. 无阻塞脚本

     现在比较常用的方法就是动态加载执行脚本。你的原理是通过DOM,你几乎可以用Javascript动态创建HTML中的所有内容,其根本在于,<script>标签与页面中其他元素并无差异:都能通过DOM引用,都能在文档中移动,删除和创建。文件在改该<script>元素被添加到页面时开始现在,它不会阻止其他文件下载,只在执行阶段阻塞渲染。特别强调:《高性能javascript》一文中说“这种技术的重点在于:无论何时启动下载,文件的下载和执行都不会阻塞页面其他进程”,这并不是说它在执行不会阻塞其他javascript代码,而是要强调不会阻塞其他资源的下载等其他任务。

     具体的代码如下:

function loadScript(url){
  var ga = document.createElement('script');
  ga.type = 'text/javascript'; 
  ga.async = true;
  ga.src = url;
  (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ga); 
}

     4. 神奇的setTimeout()

       这里我不过多的将setTimeout()的原理,有兴趣的读者可以具体去看《高性能javascript》的第六章。我重点强调下,setTimeout的第二个参数并不是一个精确的时间,二是必须在javascript线程空闲时才能运行。利用这个特性,如下代码简单可以实现等待其他js代码执行完毕后再执行function里面的代码。

setTimeout(function(){
  // do some before other javascripe codes had processed
}, 25)

      但在function里面不要使用document.write()方法,因为执行setTimeout里面函数时往往已经到了页面onload之后,此时再执行 document.write 将导致当前页面的内容被清空,因为它会自动触发 document.open 方法。

     

参考文章:

《高性能Javascript》

HOW BROWSERS WORK: BEHIND THE SCENES OF MODERN WEB BROWSERS

Google Chrome源码剖析【一】:多线程模型

javascript异步加载详解

 

 

    

posted @ 2012-05-20 18:28 lengyuhong 阅读(1188) 评论(5) 编辑

    一、简介

    CSRF (Cross-site Request Forgery),中文名称:跨站伪造。危害是攻击者可以盗用你的身份,以你的名义发送恶意请求。比如可以盗取你的账号,以你的身份发送邮件,购买商品等。

    二、原理

    具体的原理图如下:

   

    更加恐怖的是使用诸如img之类的标签,甚至不需要用户点击某个链接就可以发起攻击,比如B网站可以添加如下代码:

    <img src='http://www.company.com/action?k1=v1&k2=v2' width=0 height=0 />

      这里width=0 height=0 表示图片是不可见的。这个语句会导致游览器向另外的服务器发送一个请求。游览器不管该图片url实际是否指向一张图片,只要src字段中规定了url,就会按照地址触发这个请求。(游览器默认都是没有禁止下载图片,这是因为禁用图片后大多数web程序的可用性就会打折扣)。加载图片根本不考虑所涉及的图像所在位置(可以跨域)。如果A网站不小心提供了get接口就非常不幸得中招了

        三、攻击

      浅谈CSRF攻击方式一文中已经用实例讲解了一个php的实现和防御方法,我这里主要是讲nodejs的实现和防御。为了简单起见,假设我们有一个应用,它提供了两个接口“/get/checkvalue”和 “'/post/setvalue”,它们都接受一个参数“value”来改变系统的一个某个值。 当然前提是用户已经正常登陆了。具体逻辑是它有一个用户登陆的过程,登陆成功后会将用户信息存到cookie中,之后就根据cookie来判断是否能正常访问,当然cookie信息经过md5加密。(真实应用中千万不要这么做,md5加密并不是非常安全)。

      服务主程序:

var app = http.createServer(function(req, res) {
  //权限判断
  authMiddle(req, res, function(err, checkValue){
    if (!checkValue) return res.end(html_login);
    //数据查询和操作
    controller(req, res);  
  });
});

        用户权限判断逻辑:

var cookieValue = crypto.createHash('md5').update('jifeng_jifeng').digest('hex');
function
getCookie(headers){ var cookies = {}; headers.cookie && headers.cookie.split(';').forEach(function(cookie) { var parts = cookie.split('='); cookies[ parts[ 0 ].trim() ] = ( parts[ 1 ] || '' ).trim(); }); return cookies; } function checkUser(req, res, callback){ var chunks = []; var length = 0; var rows = null; req.on('data', function(data){ chunks.push(data); length += data.length; }) req.on('end', function(){ var rows = new Buffer(length); var len = 0; for (var i = 0, il = chunks.length; i < il; i++) { chunks[i].copy(rows, len); len += chunks[i].length; } var args = querystring.parse(rows.toString()); if (args && args.name === 'jifeng' && args.password ==='jifeng') { res.setHeader('Set-Cookie', ['cookie1987=' + cookieValue]); callback(null, true); } else { callback(null, false); } }) } function authMiddle(req, res, callback){ var flag = false; var params = urllib.parse(req.url, true); if (params.pathname === '/checkuser') { return checkUser(req, res, callback); } else { var headers = req.headers; var cookies = getCookie(headers);//得到用户cookie if (cookies && cookies.cookie1987) { var v = cookies.cookie1987; if (v == cookieValue) { flag = true; } } callback(null, flag) } }

      那具体怎样进攻呢?

      get攻击的页面很简单。

<img src='http://test3.data.taobao.com:5678/get/check?func=get&value=10'> 

      post攻击的页面相对比较复杂

  <head>
    <title>post 测试页面</title>
    <script>
      function steal(){
        var mySubmit = document.getElementById('steal_form');
        mySubmit.submit();
      }
    </script>
  </head>
  <body onload='steal()'>
  <form id = "steal_form" method="POST" action="http://test3.data.taobao.com:5678/post/check">
    <input type="hidden" name="func" value="post">
   <input type="hidden" name="value" value="1000">
  </form>
  </body>

     但这里强调一点:现在游览器(chrome,firfox)为了安全考虑,默认都做了一定的限制,form标签发送到其他网站的请求会被拦截,大家有兴趣模拟这种情况时需要注意这个问题。

     详细的代码:https://github.com/jifeng/toycode/tree/master/csrf

     四、防范

     访问csrf的措施虽然很多,但归根到底就是一条:在客户端提交请求时增加伪造随机数。

     nodejs中有些框架已经帮我们做了这件事,比如重用的connect

   它具体的实现:

     http://www.senchalabs.org/connect/csrf.html   

     举例:

     https://github.com/senchalabs/connect/blob/master/examples/csrf.js

    实现还是相对比较简单,有兴趣的同学可以再仔细看下。

 

 

参考文章:

浅谈CSRF攻击方式

posted @ 2012-05-16 22:17 lengyuhong 阅读(999) 评论(5) 编辑

    在web服务中,提交表达和数据时,get和post是非常常用的两种服务。现用nodejs实现这两个服务。

一、get操作

app.html:

<html>
  <head>
    <title>登陆页面</title>
  </head>
  <body>
    <form action="check" method="get">
      <p>First name: <input type="text" name="fname" /></p>
      <p>Last name: <input type="text" name="lname" /></p>
      <input type="submit" value="Submit" />
    </form>
  </body>
</html> 

 

app.js

var http = require('http');
var urllib = require('url');
var fs = require('fs');

var html = fs.readFileSync('./app.html');

var app = http.createServer(function(req, res){
  var params = urllib.parse(req.url, true);
  if (params.pathname === '/login') {
    res.end(html);
  } else if (params.pathname === '/check') {
    var params = urllib.parse(req.url, true);
    res.end(JSON.stringify(params.query));
  }
});

app.listen(5678, function(){
  console.log('server is listening on 5678');  
});

 

二、post操作

post的实现相对比较繁琐,可以特别注意下buffer.copy这个函数

app.html:

<html>
  <head>
    <title>登陆页面</title>
  </head>
  <body>
    <form action="check" method="post">
      <p>First name: <input type="text" name="fname" /></p>
      <p>Last name: <input type="text" name="lname" /></p>
      <input type="submit" value="Submit" />
    </form>
  </body>
</html> 

app.js:

var http = require('http');
var urllib = require('url');
var fs = require('fs');
var querystring = require('querystring');

var html = fs.readFileSync('./app.html');

var app = http.createServer(function(req, res){
  var params = urllib.parse(req.url, true);
  if (params.pathname === '/login') {
    res.end(html);
  } else if (params.pathname === '/check') {
    var chunks = [];
    var length = 0;
    var rows = null;
    req.on('data', function(data){
      chunks.push(data);
      length += data.length;
    }) 
    req.on('end', function(){
      var rows = new Buffer(length);
      var len = 0;
      for (var i = 0, il = chunks.length; i < il; i++) {
        chunks[i].copy(rows, len);
        len += chunks[i].length;
      }
      var args = querystring.parse(rows.toString());
      res.end(JSON.stringify(args));
    })
  }
  
});

app.listen(5678, function(){
  console.log('server is listening on 5678');  
});
posted @ 2012-05-07 21:37 lengyuhong 阅读(64) 评论(0) 编辑
摘要: 众所周知,mongodb是不支持join操作的,因此我们只能自己来实现这个功能。前段时间,我遇到这个一个业务场景: collection A 的数据格式:{ "_id" : { "username" : "jifeng.zjd", "version" : 2 }, "value" : 1 } collection B 的数据格式:{ "user_id" : 12630, "username" : "jifeng.zjd" } A表的数据阅读全文
posted @ 2012-04-14 15:38 lengyuhong 阅读(1271) 评论(2) 编辑
摘要: 一、JSON和JSONP JSONP的全称是JSON with Padding,由于同源策略的限制,XmlHttpRequest只允许请求当前源(协议,域名,端口)的资源。如果要进行跨域请求,我们可以通过使用html的script标记来进行跨域请求,并在相应中返回要执行的script代码,其中可以直接使用JSON传递javascript对象。这种跨域的通讯方式成为JSONP。 由此我们可以看出两者的区别: json: 一种轻量级的数据格式。 jsonp:为实现跨域,而采用的一种脚本注入方法。 备注:要了解更多json,可以参见我原先写的一篇介绍json的文章:《JSO...阅读全文
posted @ 2012-03-20 23:41 lengyuhong 阅读(275) 评论(0) 编辑
摘要: 刚看到这个标题的时候,大家会觉得比较奇怪,nodejs是一门脚本语言,它又没有“指针”这个数据结构,怎么能操作指针呢 ?这个问题关键就是nodejs中的Buffers这个数据结构。 背景 自己做的一个项目需要是从网站上下载一个图片,然后用libjpeg这个C类库对图像进行操作。程序架构是用nodejs写一个主运行程序,用C++扩展来调用libjpeg类库并实现底层的算法。libjpeg的8d版本中已经有两个接口解析图片信息,分别是从文件中读取和从内存中读取。EXTERN(void) jpeg_stdio_src JPP((j_decompress_ptr cinfo, FILE ...阅读全文
posted @ 2012-03-02 23:55 lengyuhong 阅读(1050) 评论(0) 编辑
摘要: javascript中函数定义通常有两种方法: 1. 普通的函数定义:function f1 (){} 2. 变量式函数定义:var f2 = function(){} 还有一种定义方法是new Function,由于这种方法并不常用,这里就不做解释了。 那这两种定义方法有什么不同呢?难道仅仅只是样子不同而已嘛?当然不是这样,我们来看个具体的例子吧,大家觉得以下代码的执行结果是什么?if (1 === 1) { tellAlert();} else{ function tellAlert(){ alert('hello'); }} 执行结果是:会提示“hello”...阅读全文
posted @ 2012-02-29 20:02 lengyuhong 阅读(1175) 评论(7) 编辑
摘要: 一、简单启动举例:./mongod --dbpath /var/lib/mongodb/ --port 12345这中方法相对比较简单,这里不做具体的阐述,查看下面的文档即可http://www.mongodb.org/display/DOCS/Starting+and+Stopping+Mongo二、master-slave 1. 运行脚本脚本:$ cd ~/apps$ wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-1.8.3.tgz$ tar zxvf mongodb-linux-x86_64-1.8.3.tgz$ m.阅读全文
posted @ 2012-02-21 18:05 lengyuhong 阅读(801) 评论(0) 编辑