Node.js写的轻量级HTTP静态文件的引擎

jopen 13年前

采用nodejs写的,纯javascript代码,无任何依赖(仅使用了nodejs的标准库)

轻量级,代码只有15K,带注释

支持各种配置(内嵌到代码最上面的部分,详细注释)

支持目录访问(自动列出全部子目录和文件,可关闭)

支持欢迎页(index.html,存在该文件就不会列出目录了)

支持小文件内存缓存(LRU算法,可配置尺寸)

支持304木有修改(静态服务器最重要的就是这个了吧)

支持gzip、deflate压缩

支持分段下载(支持部分,可用迅雷等多线程下载工具)

提供动态解析的接口

最后由于是本人练习所用写的,对HTTP协议的支持还是很不完善的,但是稳定性还是可以的,现在该程序运用在Minecraft多人服务器-蓝色星球上,稳定地使用了3个月了吧……

/**   * Created by JetBrains WebStorm.   * User: chishaxie   * Date: 12-1-5   * Time: 下午4:59   * To change this template use File | Settings | File Templates.   */  var conf = {      Root : 'html', //文件的根路径      IndexEnable : true, //开启目录功能?      IndexFile : 'index.html', //目录欢迎文件      DynamicExt : /^\.njs$/ig, //动态页面后缀(需要.)      ServerName : 'httpNgin/nodeJS', //服务器名字      FileCache : { //文件(内存)缓存          MaxSingleSize : 1024*1024, //单个文件最大尺寸          MaxTotalSize : 30*1024*1024 //整个文件Cache最大尺寸      },      Expires : { //浏览器缓存          FileMatch : /gif|jpg|png|js|css|ico/ig, //匹配的文件格式          MaxAge : 3600*24*365 //最大缓存时间      },      Compress : { //编码压缩          FileMatch : /css|js|html/ig //匹配的文件格式      },      MIME : {          'css': 'text/css',          'gif': 'image/gif',          'html': 'text/html',          'ico': 'image/x-icon',          'jpg': 'image/jpeg',          'js': 'text/javascript',          'png': 'image/png',          'rar' : 'application/x-rar-compressed',          'txt': 'text/plain',    'jar': 'application/java-archive'      }  };    /* 计算长度(中文算2个长度) */  String.prototype.len = function(){      return this.replace(/[^\x00-\xff]/g,'**').length;  };    /* 填充(长度,字符串,填充到左边?) */  String.prototype.pad = function(len,char,isLeft){      var bArr = len.toString(2).split('');      var ret = '';      var step = char;      for(var i=bArr.length-1;i>=0;i--){          if(bArr[i] == '1') ret += step;          step += step;      }      if(!isLeft) return this + ret;      else return ret + this;  };    /* 获取Http时间(2012-12-21 19:30形式) */  Date.prototype.getHttpTime = function(){      return this.getFullYear() + '-' + (this.getMonth()+1) + '-' + this.getDate() +  ' ' + this.getHours() + ':' + this.getMinutes() ;  };    /* 缓存类(maxSize 最大字节数) */  function Cache(maxSize){      this.maxSize = maxSize; //最大尺寸      this.curSize = 0; //当前尺寸      this._bufs = {}; //缓存Map      this._accessCount = 0; //访问计数器      this._lastClearCount = 0; //上次清理的计数器  }    Cache.prototype.put = function(key,buf){      buf.access = this._accessCount++;      var obuf = this._bufs[key];      if(obuf) this.curSize -= obuf.length;      this._bufs[key] = buf;      this.curSize += buf.length;      while(this.curSize > this.maxSize){          this._clear();      }  };    Cache.prototype.get = function (key) {      var buf = this._bufs[key];      if (buf) buf.access = this._accessCount++;      return buf;  };    Cache.prototype.del = function (key) {      var buf = this._bufs[key];      if (buf) {          this.curSize -= buf.length;          delete this._bufs[key];      }  };    Cache.prototype._clear = function () {      var clearCount = (this._lastClearCount + this._accessCount) / 2;      for (var e in this._bufs) {          var buf = this._bufs[e];          if (buf.access <= clearCount) {              this.curSize -= buf.length;              delete this._bufs[e];          }      }      this._lastClearCount = clearCount;  };    /* HTTP缓存类(mtime不可更改) */  function HttpCache(mtime,obuf,gbuf,dbuf){      this.mtime = mtime; //修改时间      this.obuf = obuf; //原始数据      this.gbuf = gbuf; //gzip数据      this.dbuf = dbuf; //deflate数据      this.length = (obuf?obuf.length:0) + (dbuf?dbuf.length:0) + (gbuf?gbuf.length:0);  }    /*  HttpCache.prototype.setObuf = function(obuf){      this.length += obuf.length - (this.obuf?this.obuf.length:0);      this.obuf = obuf;  };  */    HttpCache.prototype.setGbuf = function(gbuf){      this.length += gbuf.length - (this.gbuf?this.gbuf.length:0);      this.gbuf = gbuf;  };    HttpCache.prototype.setDbuf = function(dbuf){      this.length += dbuf.length - (this.dbuf?this.dbuf.length:0);      this.dbuf = dbuf;  };    var http = require('http'),      url = require('url'),      path = require('path'),      fs = require('fs'),      zlib = require('zlib');    /* 路径状态查询(自带文件名封装) */  fs.statWithFN = function(dirpath,filename,callback){      fs.stat(path.join(dirpath,filename),function(err,stats){          callback(err,stats,filename);      });  };    var Com = {      fileCache : new Cache(conf.FileCache.MaxTotalSize),      ifModifiedSince : 'If-Modified-Since'.toLowerCase(),      parseRange : function(str,size){ //范围解析(HTTP Range字段)          if(str.indexOf(",") != -1) //不支持多段请求              return;          var strs = str.split("=");          str = strs[1] || '';          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};      },      error : function(response,id,err){ //返回错误          response.writeHeader(id, {'Content-Type': 'text/html'});          var txt;          switch(id){              case 404:                  txt = '<h3>404: Not Found</h3>';                  break;              case 403:                  txt = '<h3>403: Forbidden</h3>';                  break;     case 416:      txt = '<h3>416: Requested Range not satisfiable</h3>';      break;              case 500:                  txt = '<h3>500: Internal Server Error</h3>';                  break;          }          if(err) txt += err;          response.end(txt);      },      cache : function(response,lastModified,ext){ //写客户端Cache          response.setHeader('Last-Modified', lastModified);          if(ext && ext.search(conf.Expires.FileMatch)!=-1){              var expires = new Date();              expires.setTime(expires.getTime() + conf.Expires.MaxAge * 1000);              response.setHeader('Expires', expires.toUTCString());              response.setHeader('Cache-Control', 'max-age=' + conf.Expires.MaxAge);          }      },      compressHandle : function(request,response,raw,ext,contentType,statusCode){ //流压缩处理          var stream = raw;          var acceptEncoding = request.headers['accept-encoding'] || '';          var matched = ext.match(conf.Compress.match);          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.setHeader('Content-Type', contentType);          response.writeHead(statusCode);          stream.pipe(response);      },      flush : function(request,response,cache,ext,contentType){ //Cache输出          var acceptEncoding = request.headers['accept-encoding'] || "";          var matched = ext.match(conf.Compress.FileMatch);          if (matched && acceptEncoding.match(/\bgzip\b/)) {              if(cache.gbuf){                  response.writeHead(200, {'Content-Encoding': 'gzip','Content-Type': contentType});                  response.end(cache.gbuf);              }              else{                  zlib.gzip(cache.obuf,function(err,buf){                      if(err) Com.error(response,500,'<h4>Error : ' + err + '</h4>');                      else{                          response.writeHead(200, {'Content-Encoding': 'gzip','Content-Type': contentType});                          response.end(buf);                          cache.setGbuf(buf);                      }                  });              }          } else if (matched && acceptEncoding.match(/\bdeflate\b/)) {              if(cache.dbuf){                  response.writeHead(200, {'Content-Encoding': 'deflate','Content-Type': contentType});                  response.end(cache.dbuf);              }              else{                  zlib.deflate(cache.obuf,function(err,buf){                      if(err) Com.error(response,500,'<h4>Error : ' + err + '</h4>');                      else{                          response.writeHead(200, {'Content-Encoding': 'deflate','Content-Type': contentType});                          response.end(buf);                          cache.setDbuf(buf);                      }                  });              }          } else {              response.writeHead(200,{'Content-Type': contentType});              response.end(cache.obuf);          }      },      pathHandle : function(request,response,realpath,httppath,dirmtime){          fs.stat(realpath,function(err,stats){              if(err){                  if(dirmtime){                      var dirPath = path.dirname(realpath);                      fs.readdir(dirPath,function(err,files){                          if(err) Com.error(response,404);                          else{                              var httpP = httppath.replace(/\\/g,'/');                              var txt = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>Index of '+httpP+'</title></head><body><h1>Index of '+httpP+'</h1><hr ><pre>';                              if(httpP!='/')                                  txt += '<a href="'+path.dirname(httppath).replace(/\\/g,'/')+'">../</a>\n';                              var fileI = 0;                              var fileInfos = [];                              var fsCallback = function(err,stats,filename){                                  if(!err) fileInfos.push([stats,filename]);                                  fileI++;                                  if(fileI == files.length){                                      fileInfos.sort(function(a,b){                                          if(a[0].isDirectory() == b[0].isDirectory())                                              return a[1].localeCompare(b[1]);                                          return b[0].isDirectory() - a[0].isDirectory();                                      });                                      for(var i=0;i<fileInfos.length;i++){                                          if(fileInfos[i][0].isDirectory()) fileInfos[i][1] += '/';                                          var sf = fileInfos[i][1];                                          var st = fileInfos[i][0].mtime.getHttpTime();                                          var ss = fileInfos[i][0].isDirectory()?'-':fileInfos[i][0].size.toString();                                          txt += '<a href="'+path.join(httppath,sf).replace(/\\/g,'/')+'">'+sf+'</a>'+''.pad(50-sf.len(),' ')+st+''.pad(35-st.len()-ss.length,' ')+ss+'\n'                                      }                                      txt += '</pre><hr ><h3>Powered by '+conf.ServerName+'</h3></body></html>';                                      var cache = new HttpCache(dirmtime.getTime(),new Buffer(txt));                                      Com.cache(response,dirmtime.toUTCString(),'html');                                      Com.flush(request,response,cache,'html','text/html');                                      Com.fileCache.put(dirPath+'\\',cache);                                  }                              };                              for(var i=0;i<files.length;i++)                                  fs.statWithFN(dirPath,files[i],fsCallback);                          }                      });                  }                  else{                      Com.fileCache.del(realpath);                      Com.error(response,404);                  }              }              else{                  var lastModified = stats.mtime.toUTCString();                  //304 客户端有Cache,且木有改动                  if(request.headers[Com.ifModifiedSince] && lastModified == request.headers[Com.ifModifiedSince]){                      response.writeHead(304);                      response.end();                      return;                  }                  var ext = path.extname(realpath);                  ext = ext?ext.slice(1):'unknown';                  ext = stats.isDirectory()?'html':ext;                  var contentType = conf.MIME[ext];                  var cache = Com.fileCache.get(realpath);                  //服务端有Cache,且木有改动                  if(cache && cache.mtime == stats.mtime.getTime()){                      Com.cache(response,lastModified,ext);                      Com.flush(request,response,cache,ext,contentType);                      Com.fileCache.put(realpath,cache);                      return;                  }                  if(stats.isDirectory()){                      realpath = path.join(realpath,conf.IndexFile);                      Com.pathHandle(request,response,realpath,httppath,conf.IndexEnable?stats.mtime:0);                  }                  else{                      //不合法的MIME                      if(!contentType){                          Com.error(response,403);                          return;                      }                      Com.cache(response,lastModified,ext);                      //文件太大,服务端不Cache                      if(stats.size > conf.FileCache.MaxSingleSize){                          if(request.headers['range']){                              var range = Com.parseRange(request.headers['range'], stats.size);                              if(range){                                  response.setHeader('Content-Range', 'bytes ' + range.start + '-' + range.end + '/' + stats.size);                                  response.setHeader('Content-Length', (range.end - range.start + 1));                                  var raw = fs.createReadStream(realpath, {'start': range.start, 'end': range.end});                                  Com.compressHandle(request,response,raw,ext,contentType,206);                              }                              else          Com.error(response,416);                          }                          else{                              var raw = fs.createReadStream(realpath);                              Com.compressHandle(request,response,raw,ext,contentType,200);                          }                      }                      else{                          fs.readFile(realpath,function(err,data){                              if(err) Com.error(response,500,'<h4>Error : ' + err + '</h4>');                              else{                                  var buf = new HttpCache(stats.mtime.getTime(),data);                                  Com.flush(request,response,buf,ext,contentType);                                  Com.fileCache.put(realpath,buf);                              }                          });                      }                  }              }          });      }  };    /* 对外的接口 */  exports.createServer = function(port,dynamicCallBack){      if(!port) port = 80;      http.createServer(function(req,res){          if(conf.ServerName) res.setHeader('Server',conf.ServerName);    var httppath = '/';    try{     httppath = path.normalize(decodeURI(url.parse(req.url).pathname.replace(/\.\./g, '')));    }    catch(err){     httppath = path.normalize(url.parse(req.url).pathname.replace(/\.\./g, ''));    }          var realpath = path.join(conf.Root, httppath );          var ext = path.extname(realpath);          if(ext.search(conf.DynamicExt) != -1){              if(typeof dynamicCallBack === 'function')                  dynamicCallBack(req,res,httppath,realpath);              else                  Com.error(res,500,"<h4>Error : Can't find the dynamic page callback function!</h4>");          }          else              Com.pathHandle(req,res,realpath,httppath);      }).listen(port);  };