Web Server之处理静态文件

来源:互联网 发布:js基础知识定义变量 编辑:程序博客网 时间:2024/05/13 10:40

Web Server之处理静态文件

标签(空格分隔): webserver


细心的同学会发现,在express server的代码中,包含这么一句app.use('/static', express.static('public'));,什么用呢?这个是用来处理静态文件,所有访问/static及其子目录下的url均会检查静态文件,如果静态文件存在,则直接返回静态资源。
是不是很简单,但是我们还得深挖一下,看这货到底是怎么实现的。

handle static file

我们还是通过原生态的httpServer,新建一个静态文件的处理流程。

var http = require("http");var url = require("url");var fs = require("fs");var mimeType = require("mime-types");http.createServer(function(req, res){    var obj = url.parse(req.url);    var pathname = obj.pathname;    if(pathname.indexOf("/static")===0){        var fileName = __dirname + pathname;        fs.stat(fileName, function(err, is){            if(err){                console.error(err);                res.writeHead(500, "Error occur.");                res.end();            }            else{                if(is.isFile()){                    fs.readFile(fileName, function(err, buf){                        if(err){                            console.error(err);                            res.writeHead(500, "Read file error.");                            res.end();                        }                        else{                            var tempObject = {                                "Content-Type" : mimeType.lookup(pathname),                                "Last-Modified": is.mtime.toLocaleString(),                                "Content-Length": is.size                            };                            res.writeHead(200, tempObject);                            res.write(buf);                            res.end();                        }                    });                }                else{                    res.writeHead(404, "Not Found");                    res.end();                }            }        });    }    else{        res.writeHead(200);        res.write("Hello!");        res.end();    }}).listen(3001);

代码解析

  1. 获取模块httpurlfsmime-types
  2. 新建一个httpServer实例,并绑定端口3001.
  3. 定义httpServer的处理逻辑,对于/static开头的请求,均按照静态文件进行处理,如果文件存在,则将文件内容返回值Response对象,否则则返回404;

API介绍

看着代码,两眼一抹黑!这是什么鬼,怎么会如此之多,而且req、res这是啥,怎么中知道哪些方法能用,哪些属性能访问。这个我只能告诉你,平时多看书,需要时自觉上网查资料。此处我们不妨列举一下用到的对象。

http.Server

通过http.createServer创建一个Server实例
方法

server.close([callback]) server不在接收新的端口
server.listen(param, [callback]) 监听端口或者一个Unix Socker path。
其他,具体参考https://nodejs.org/api/http.html#http_class_http_server

事件

checkContinue
clientError
close
connect
connection
request
upgrade

http.ServerResponse

http请求一旦建立,会产生一个request实例(IncomingMessage)和response实例(http.ServerResponse),下面介绍一下常用的方法。
方法

response.end(data, [encoding], [callback]) 通过该方法告诉服务器所有的header和body均已经发送。因此每个http请求均需要调用该方法来结束处理,否则服务器将持续等待。
response.finished 在调用end方法之前,该值为false,调用之后该值为false,可以通过该方法判断response是否调用end()方法。
response.getHeader(name) 获取指定header的值
response.removeHeader(name) 删除指定header
response.setHeader(name, value)
response.writeHead(code[, codeMessage][, header]) 发送header,包括状态码,调用此方法之后headersSent将置为true。
response.headerSent 只读,表示headers是否被发送。
response.write(chuck, [encoding], [callback]) 异步发送数据。

事件

close
finish

http.IncomingMessage

request的类。
方法

request.destroy([error])
request.headers 一个object对象,内部的key使用小写格式,如content-type、last-modified等
request.httpVersion http版本,如1.1/1.0
request.method http使用的方法,如GET、POST、DELETE等
request.rawHeaders 原始header,request.headers是经过滤重处理的,而request.rawHeaders未经过任何处理。
request.url, url属性,可以通过url模块处理该字段。

fs.Stats

通过fs.stat(filePath, callback)可以在callback中得到一个Stats对象,Stats对象包括的属性和方法如下:
属性&方法

stats.isFile() 是否文件
stats.iDirectory() 是否文件夹
stats.isBlockDevice() 是否块设备
stats.isCharacterDevice() 是否字符设备
stats.isSymbolicLink() 是否链接
stats.isFIFO() 是否管道文件
stats.isSocket() 是否Socket文件

属性(节选)

size 文件大小,单位字节
blksize: 块大小,单位字节
atime: access time,Date对象
mtime: modified time, Date对象,上次更改时间,
ctime: change time, Date对象,上次更改时间,重命名,更改权限等均能修改该属性。
birthtime: Birth Time,创建时间。

url.parse方法

一个url通过url.parse方法会返回一个object对象,对象格式如下:

{    href: "full url",    protocol : "", // 协议    slashes: true | false, //     host: "", // hostname + port, 如 "host.com:8080"    auth: "", // 授权信息,如URL(http://)"user:pass",     hostname: "", // host信息,不包括端口,    port: number, // 端口信息,如http的80, ftp的21    pathname: "", // 文件地址,如abc/d/e.txt    search: "", // query部分,如?query=string    path: "" // pathname + search集合}

更深一步

对于静态资源而言,一但生成,修改的可能性一般都不大,在实际操作过程,我们可以利用缓存以减少带宽的压力。
一般地,浏览器会根据header中的字段,判断是否利用缓存。因此,我们可以将代码略做修改:
>
1. 获取request中的If-Modified-Since字段,如果非空,比当前文件的last modified time进行比较,如果If-Modified-Since小于last modified time,进入第3步,否则直接返回304.
2. 获取request中的If-None-Match字段,如果字段非空,且If-None-Match等于文件的MD5值,直接返回304,否则进入第3步。
3. 读取文件,设置Response的Last-Modified、Expires、Cache-Control、ETag字段。

// 处理修改时间var lastModifiedTime = is.mtime;var ifModifiedSince = req.headers["if-modified-since"];if(ifModifiedSince){    ifModifiedSince = new Date().strtotime(ifModifiedSince);    if(!(ifModifiedSince instanceof Date)){        ifModifiedSince = null;    }}if(ifModifiedSince && ifModifiedSince.getTime() >= lastModifiedTime.getTime()){    res.writeHead(304);    res.end();    return ;}// 处理etagvar et = eTag(is);var ifNoneMatch = req.headers["if-none-match"];if(ifNoneMatch && et === ifNoneMatch){    res.writeHead(304);    res.end();    return ;}// 如果modified和etag校验均失败,返回静态文件内容。var tempObject = {    "Expires": new Date().strtotime("30 days").format("ddd, dd mmm yyyy HH:MM:ss Z"),    "Content-Type" : mimeType.lookup(pathname),    "Last-Modified": lastModifiedTime.format('ddd, dd mmm yyyy HH:MM:ss Z'),    "eTag": et,    "Content-Length": is.size,    "Cache-Control": "public,max-age=2592000000"};res.writeHead(200, tempObject);res.write(buf);res.end();

代码分析

  1. 获取request header中的If-Modified-Since值,并与当前文件的Last Mofidified Time比较;如果If-Modified-Since大于等于Last Mofidified Time,则表示服务器文件并未更新,直接返回304;
  2. 通过etag包的方法当前文件的etag值,并获取request header中If-None-Match值,两者对比;
  3. 上述比配均失败时,获取文件内容,并在header中写入Expires、Last-Modified、Etag。

再深一步

我们可以通过浏览器验证一下,执行如下代码:

var loadJS = function(src){    var script = document.createElement("script");    script.type = "text/javascript";    script.src = src;    document.body.appendChild(script);}

然后执行loadJS("http://localhost:3001/static/js/index.js");10次。监控网络状况,可以发现如下情况:
image_1ap7d9nvh1mqno6r1f3hft114869.png-85.4kB
image_1ap7dabhu1g9mefl6jevplt3om.png-63.4kB
浏览器只发送了一次请求!!!
此时我们再使用浏览器打开http://localhost:3001/static/js/index.js,发现网络请求如下:
image_1ap7df9qg9ls402ts3tou1ihi13.png-8.1kB
没有意外,直接返回304。使用调试工具,查看网络请求,如下图:
image_1ap7dgjf8j0klelun11c5o19qb1g.png-88.8kB
我们可以看出,在request请求中包含了两个header
* If-Modified-Since
* If-None-Match

那么问题来了,浏览器为什么会只发送一个请求:
当浏览器首次获取资源时,资源返回内容并附带Expires,Cache-Control,Last-Modified,ETag;当浏览器再次请求时,浏览器根据Expires、Cache-Control,判断是否从cache中返回,此例中,直接从cache中返回,http状态码为200;此后无论发送多少次请求,只要Cache没有过期,均从cache中获取。

附:浏览器缓存操作:
image_1ap9oe6hi6121j44he9al11c6e1t.png-219.6kB

参考文献

  • Node.js API: https://nodejs.org/dist/latest-v4.x/docs/api/
  • 浏览器缓存: http://www.alloyteam.com/2012/03/web-cache-2-browser-cache/
1 0