• 用NodeJS打造你的静态文件服务器(上)


    本文为原创文章,出自http://cnodejs.org,转载请注明出处和作者
    作者:Jackson
    原文:http://cnodejs.org/blog/?p=3904

    前言

    在《The Node Beginner Book》的中文版(http://nodebeginner.org/index-zh-cn.html)发布之后,获得国内的好评。也有同学觉得这本书略薄,没有包含进阶式的例子。@otakustay同学说:“确实,我的想法是在这之上补一个简单的MVC框架和一个StaticFile+Mimetype+CacheControl机制,可以成为一个更全面的教程”。正巧的是目前我手里的V5项目有一些特殊性:

    1. 项目大多数的文件都是属于静态文件,只有数据部分存在动态请求
    2. 数据部分的请求都呈现为RESTful的特性

    那么我之前写的Node_CI框架跟V5搭配起来感觉就有那么一点点怪怪的。所以我决定改造Node_CI框架,使之更适合V5前端的使用。原有的Node_CI项目继续保留着,新开项目为V5Node,同时在改造这个框架的过程完成@otakustay 同学提到的几点进阶部分,也算是对我自己学习Node的总结。

    这个项目主要包含的两个部分就是静态服务器和RESTful服务器。

    第一部分 静态文件服务器

    既是一个新的项目,那么创建v5node目录是应该的。既是一个Node应用,创建一个app.js文件也是应该的。

    如果你有认真读完《The Node Beginner Book》或是看到过Nodejs官方网站上的那段经典代码,那么你对下面这段代码应当是非常不陌生的。

    1
    2
    3
    4
    5
    6
    var http = require("http");
    http.createServer(function(request, response) {
      response.writeHead(200, {"Content-Type""text/plain"});
      response.write("Hello World");
      response.end();
    }).listen(8888);

    那么我们的app.js文件里的结构也很明确了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var PORT = 8000;
     
    var http = require('http');
    var server = http.createServer(function(request, response) {
    // TODO
    });
     
    server.listen(PORT);
    console.log("Server runing at port: " + PORT + ".");

    因为当前要实现的功能是静态文件服务器,那么以Apache为例,让我们回忆一下静态文件服务器都有哪些功能。

    浏览器发送URL,服务端解析URL,对应到硬盘上的文件。如果文件存在,返回200状态码,并发送文件到浏览器端;如果文件不存在,返回404状态码,发送一个404的文件到浏览器端。

    以下两图是Apache经典的两种状态。

    现在cases已经明了,那么我们开始实现吧。

    实现路由

    路由部分的实现在《The Node Beginner Book》已经被描述过,此处不例外。

    添加url模块是必要的。然后解析pathname。

    1
    2
    var url = require("url");
    var pathname = url.parse(request.url).pathname;

    以下是实现代码:

    1
    2
    3
    4
    5
    var server = http.createServer(function(request, response) {
        var pathname = url.parse(request.url).pathname;
        response.write(pathname);
        response.end();
    });

    现在的代码是向浏览器端输出请求的路径,类似一个echo服务器。接下来我们为其添加输出对应文件的功能。

    读取静态文件

    为了不让用户在浏览器端通过请求/app.js查看到我们的代码,我们设定用户只能请求assets目录下的文件。服务器会将路径信息映射到assets目录。

    涉及到了文件读取的这部分,自然不能避开fs(file system)这个模块。那么引入fs模块吧。

    1
    var fs = require("fs");

    同样,涉及到了路径处理,path模块也是需要的。

    1
    var path = require("path");

    我们通过path模块的path.exists方法来判断静态文件是否存在磁盘上。不存在我们直接响应给客户端404错误。

    如果文件存在则调用fs.readFile方法读取文件。如果发生错误,我们响应给客户端500错误,表明存在内部错误。正常状态下则发送读取到的文件给客户端,表明200状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    var server = http.createServer(function(request, response) {
        var pathname = url.parse(request.url).pathname;
        var realPath = "assets" + pathname;
     
        path.exists(realPath, function (exists) {
            if (!exists) {
                response.writeHead(404, {'Content-Type''text/plain'});
                response.write("This request URL " + pathname + " was not found on this server.");
                response.end();
            else {
                fs.readFile(realPath, "binary"function(err, file) {
                    if (err) {
                        response.writeHead(500, {'Content-Type''text/plain'});
                        response.end(err);
                    else {
                        response.writeHead(200, {'Content-Type''text/html'});
                        response.write(file, "binary");
                        response.end();
                    }
                 });
              }
          });
    });

    以上这段简单的代码加上一个assets目录,就构成了我们最基本的静态文件服务器。

    那么眼尖的你且看看,这个最基本的静态文件服务器存在哪些问题呢?答案是MIME类型支持。因为我们的服务器同时要存放html, css, js, png, gif, jpg等等文件。并非每一种文件的MIME类型都是text/html的。

    MIME类型支持

    像其他服务器一样,支持MIME的话,就得一张映射表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    exports.types = {
      "css""text/css",
      "gif""image/gif",
      "html""text/html",
      "ico""image/x-icon",
      "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"
    };

    以上代码另存在mime.js文件中。该文件仅仅只列举了一些常用的MIME类型,以文件后缀作为key,MIME类型为value。那么引入mime.js文件吧。

    1
    var mime = require("./mime").types;

    我们通过path.extname来获取文件的后缀名。由于extname返回值包含”.”,所以通过slice方法来剔除掉”.”,对于没有后缀名的文件,我们一律认为是unknown。

    1
    2
    var ext = path.extname(realPath);
    ext = ext ? ext.slice(1) : 'unknown';

    接下来我们很容易得到真正的MIME类型了。

    1
    2
    3
    4
    5
    6
    var ext = path.extname(realPath);
    ext = ext ? ext.slice(1) : 'unknown';
    var contentType = mime[ext] || "text/plain";
    response.writeHead(200, {'Content-Type': contentType});
    response.write(file, "binary");
    response.end();

    对于未知的类型,我们一律返回text/plain类型。

    缓存支持/控制

    在MIME支持之后,静态文件服务器看起来已经很完美了。任何静态文件只要丢进assets目录之后就可以万事大吉不管了。看起来已经达到了Apache作为静态文件服务器的相同效果了。我们实现这样的服务器用的代码只有这么多行而已。是不是很简单呢?

    但是,我们发现用户在每次请求的时候,服务器每次都要调用fs.readFile方法去读取硬盘上的文件的。当服务器的请求量一上涨,硬盘IO会吃不消的。

    在解决这个问题之前,我们有必要了解一番前端浏览器缓存的一些机制和提高性能的方案。

    1. Gzip压缩文件可以减少响应的大小,能够达到节省带宽的目的。
    2. 浏览器缓存中存有文件副本的时候,不能确定有效的时候,会生成一个条件get请求
      1. 在请求的头中会包含 If-Modified-Since
      2. 如果服务器端文件在这个时间后发生过修改,则发送整个文件给前端。
      3. 如果没有修改,则返回304状态码。并不发送整个文件给前端。
      4. 另外一种判断机制是ETag。在此并不讨论。
      5. 如果副本有效,这个get请求都会省掉。判断有效的最主要的方法是服务端响应的时候带上Expires的头。
        1. 浏览器会判断Expires头,直到制定的日期过期,才会发起新的请求。
        2. 另一个可以达到相同目的的方法是返回Cache-Control: max-age=xxxx。

    欲了解更多缓存机制,请参见Steve Sounders著作的《高性能网站建设指南》。

    为了简化问题,我们只做如下这几件事情:

    1. 为指定几种后缀的文件,在响应时添加Expires头和Cache-Control: max-age头。超时日期设置为1年。
    2. 由于这是静态文件服务器,为所有请求,响应时返回Last-Modified头。
    3. 为带If-Modified-Since的请求头,做日期检查,如果没有修改,则返回304。若修改,则返回文件。

    对于以上的静态文件服务器,Node给的响应头是十分简单的:

    1
    2
    3
    Connection: keep-alive
    Content-Type: text/html
    Transfer-Encoding:  chunked

    那么我们搞起吧。

    对于指定后缀文件和过期日期,为了保证可配置。那么建立一个config.js文件是应该的。

    1
    2
    3
    4
    exports.Expires = {
        fileMatch: /^(gif|png|jpg|js|css)$/ig,
        maxAge: 60*60*24*365
    };

    引入config.js文件。

    1
    var config = require("./config");

    我们在相应之前判断后缀名是否符合我们要添加过期时间头的条件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var ext = path.extname(realPath);
    ext = ext ? ext.slice(1) : 'unknown';
     
    if (ext.match(config.Expires.fileMatch)) {
        var expires = new Date();
        expires.setTime(expires.getTime() + config.Expires.maxAge * 1000);
        response.setHeader("Expires", expires.toUTCString());
        response.setHeader("Cache-Control""max-age=" + config.Expires.maxAge);
    }

    这次的响应头中多了两个header。

    1
    2
    3
    4
    5
    Cache-Control: max-age=31536000
    Connection: keep-alive
    Content-Type: image/png
    Expires: Fri, 09 Nov 2012 12:55:41 GMT
    Transfer-Encoding: chunked

    浏览器在发送请求之前由于检测到Cache-Control和Expires(Cache-Control的优先级高于Expires,但有的浏览器不支持Cache-Control,这时采用Expires),如果没有过期,则不会发送请求,而直接从缓存中读取文件。

    接下来我们为所有请求的响应都添加Last-Modified头。

    读取文件的最后修改时间是通过fs模块的fs.stat()方法来实现的。关于stat的详细介绍请参见此处:http://www.cnitblog.com/guopingleee/archive/2008/11/13/51411.aspx

    1
    2
    3
    4
    fs.stat(realPath, function (err, stat) {
        var lastModified = stat.mtime.toUTCString();
        response.setHeader("Last-Modified", lastModified);
    });

    我们同时也要检测浏览器是否发送了If-Modified-Since请求头。如果发送而且跟文件的修改时间相同的话,我们返回304状态。

    1
    2
    3
    4
    if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) {
        response.writeHead(304, "Not Modified");
        response.end();
    }

    如果没有发送或者跟磁盘上的文件修改时间不相符合,则发送回磁盘上的最新文件。

    此时的代码大致如下:

    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    var server = http.createServer(function(request, response) {
        var pathname = url.parse(request.url).pathname;console.log(pathname);
        var realPath = path.join("assets", pathname);
     
        path.exists(realPath, function (exists) {
            if (!exists) {
                response.writeHead(404, "Not Found", {'Content-Type''text/plain'});
                response.write("This request URL " + pathname + " was not found on this server.");
                response.end();
            else {
                var ext = path.extname(realPath);
                ext = ext ? ext.slice(1) : 'unknown';
                var contentType = mime[ext] || "text/plain";
                response.setHeader("Content-Type", contentType);
     
                fs.stat(realPath, function (err, stat) {
                    var lastModified = stat.mtime.toUTCString();
                    var ifModifiedSince = "If-Modified-Since".toLowerCase();
                    response.setHeader("Last-Modified", lastModified);
     
                    if (ext.match(config.Expires.fileMatch)) {
                        var expires = new Date();
                        expires.setTime(expires.getTime() + config.Expires.maxAge * 1000);
                        response.setHeader("Expires", expires.toUTCString());
                        response.setHeader("Cache-Control""max-age=" + config.Expires.maxAge);
                    }
     
                    if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) {
                        response.writeHead(304, "Not Modified");
                        response.end();
                    else {
                        fs.readFile(realPath, "binary"function(err, file) {
                            if (err) {
                                response.writeHead(500, "Internal Server Error", {'Content-Type''text/plain'});
                                response.end(err);
                            else {
                                response.writeHead(200, "Ok");
                                response.write(file, "binary");
                                response.end();
                            }
                        });
                    }
                });
            }
        });
    });

    通过Expires和Last-Modified两个方案以及与浏览器之间的通力合作,会节省相当大的一部分网络流量,同时也会降低部分硬盘IO的请求。如果在这之前还存在CDN的话,整个solution就比较完美了。

    由于Expires和Max-Age都是由浏览器来进行判断的,如果判断成功,http请求都不会发送到服务端的,这里只能通过fiddler和浏览器配合进行测试。但是Last-Modified却是可以通过curl来进行测试的。

    1
    curl --header "If-Modified-Since: Fri, 11 Nov 2011 19:14:51 GMT" -i <a href="http://localhost:8000" rel="nofollow">http://localhost:8000</a>

    结果:

    1
    2
    3
    4
    HTTP/1.1 304 Not Modified
    Content-Type: text/html
    Last-Modified: Fri, 11 Nov 2011 19:14:51 GMT
    Connection: keep-alive

    注意,我们看到这个304请求的响应是不带body信息的。所以,达到我们节省带宽的需求。只需几行代码,就可以替老板省下许多的带宽费用,咱们程序员是有力量的。

  • 相关阅读:
    一个别人的心得(转发的)
    常见的游戏设计技术
    查看更新
    xml,json和各种序列化工具的对比
    python游戏环境搭建
    快速制作游戏
    子网和掩码
    nat
    pycharm使用技巧
    IP的面向无连接状态
  • 原文地址:https://www.cnblogs.com/marryZhan/p/2268283.html
Copyright © 2020-2023  润新知