代码改变世界

简单文件服务器

2013-07-03 23:27  king0222  阅读(368)  评论(0编辑  收藏  举报

一,静态服务器基本结构

写一个简单的静态服务器的功能,只需要能够读取文件即可。在这之前我们应该需要一个mimetype文件,用来映射content-type.

 1     var mimeTypes = {
 2         'js' : 'text/javascript',
 3         'html': 'text/html',
 4         'css' : 'text/css',
 5         'gif': 'image/gif',
 6         'ico': 'image/x-icon',
 7         'jpeg': 'image/jpeg',
 8         'jpg': 'image/jpeg',
 9         'json': 'application/json',
10         'pdf': 'application/pdf',
11         'png': 'image/png',
12         'svg': 'image/svg+xml',
13         'swf': 'application/x-shockwave-flash',
14         'tiff': 'image/tiff',
15         'wav': 'audio/x-wav',
16         'wma': 'audio/x-ms-wma',
17         'wmv': 'video/x-ms-wmv',
18         'xml': 'text/xml'
19     };
View Code

然后写出我们创建http服务器的基本框架吧:

1     var http = require('http');
2 
3     http.createServer(function(req, res){
4         
5     }).listen(8080);
View Code

我们需要读取服务器的文件时通过Url中的文件路径来读取的,通过req.url我们可以读到用户的访问路径。只要对这个url进行处理,我们就能得到用户访问文件的路径和文件名。得到文件路径后,我们就可以通过nodejs提供的readFile接口来读取文件了,在这里我们进行一下修改:

 1     var STATIC = '/public';
 2     http.createServer(function (req, res) {
 3         //需要引入path模块
 4         var lookup = url.parse(decodeURI(request.url)).pathname;
 5         //路径标准化
 6         lookup = path.normalize(lookup);
 7         lookup = (lookup === "/") ? '/index.html' : lookup;
 8         var f = STATIC + lookup;
 9         var suffix = path.extname(look).slice(1);
10         fs.exists(f, function (exists) {
11             if (exists) {
12                 fs.readFile(f, function (err, data) {
13                     if (err) { 
14                         //读取文件失败则返回500状态
15                         res.writeHead(500);
16                         res.end('Server Error!'); 
17                         return; 
18                     }
19                     var headers = {'Content-type': mimeTypes[suffix]};
20                     res.writeHead(200, headers);
21                     res.end(data);
22                 });
23                 return;
24             }
25             //文件不存在则返回404状态
26             res.writeHead(404); 
27             res.end();
28         });
29     }).listen(8080, function(){
30         console.log('server listen on port 8080');
31     });
View Code

如果在windows环境下,上面的代码可能是会出错的,会出现查找不到文件的问题,因此需要将

1     //需要引入path模块
2     var lookup = url.parse(decodeURI(request.url)).pathname;
3     //路径标准化
4     lookup = path.normalize(lookup);
5     lookup = (lookup === "/") ? '/index.html' : lookup;
6     var f = STATIC + lookup;
7     var suffix = path.extname(lookup).slice(1);
View Code

修改成为如下代码:

1     var lookup = url.parse(decodeURI(req.url)).pathname;
2     lookup = path.normalize(lookup);
3     lookup = (lookup === "\\") ? 'index.html' : lookup.slice(1);
4     var f = STATIC + lookup;
5     var suffix = path.extname(lookup).slice(1);
View Code

二、添加缓存

我们当然希望能够减轻服务器的负担,为了避免重复性的访问同一个文件,我们可以对访问过的文件进行缓存,如果有了缓存,我们则直接从缓存中读取文件了,就不需要再次访问磁盘了,所以再我们上面的代码块中:

 1     fs.readFile(f, function (err, data) {
 2         if (err) { 
 3             //读取文件失败则返回500状态
 4             res.writeHead(500);
 5             res.end('Server Error!'); 
 6             return; 
 7         }
 8         var headers = {'Content-type': mimeTypes[path.extname(lookup)]};
 9         res.writeHead(200, headers);
10         res.end(data);
11     });
12     这里我们可以做一个缓存判断,大致过程就是:
13     if(cache[f]){
14         //使用缓存数据
15     } else {
16         fs.readFile(f, function(err, data){
17             ...
18         });    
19     }
View Code

要完成上面的代码,我们就需要创建一个cache对象来保存这些缓存数据,var cache = {};
当然我们可以将缓存这个过程单独拆分出来写成一个函数,为了与原来的代码结构一直,我们希望能跟fs.read接口的形式一样来使用该函数:

1     fs.readFile(f, function(err, data){
2         //...
3     });
4     缓存函数使用形式与fs.readFile一致:
5     cacheFile(f, function(err, data){
6         //...
7     });
View Code

完成的cacheFile函数为:

 1     function cacheFile(f, cb){
 2         if (!cache[f]) {
 3             fs.readFile(f, function(err, data){
 4                 if (!err) {
 5                     cache[f] = {content: data};
 6                 }
 7                 cb(err, data);
 8             });
 9             return;
10         }
11         cb(null, cache[f].content);
12     }
View Code

有了缓存处理之后,在createServer回调中,代码改为:

 1     fs.exists(f, function(exists){
 2         if (exists) {
 3             cacheFile(f, function(err, data){
 4                 if (err) {
 5                     res.writeHead(500);
 6                     res.end('服务器出错!');
 7                     return;
 8                 }
 9                 var headers = {'Content-type': mimeTypes[path.extname(f)]};
10                 response.writeHead(200, headers);
11                 response.end(data);
12             });
13             return;
14         }
15         res.writeHead(404);
16     });
View Code

三、处理文件更改

我们给静态服务器添加了缓存处理,但是如果我们的文件有修改过的话,我们所访问到的就不会跟着更新了,因此我们还需要对文件的状态进行一些状态处理,在Nodejs官方文档,有一个fs.stat接口,里面的例子中给我们列举了stat所返回的一些数据:
使用util.inspect(stats),就会得到下面的字符串:

 1     { dev: 2114,
 2       ino: 48064969,
 3       mode: 33188,
 4       nlink: 1,
 5       uid: 85,
 6       gid: 100,
 7       rdev: 0,
 8       size: 527,
 9       blksize: 4096,
10       blocks: 8,
11       atime: Mon, 10 Oct 2011 23:24:11 GMT,
12       mtime: Mon, 10 Oct 2011 23:24:11 GMT,
13       ctime: Mon, 10 Oct 2011 23:24:11 GMT }
View Code

关键的地方是atime,mtime,ctime三个值,atime对应的最近一次的访问时间,mtime对应的是上一次编辑过的时间,ctime对应的是上一次修改过的时间。mtime和ctime的区别在于,mtime所反映的仅仅是文件中文档的变化,若是对文件的可读写操作进行了修改,这只会更改ctime的值而不会更改mtime的值。因此,一般情况下,我们直接使用ctime就可以了。如果文件的ctime比当前时间大的话,那么我们就用fs.readFile重新读取文件,并加以缓存,否则仍然读取原来的缓存文件。

看到这个ctime是与缓存对象cache关联的,因此,我们需要修改cache为下面的形式:

 1     cache[f] = {content: data,
 2         timestamp: Date.now() 
 3     };
 4     通过fs.stat读取文件状态,
 5     fs.stat(f, function(err, stat){
 6         if (stat.ctime > cache[f].timestamp) {
 7             //如果文件的ctime大于缓存中的时间戳,则重新读取文件
 8             fs.readFile(f, function(err, data){
 9                 //读出数据后进行缓存
10             });
11         } else {
12             //直接读取缓存
13         }
14     });
View Code

我们只需要将这个处理逻辑放在cacheFile函数中即可,完整的代码如下:

 1     function cacheFile(f, cb){
 2         fs.stat(f, function(err, stats){
 3             //stats.ctime的值为:'Mon, 10 Oct 2011 23:24:11 GMT'的形式,因此需要进行解析
 4             var lastChanged = Date.parse(stats.ctime);
 5             isUpdated = (cache[f] && lastChanged > cache[f].timestamp);
 6             if (!cache[f] || isUpdated) {
 7                 fs.readFile(f, function(err, data){
 8                     if (!err) {
 9                         cache[f] = {content: data};
10                     }
11                     cb(err, data);
12                 });
13                 return;
14             }
15             cb(null, cache[f].content);
16         });
17     }
18     //...
19     http.createServer(function (req, res) {
20         var lookup = url.parse(decodeURI(req.url)).pathname;
21         lookup = path.normalize(lookup);
22         lookup = (lookup === "\\") ? 'index.html' : lookup.slice(1);
23         var f = STATIC + lookup;
24         var suffix = path.extname(lookup).slice(1);
25         fs.exists(f, function(exists){
26             if (exists) {
27                 cacheFile(f, function(err, data){
28                     if (err) {
29                         res.writeHead(500);
30                         res.end('服务器出错!');
31                         return;
32                     }
33                     var headers = {'Content-type': mimeTypes[suffix]};
34                     response.writeHead(200, headers);
35                     response.end(data);
36                 });
37                 return;
38             }
39             res.writeHead(404);
40         });
41     }).listen(8080);
View Code

四、读取速度的优化

当我们用fs.readFile来读取文件的时候,文件会先被读取到内存中,然后再被响应输出。这个过程我们其实可以跳过中间的环节,直接将文件以流的形式输出给响应对象。

在nodejs中,我们可以用fs.createReadStream来初始化一个流,考虑到流需要跟请求和响应直接交互,因此为了代码的简约,我们不会在cacheFile中修改成读取流的形式,而是直接在createServer中进行处理。

http.createServer(function(req, res){
          var lookup = url.parse(decodeURI(req.url)).pathname;
         lookup = path.normalize(lookup);
          lookup = (lookup === "\\") ? 'index.html' : lookup.slice(1);
          var f = STATIC + lookup;
          var suffix = path.extname(lookup).slice(1);
          fs.exists(f, function(err, exists){
              var header = {'Content-type': mimeTypes[suffix]};
              if (exists) {
                 if (cache[f]) {
                     res.writeHead(200, header);
                     res.end(cache[f].content);
                 } else {
                     var s = fs.createReadStream(f).once('open', function(){
                         res.writeHead(200, header);
                        //直接输出给res对象
                         this.pipe(res);
                     }).on('error', function(err){
                         res.writeHead(500);
                         res.end('server error!');
                     });
                     //读取完成后,还需要进行缓存,需要通过fs.stat来获取文件大小信息
                     fs.stat(f, function(err, stats){
                         var bufferOffset = 0,
                             cache[f].content = new Buffer(stats.size);
                         s.on('data', function(err, chunk){
                             chunk.copy(cache[f].content, bufferOffset);
                             bufferOffset += chunk.length;
                         });
                     });
                 }
             } 
             res.writeHead(404);
            res.end('file not exists!');
         });
     }).listen(8080);
View Code

 

完成上面的基本步骤之后,我们就可以继续着前面的内容,给它添砖加瓦了,加上文件限制大小,加上过期时间。
文件大小的限制很简单,我们有了fs.stat来得到文件的状态信息中就包括有文件的大小属性,通过它我么就能够轻而易举的实现缓存文件的大小限制,另外缓存时间的处理上,我们可以用当前时间减去文件最近的缓存时间是否超过我们预设的一个固定值就可以了,如果超过了这个值,那我们就将这个缓存文件删除。文件最近一次的访问时间我们需要将他作为cache[f]的一个状态,以方便记录和访问。

 1     //处理文件的限制大小
 2     fs.stat(f, function(err, stats){
 3         if (maxSize > stats.size){
 4             //...the same
 5         }
 6     });
 7     //cache保留时间处理,在原来代码的条件判断中修改代码:
 8     if(cache[f]){
 9         res.writeHead(200, header);
10         res.end(cache[f].content);
11         if (Date.now > maxAge+cache[f].timestamp){
12             delete cache[f];
13         }
14     }
View Code

但这样的修改对cache保留时间的处理似乎有些不妥,每次都是在访问某个文件的时候再去处理该文件的缓存问题,这会导致我们访问到的文件可能已经过期,因此我们可以更加优化一些,让用户在每次访问服务器的时候都会对所有的cache进行一次处理,这样所有的缓存文件都能实时更新了。最后我们再对cache对象进行一些优化处理。

 1     var cache = {
 2         store: {},
 3         maxSize: 26214400, //1024X1024X25=25mb
 4         maxAge: 5400 * 1000, //基本单位为毫秒
 5         cleanAfter: 7200 * 1000,//设置两小时候清除cache
 6         cleanedAt: 0, //这个值是动态变化的,每次执行cache清除的时候,就会将当前时间值赋给它
 7         clean: function(now) {
 8             if (now - cleanedAt > cleanAfter) {
 9                 var that = this;
10                 Object.keys(this.store).forEach(function (file) {
11                     if (now > that.store[file].timestamp + that.maxAge) {
12                         delete that.store[file];
13                     }
14                 });
15             }
16         }
17     };
View Code

完成后在http.createServer回调函数结尾添加cache清除方法即可

1     http.createServer(function(req, res){
2         //...
3         cache.clean(Date.now());
4     }).listen(8080);
View Code