Tag Archives: Node.js

Hello World

搞定异步编程(一)——使用高阶函数管理并行程序

nodejs对外宣称的最大优势就是所谓的异步io,利用异步io,可以并行执行互相没有依赖的网络、数据库等操作,在某些场景下,这种方式可以极大的提高站点的响应速度
但是异步io也带来了代码本身逻辑的复杂性,比如一个应用,需要访问三个http数据源,然后综合比较三个数据源的结果,来得出最终的返回结果
三个数据源的回调执行顺序是不定的,这就需要一个计数器,在回调次数等于3时,再执行最终的处理程序
这里,借助js的高阶函数,来实现这个逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var _httpLib = require('./lib/http.js');  //简单的http辅助类,如:get(url, callback)
//高阶函数
var pending = (function(callback){
  var count = 0, //计数
    returns = {};
  return function(key){
    count++;     //注册一个回调
    return function(data){
      count--;   //执行一个回调
      //按照并行逻辑的key保存返回结果
      returns[key] = data;
   //   returns[key] = data.substr(0, 100);
      if(count === 0){   //所有回调都执行以后,运行最终的逻辑
        callback(returns);
      }
    };
  };
});
//。。。怎么来准确的称呼done哪?
var done = pending(function(returns){
  //最终的处理逻辑
  console.log('all-------over');
  console.log(returns);
});
//三个并行数据源
_httpLib.get('http://www.baidu.com', done('baidu'));
_httpLib.get('http://www.qq.com', done('qq'));
_httpLib.get('http://www.jiangkl.com', done('jkl'));

~~~呵呵,看到一坨return、function,如果你还没晕,那么,恭喜你。反正我盯着这段代码看了不下10分钟,然后又执行了几次才算大概弄明白的
整个的逻辑,有点类似aop的思想,即对多个并行逻辑的回调做一个拦截、记录,这样可以更好的让程序员将注意力集中到业务逻辑上
pending是一个公用的并行处理函数
done是通过pending得到的具体的业务执行function,其回调参数就是前面提到的并行逻辑“最终的处理程序”
done()执行时,会将pending闭包内的计数器加1,同时返回一个function,作为httpget的回调
done()()执行时,计时器减1,同时将httpget的返回值保存
计数器减到0,“最终的处理程序”开始执行

———–
参考:http://www.infoq.com/cn/articles/generator-and-asynchronous-programming

Hello World 业界杂谈

nodejs-express初体验

上次文章提到,正在用nodejs做一个小项目,现在总算是做完了

学习/搞着玩是一回事,虽然两年前就开始接触nodejs,做实际要用到项目里是另一回事,经过两周的折腾,我这里不想夸,也不想踩,只想说一下我真实的感受,给准备用nodejs的朋友一点参考

整体来讲,nodejs+express的开发模式,感觉还可以,既不像前端大牛们说的一样好,也不像另一些人说的那样一无是处,下面针对几个关键点分别说一下

1. express

早就听人说过,express是个“优秀”的web框架,我这次的使用体验,却让我没有这种感觉。大概是用惯了cakephp那种规约编程/傻瓜化的web框架的缘故,我觉得一个框架最应该做的,就是让使用者不要过多感觉到框架的存在,以最简单的方式获取请求参数/调取model/lib/以及向view层传递显示,可以让人将所有的精力都投入到业务逻辑中~~要不我还用你框架干嘛?

对express,最别扭的一点,就是它的routes配置~~居然每一个请求的url都要去做配置,让我一下想到了java/spring mvc的那个xml~~懒婆娘的裹脚布也不过如此

另一个sb的地方,是它的logger~~本来不想重写日志类的,特别是access日志,服务器能自动记录最好,找了好久,才找到如何设置express 日志的格式,可是设置好以后,发现日志时间记录的不对,格式也不好看,一查原码才发现,logger里用的是date.toUTCString~~UTC啊,尼玛我看日志的时候,还得想着和实际时间差8小时。。。

最关键的一点是,express官方的api极其“简约”,很多东西不得不去翻框架原码才能发现一些“隐藏”的功能

最后,作为一个web框架,居然没有filter,虽然app.use可以部分实现filter的功能,但用起来还是感觉很不爽

2. npm

npm真TMD乱,很多好名字都被占了,占着茅坑不拉屎

比如thrift,装上以后发现根本不能用,版本太老,仔细一看,居然2年没维护了

再比如email,居然没看见在哪儿设置邮件服务器~~太TM扯淡了

3. 模板引擎

express默认的jade就不提了,稍微复杂点的页面,就会搞得一团糟

我用的是ejs,不好不坏,只能说“可用”。我比较喜欢view层代码,后端程序和前端html/js可以一目了然,所以我使用<%作为格式符,然后用jsp编辑器打开ejs文件~~哈哈,一目了然

ejs比较不好用的一点是对于变量的判断过于严格:如果试图输出没往view层传递的变量,页面居然会直接抛500错误~~太较真了吧,你当空串处理不久行了吗?

4. nodejs

不好意思,上面说了3点,大部分都是在踩,现在该说点好的了

js&node,本身还是一款很不错的组合,虽然异步编程需要一些时间来适应,但却给高并发和快速影响带了了天生支持;另外,js语法简单自由,也可以带来很高的开发效率

对于所谓的“前后端共用代码”,我倒是觉得不太有必要~~毕竟,前后端执行环境差异巨大,写这种“公用代码”的维护成本,还不如直接copy一份给前端用

最后,给自己留一个问题:下次你还会选nodejs吗?

以前写过一个简单的前端mvc框架,jLeaf,基本思路是模仿cake和spring来做的,node出来后,曾经想顺势再写一个后端框架,名字就叫jRoot,可惜因为工作忙,也因为人懒,没能实际做出来,看到express这么难用,真想现在就把jRoot做出来,可惜现在更忙了~~~希望后面能有时间把它搞出来,也希望node能有其它更好用的web框架出来

 

Hello World

Xcode对编译node.js扩展的影响

mac系统下,使用node-gyp编译node.js扩展,总共两步:

1
2
	node-gyp configure
	node-gyp build

悲催的是,两步居然都出了问题
第一步碰上的问题是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	gyp info spawn args   'build',
	gyp info spawn args   '-Goutput_dir=.' ]
	xcode-select: Error: No Xcode is selected. Use xcode-select -switch <path-to-xcode>, or see the xcode-select manpage (man xcode-select) for further information.
 
	Traceback (most recent call last):
  	  File "/usr/local/lib/node_modules/node-gyp/gyp/gyp", line 18, in <module>
    	sys.exit(gyp.main(sys.argv[1:]))
		//..................
	Exception: Error 2 running xcode-select
	gyp ERR! configure error 
	gyp ERR! stack Error: `gyp` failed with exit code: 1
	gyp ERR! stack     at ChildProcess.onCpExit (/usr/local/lib/node_modules/node-gyp/lib/configure.js:420:16)
	gyp ERR! stack     at ChildProcess.EventEmitter.emit (events.js:91:17)
	gyp ERR! stack     at Process._handle.onexit (child_process.js:678:10)
	gyp ERR! System Darwin 12.2.0
	gyp ERR! command "node" "/usr/local/bin/node-gyp" "configure"
	gyp ERR! cwd /JKL/MyWorld/node/c_m/helloWorld
	gyp ERR! node -v v0.8.9
	gyp ERR! node-gyp -v v0.8.3
	gyp ERR! not ok 
	xcode-select: Error: No Xcode is selected. Use xcode-select -switch <path-to-xcode>, or see the xcode-select manpage (man xcode-select) for further information.

第一反应是,node.js和Xcode有毛关系,google“path-to-xcode”,发现还真是有问题:我的系统曾经装过早期版本的xcode,是默认装在developer系统下,但是最新版本的xcode是在Applications目录里,虽然这对xcode没影响(至少我没发现),但是看起来是对里面的c++模块有影响,具体啥情况,我也不晓得~~只要知道解决方案就可以了:
重新设置xcode-select指到你的Xcode.app

1
	sudo xcode-select -switch /Applications/Xcode.app/

设置了xcode-select以后,“node-gyp configure”通过,但是“node-gyp build”再次出现问题:

1
2
3
4
5
6
7
8
	gyp info it worked if it ends with ok
	gyp info using node-gyp@0.8.3
	gyp info using node@0.8.9 | darwin | x64
	gyp ERR! build error 
	gyp ERR! stack Error: not found: make
	gyp ERR! stack     at F (/usr/local/lib/node_modules/node-gyp/node_modules/which/which.js:43:28)
	//..........
	gyp ERR! not ok

对configure/make这种玩意本来就不熟,于是有迷茫了一阵,继续google,发现据说是缺少GCC(GNU Compiler Collection,GNU编译器集合),还是需要通过Xcode解决:
打开Xcode -> Preferences -> Downloads -> Command Line Tools -> install -> Reboot
下载这个100多M的东西,用了些时间,然后再次“node-gyp build”
~~~ok

Hello World

node.js入门—-静态文件服务器

本文展示是基于node.js的静态文件服务器,代码参考自这里,主要是练习node http、文件模块的使用,另外,对理解http协议也很有帮助
除了实现了基本的路由控制,还实现了MIME类型、304缓存、gzip压缩、目录读取

首先是配置文件,setting.js

var setting = {
  webroot : '/xxx/xxx/webroot',
  viewdir : false,
  index : 'index.html',	//只有当viewdir为false时,此设置才有用
  expires : {
    filematch : /^(gif|png|jpg|js|css)$/ig,
    maxAge: 60 * 60 //默认为一个月
  },
  compress : {
    match : /css|js|html/ig
  }
};
module.exports = setting;

MIME映射,mime.js

var mime = {
  "html": "text/html",
  "ico": "image/x-icon",
  "css": "text/css",
  "gif": "image/gif",
  "jpeg": "image/jpeg",
  "jpg": "image/jpeg",
  "js": "text/javascript",
  "json": "application/json",
  "pdf": "application/pdf",
  "png": "image/png",
  "svg": "image/svg+xml",
  "swf": "application/x-shockwave-flash",
  "tiff": "image/tiff",
  "txt": "text/plain",
  "wav": "audio/x-wav",
  "wma": "audio/x-ms-wma",
  "wmv": "video/x-ms-wmv",
  "xml": "text/xml"
};
module.exports = mime;

然后是主程序,server.js

var http = require('http');
var setting = require('./setting.js');
var mime = require('./mime');
var url = require('url');
var util = require('util');
var path = require('path');
var fs = require('fs');
var zlib = require('zlib');
//访问统计
var number = 0;
var accesses = {};
 
http.createServer(function(req, res){
  res.number = ++number;
  accesses[res.number] = {startTime : new Date().getTime()};
  var pathname = url.parse(req.url).pathname;
  //安全问题,禁止父路径
  pathname = pathname.replace(/\.\./g, '');
  var realPath = setting.webroot + pathname;
  accesses[res.number].path = pathname;
  readPath(req, res, realPath, pathname);
}).listen(8000);
 
console.log('http server start at parth 8000\n\n\n');
//判断文件是否存在
function readPath(req, res, realPath, pathname){
  //首先判断所请求的资源是否存在
  path.exists(realPath, function(ex){
    console.log('path.exists--%s', ex);
    if(!ex){
      responseWrite(res, 404, {'Content-Type' : 'text/plain'}, 
          'This request URL ' + pathname + ' was not found on this server.');
    }else{
      //文件类型
      fs.stat(realPath, function(err, stat){
        if(err){
          responseWrite(res, 500, err);
        }else{
          //目录
          if(stat.isDirectory()){
            //是否读取目录
            if(setting.viewdir){
              fs.readdir(realPath, function(err, files){
                if(err){
                  responseWrite(res, 500, err);
                }else{
                  var htm = '<html><head><title>' + pathname + '</title></head><body>' + pathname + '<hr>';
                  for(var i = 0; i < files.length; i++){
                    htm += '<br><a href="' + pathname + (pathname.slice(-1) != '/' ? '/' : '') 
                        + files[i] + '">' + files[i] + '</a>', 'utf8';
                  }
                  responseWrite(res, 200, {'Content-Type' : 'text/html'}, htm);
                }
              });
            }else if(setting.index && realPath.indexOf(setting.index) < 0){
              readPath(req, res, path.join(realPath, '/', setting.index), path.join(pathname, '/', setting.index));
            }else{
              responseWrite(res, 404, {'Content-Type' : 'text/plain'}, 
                  'This request URL ' + pathname + ' was not found on this server.');
            }
          }else{
            var type = path.extname(realPath);
            type = type ? type.slice(1) : 'nuknown';
            var header = {'Content-Type' : mime[type] || 'text/plain'};
            //缓存支持
            if(setting.expires && setting.expires.filematch 
                && type.match(setting.expires.filematch)){
              var expires = new Date(),
              maxAge = setting.expires.maxAge || 3600 * 30;
              expires.setTime(expires.getTime() + maxAge * 1000);
              header['Expires'] = expires.toUTCString();
              header['Cache-Control'] = 'max-age=' + maxAge;
              var lastModified = stat.mtime.toUTCString();
              header['Last-Modified'] = lastModified;
              //判断是否304
              if(req.headers['if-modified-since'] && lastModified == req.headers['if-modified-since']){
                responseWrite(res, 304, 'Not Modified');
              }else{
                readFile(req, res, realPath, header, type);
              }
            }else{
              readFile(req, res, realPath, header, type);
            }
          }
        }
      });
    }
  });
}
//读文件/压缩/输出
function readFile(req, res, realPath, header, type){
  var raw = fs.createReadStream(realPath), cFun;
  //是否gzip
  if(setting.compress && setting.compress.match 
      && type.match(setting.compress.match) && req.headers['accept-encoding']){
    if(req.headers['accept-encoding'].match(/\bgzip\b/)){
      header['Content-Encoding'] = 'gzip';
      cFun = 'createGzip';
    }else if(req.headers['accept-encoding'].match(/\bdeflate\b/)){
      header['Content-Encoding'] = 'deflate';
      cFun = 'createDeflate';
    }
  }
  res.writeHead(200, header);
  if(cFun){
    raw.pipe(zlib[cFun]()).pipe(res);
  }else{
    raw.pipe(res);
  }
}
//普通输出
function responseWrite(res, starus, header, output, encoding){
  encoding = encoding || 'utf8';
  res.writeHead(starus, header);
  if(output){
    res.write(output, encoding);
  }
  res.end();
  accesses[res.number].endTime = new Date().getTime();
  //日志输出
  console.log('access[%s]--%s--%s--%s--%s\n\n', res.number, accesses[res.number].path, 
      (accesses[res.number].endTime - accesses[res.number].startTime), 
      starus, (output ? output.length : 0));
 delete accesses[res.number];
}

over!
尚欠缺的功能:日志记录、断点、容错等~~以后有时间再加啦

Hello World

node.js入门—-做个代理服务器

看到node.js的httpServer和http.request,第一个想法居然是可以用它做一个代理服务器
下面代码,实现了代理的基本功能,通过网络的代理设置将你的浏览器的请求转到这个httpServer上,其接收到浏览器的http请求,转发到目的服务器,再将收到的数据转移到浏览器~~~就一二道贩子。

var _http = require('http'),
    _util = require('util'),
    //记录当前是第几个请求
    number = 0;
 
_http.createServer(function(req, res){
	number++;
	res.number = number;
	var headers = req.headers,
		post = '',
		url = req.url;	//_url.parse(req.url, true)--解析url
	console.log('\n======\n[' + number + ']--http--start--[' + req.method + ']' + url);
	if(req.method == 'POST'){
		//接收post
		req.on('data', function(chunk){
			post += chunk;
		});
		req.on('end', function(){
			post = _querystring.parse(post);
			httpRequest(res, headers, url, post);
		});
	}else{
		httpRequest(res, headers, url);
	}
}).listen(3333);
 
console.log('http server start at parth 3333\n\n\n');
 
/*
 * 发送请求
 */
function httpRequest(mainRes, headers, url, post){
	var host = headers.host;
	host = host.split(':');
	var options = {
		host : host[0],
		port : (host[1] ? host[1] : '80'),
		path : url,
		method : (post ? 'POST' : 'GET'),
		headers : headers
	};
	//去掉gzip
	delete options.headers['accept-encoding'];
	console.log('[' + mainRes.number + ']----start http.request-- options:\n%s\n', _util.inspect(options));
 
	var req_ = _http.request(options, function(res_){
		var status_ = res_.statusCode,
			headers_ = res_.headers,
			data = '';
		console.log('[' + mainRes.number + ']----export http.request-(' + status_ + ')-headers:%s', 
				_util.inspect(headers_));
 
		//写header
		mainRes.writeHead(status_, headers_);
 
		var encoding = 'utf8';
		//根据数据类型确定编码方式
		if(headers_['content-type'] && (headers_['content-type'].indexOf('image') >= 0 
				|| headers_['content-type'].indexOf('application') >= 0 
				|| headers_['content-type'].indexOf('audio') >= 0 
				|| headers_['content-type'].indexOf('video') >= 0)){
			encoding = 'binary';
		}
		res_.setEncoding(encoding);
		//接收数据,转给原请求
		res_.on('data', function(d){
			console.log('data-------' + d.length);
			var b = new Buffer(d, encoding);
			mainRes.write(b, encoding);
		});
		res_.on('end', function(){
			//请求结束
			mainRes.end();
			console.log('[' + mainRes.number + ']--------http.request---end----');
		});
	});
	req_.on('error', function(e){
		console.log([' + mainRes.number + '] + '----ERROR----problem with request: ' + e.message);
	});
	//发送post
	if(post){
		req_.write(_querystring.stringify(post));
	}
	req_.end();
}

当然,这段代码仅仅是学习目的的,并没有什么实际意义,https、gzip的问题都没解决,如果网站是gbk的,拿它访问也会乱码
如果要拿它翻墙,需要部署两个类似的服务,比如墙外A墙内B,将浏览器代理到B,由B将host隐藏起来,再转到A,A将隐藏的目的host恢复过来,再去访问最终的目的网站
用上面的目的试了试非死不可,不稳定,时不时的会出错~~等完善的再分享给大家