基于Node.js的文件服务器(使用Q重构代码)

来源:互联网 发布:怎么解决网络协议冲突 编辑:程序博客网 时间:2024/05/01 15:09

        之前写过一篇文章,简单介绍了一个基于Node.js的静态文件服务器。那时还只是个人兴趣。最近又有了关于服务器的新的需求,我就想花点时间,好好研究一下。所以把之前的代码拿出来重构了一番,整体代码变得干净很多。

        首先最新Node.js是支持generator的,所谓generator,就是javascript中的协程(半协程),不过功能稍弱,仅仅是为了解决js中凶名赫赫的callback hell而诞生的。这里我并没有使用generator,而是使用promise(饭要一口一口吃,先弄明白promise再去学习generator)。promise并不是新的语法,而是一种书写方式。最出名的实现是Q。

        关于Q的学习资料可以看这里,非常清楚。

server.js的代码:

'use strict';var CONFIG = {'host': '127.0.0.1','port': 9527,'site_base': './site','file_expiry_time': 0, // HTTP cache expiry time, minutes'directory_listing': true};var MIME_TYPES = {'.txt': 'text/plain','.md': 'text/plain','': 'text/plain','.html': 'text/html','.css': 'text/css','.js': 'application/javascript','.json': 'application/json','.jpg': 'image/jpeg','.png': 'image/png','.gif': 'image/gif','.zip': 'text/plain','.cfg': 'text/plain'};var EXPIRY_TIME = (CONFIG.file_expiry_time * 60).toString();var http = require('http');var Path = require('path');var Crypto = require('crypto');var Custard = require('./custard/custard');var Q = require('q');var fs = require('./filesystem')// An object representing a server responsefunction ResponseObject( metadata ){this.status = metadata.status || 200;this.data = metadata.data || false;this.type = metadata.type || false;}ResponseObject.prototype.getEtag = function (){var hash = Crypto.createHash( 'md5' );hash.update( this.data );return hash.digest( 'hex' );};function getFileList(files, url, callback, error) {var template = new Custard;var full_path = CONFIG.site_base + url;var i = 0;template.addTagSet('h', require('./custard/templates/tags/html'));template.addTagSet('c', {'title': 'Index of ' + url,'file_list': function (h) {var items = [];var stats;for (i = 0; i < files.length; i += 1) {stats = fs.statSync(full_path + files[i]);if (stats.isDirectory()) {files[i] += '/';}items.push(h.el('li', [h.el('a', {'href': url + files[i]}, files[i])]));}return items;}});Q.nbind(template.render, template)("\h.doctype('html5'),\h.html([\h.head([\h.el('title', c.title),\]),\h.body([\h.el('h1', c.title),\h.el('ul', c.file_list(h))\])\])").then(callback, error);}// Filter server requests by typefunction handleRequest(url){// hack fix  version没有扩展名,但是是一个文本文件]var url = url;var deferred = Q.defer();if ( Path.extname( url ) === '' && url.indexOf('version') == -1 ) {// 路径var full_path = CONFIG.site_base + url;if (!CONFIG.directory_listing) {//Forbiddendeferred.resolve(new ResponseObject({'status': 403}))return deferred.Promise;}fs.exists(full_path).then(function() {return fs.readdir(full_path)}, function() {deferred.resolve(new ResponseObject({'status': 404}))}).then(function(files) {getFileList(files, url, function(html) {deferred.resolve(new ResponseObject({'data': new Buffer(html), 'type': 'text/html'}));}, function(error) {deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}));})}, function(error) {// Internal errordeferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}));})} else {// 文件var path = CONFIG.site_base + url;fs.exists(path).then(function() {return fs.readFile(path)}, function() {deferred.resolve(new ResponseObject({'status': 404}));}).then(function(data){deferred.resolve(new ResponseObject( {'data': new Buffer( data ), 'type': MIME_TYPES[Path.extname(path)]}));}, function(error) {deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}))})}return deferred.promise;;}function parseRange (str, size) {    if (str.indexOf(",") != -1) {        return;    }    str = str.replace("bytes=", "");    var range = str.split("-"),        start = parseInt(range[0], 10),        end = parseInt(range[1], 10);    // Case: -100    if (isNaN(start)) {        start = size - end;        end = size - 1;        // Case: 100-    } else if (isNaN(end)) {        end = size - 1;    }    // Invalid    if (isNaN(start) || isNaN(end) || start > end || end > size) {        return;    }    return {        start: start,        end: end    };};var compressHandle = function (raw, matched, statusCode, reasonPhrase) {    var stream = raw;    var acceptEncoding = request.headers['accept-encoding'] || "";    if (matched && acceptEncoding.match(/\bgzip\b/)) {        response.setHeader("Content-Encoding", "gzip");        stream = raw.pipe(zlib.createGzip());    } else if (matched && acceptEncoding.match(/\bdeflate\b/)) {        response.setHeader("Content-Encoding", "deflate");        stream = raw.pipe(zlib.createDeflate());    }    response.writeHead(statusCode, reasonPhrase);    stream.pipe(response);};// Start serverhttp.createServer(function(request, response) {var headers;var etag;if ( request.method === 'GET' ){//Get response objecthandleRequest( request.url).then(function (response_object) {if (!response_object || ! response_object.data || response_object.data.length <= 0 ) {// 无文件内容response.writeHead(response_object.status);response.end();return;}etag = response_object.getEtag();if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){//Not Modifiedresponse.writeHead( 304 );response.end();return;}var fileFullSize = response_object.data.length;if (request.headers["range"]) {// 如果有rangevar range = parseRange(request.headers["range"], fileFullSize);if (range) {var raw = fs.createReadStream(CONFIG.site_base + request.url, {"start": range.start,"end": range.end});//console.log(raw);headers = {'Accept-Ranges': 'bytes','Content-Type': response_object.type,'Content-Length' : (range.end - range.start + 1),'Cache-Control' : 'max-age=' + EXPIRY_TIME,'Content-Range' : "bytes " + range.start + "-" + range.end + "/" + fileFullSize,'ETag' : etag};console.log("range " + range.start + "-" + range.end);response.writeHead( response_object.status, headers );//response.end( response_object.data );raw.pipe(response);//raw = "";//response.end( raw );} else {console.log("range format error");// range格式错误response.removeHeader("Content-Length");response.writeHead(416, "Request Range Not Satisfiable");response.end();}} else {// 没有range,全文件headers = {'Accept-Ranges': 'bytes','Content-Type': response_object.type,'Content-Length' : response_object.data.length,'Cache-Control' : 'max-age=' + EXPIRY_TIME,'ETag' : etag};response.writeHead( response_object.status, headers );response.end( response_object.data );}} );} else if ( request.method == 'HEAD') {handleRequest(request.url).then(function(response_object){if (!response_object || !response_object.data || response_object.data.length <= 0 ) {response.writeHead(response_object.status);response.end();return;}etag = response_object.getEtag();if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){// Not Modifiedresponse.writeHead( 304 );response.end();return;}headers = {'Content-Type': response_object.type,'Content-Length' : response_object.data.length,'Cache-Control' : 'max-age=' + EXPIRY_TIME,'ETag' : etag};response.writeHead( response_object.status, headers );response.end();} );} else {// Forbiddenresponse.writeHead(403);response.end();}} ).listen( CONFIG.port, CONFIG.host );console.log( 'Site Online : http://' + CONFIG.host + ':' + CONFIG.port.toString() + '/' );


filesystem.js的代码(在server.js中有用到,只是使用Q简单的封装了下node.js的异步文件操作):

var Q = require('q')var fs = require('fs')// 使用Q封装回调形式的文件操作函数var fs_readFile = Q.nfbind(fs.readFile);var fs_readdir = Q.nfbind(fs.readdir);var fs_stat = Q.nfbind(fs.stat);function exists(path) {    var defer = Q.defer();    fs.exists(path, function(exists) {        if (exists) {            defer.resolve(exists);        } else {            defer.reject(exists);        }    });    return defer.promise;}function readFile(path) {    return fs_readFile(path);}function readdir(path) {    return fs_readdir(path);}function stat(path) {    return fs_stat(path)}// 同步函数,直接调用function statSync(path) {    return fs.statSync(path);}function readFileSync(path) {    return fs.readFileSync(path)}module.exports.exists = exists;module.exports.readFile = readFile;module.exports.readFileSync = readFileSync;module.exports.readdir = readdir;module.exports.stat = stat;module.exports.statSync = statSync;


        说实话,即便是重构后的代码,也不是非常简洁。使用Q最主要是解决多层回调的问题的。在上面的代码中,其实没有太多的多层调用,所以体现的不是非常明显,不过项目大了,会有显著的效果。

        即便使用Q之后,调试起来还是比较麻烦,因为我们无法清晰的知道这个函数是谁,在什么时候调用的,也就是说,很多时候我们获取不到调用堆栈(可以获取到,但是获取到的信息几乎无意义,因为函数都是在Q的task中调用的)。

        关于Q封装Node.js的异步函数,这里需要注意一下filesystem.js中的exists()函数。Q.nfbind做的事情其实就是exists中所做的。函数第一个参数必须是error,第二个参数必须是data,这样的函数才能够直接使用Q.nfbind封装,像fs.exits函数,只有一个参数,所以无法使用Q.nfbind封装。

0 0
原创粉丝点击