经过手写文件服务器,说说先后端交互

前言

      最近用node写了一个静态文件服务器(已发布),想经过这个小例子说说先后端基于HTTP协议交互过程当中的一些常见问题。html

代码地址

       https://github.com/alive1541/static-server
       下文中所贴出来的代码都在这个目录下。前端

安装方法

       npm install static-server2 -gnode

node版本

       使用了async函数,支持版本7.6以上git

用法示例

      按照前言的安装法安装到全局后,命令行执行 server-start后,会提示服务启动成功。这时能够访问 localhost:8080,程序有如下两个功能:

托管静态文件

      服务启动成功后能够访问localhost:8080查看根目录下的静态文件。
      命令行启动时能够经过server-start -d来改变根目录。还能够经过-o参数配置主机,-p参数配置端口,-h参数查看帮助。github

文件上传

      支持上传文件,能够经过暂停进行断点续传。算法

说说缓存

      下面我就基于这个例子说说先后端交互过程当中的几个问题。首先说说缓存,下面先上代码。
      这是例子中根目录下index.js文件中的一个方法。这个方法用来过滤请求,若是命中缓存,返回304,未命中则返回新的资源。
      这个函数处理了强制缓存和对比缓存。npm

//缓存处理函数
    handleCatch(req, res, fileStat) {
        //强制缓存
        res.setHeader('Expries', new Date(Date.now() + 30 * 1000).toGMTString())
        res.setHeader('Catch-Control', 'private,max-age=30')
        //对比缓存
        let ifModifiedSince = req.headers['if-modified-since']
        let ifNoneMatch = req.headers['if-none-match']
        let lastModified = fileStat.ctime.toGMTString()
        let eTag = fileStat.mtime.toGMTString()
        res.setHeader('Last-Modified', lastModified)
        res.setHeader('ETag', eTag)
        //任何一个对比缓存头不匹配,则不走缓存
        if (ifModifiedSince && ifModifiedSince != lastModified) {
            return false
        }
        if (ifNoneMatch && ifNoneMatch != eTag) {
            return false
        }
        //当请求中存在任何一个对比缓存头,则返回304,不然不走缓存
        if (ifModifiedSince || ifNoneMatch) {
            res.writeHead(304)
            res.end()
            return true
        } else {
            return false
        }
    }
复制代码

强制缓存

      强制缓存的好处是浏览器不须要发送HTTP请求,通常不常更改的页面都会设置一个较长的强制缓存。
      能够经过清理浏览器缓存和强制刷新页面(ctrl+F5)来跳过它强制请求数据。它主要是靠两个HTTP头来实现。
后端

Cache-Control 和 Expires

      这两个头的做用是同样的。都是告诉浏览器多长时间之内能够不发送请求而是直接使用本地的缓存。Cache-Control是HTTP1.1版本规范,而Expires是HTTP1.0版本规范,因此同时存在的话Catch-Control的优先级更高。
      通常都是像我上面的代码同样,两个都设置。由于低版本浏览器不支持Cache-Control
      此外,Catch-Control还有更加细致的配置项,能够更加精确的进行一些控制,规则以下:浏览器

public:客户端和代理服务器均可缓存
private:仅客户端能够缓存,代理服务器不可缓存
no-cache:禁止强制缓存
no-store:禁止强制缓存和对比缓存
must-revalidation/proxy-revalidation:若是缓存的内容失效,请求必须发送到服务器/代理以进行从新验证
max-age=xxx:缓存的内容将在 xxx 秒后失效缓存

对比缓存

Last-Modified/If-Modified-Since

      Last-Modified是服务器携带的头,它表明这个资源的最后更新时间。
      If-Modified-Since是客户端携带的头。在浏览器中,若是不是第一次请求这个资源浏览器就会发送这个头。前提是上一次服务器返回的头中有Last-Modified,它的值也是上次返回的Last-Modified的值。

Etag/If-None-Match

      这两个头和上面的两个头的目的同样,都是校验资源。它们出现的目的是为了解决上面两个头存在的一些问题。例如:

一、在集群服务器上各个服务器上的文件时间可能不一样。
二、有可能文件作了更新,可是内容没有变化。
三、last-modified时间精度为秒,若是文件存在毫秒级的修改,last-modified不能识别

      ETag是资源标签。若是资源没有变化它就不会变。这样就解决了上面说的三个问题。
      可是ETag解决问题的同时也创造出了新的问题,计算出ETag读取文件内容,这就会耗费额外的性能和时间。因此它并不能彻底取代Last-Modified,须要根据实际须要权衡使用。
      在实际的开发中ETag的算法也各不相同,像我在例子中的直接使用了mtime。

说说压缩

      如图,浏览器每次发送请求都会携带本身支持的压缩类型,最经常使用的两种是gzip和deflate。
      服务端能够根据 Accept-Ecoding头来返回响应的压缩资源,同时设置 Content-Encoding头告诉浏览器你用了什么压缩方式,代码以下:

//处理压缩
    handleZlib(req, res) {
        let acceptEncoding = req.headers['accept-encoding']
        if (/\bgzip\b/g.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'gzip');
            //zlib是node的一个模块
            return zlib.createGzip()
        } else if (/\bdeflate\b/g.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'deflate');
            return zlib.createDeflate()
        } else {
            return null
        }
    }
复制代码

说说断点续传

      先看代码,断点续传的原理就是利用HTTP头中的Range来告诉服务器我所上传的文件的内容区间。固然断点续传在不一样的场景下也有不一样的处理方法。这里只是基于这种简单场景作个示范。
      前端逻辑是这样的:
      一、获取用户要上传的文件
      二、切割文件,获取到要上传的第一部分
      三、调用后台的上传文件接口,上传这一部分
      四、接口返回成功后再切割文件,上传第二部分
      五、每次上传用Range头发送文件的字节区间
      下面是切割文件和xhr上传的代码,完整代码在项目目录/src/template/list.html中(使用了handlebars模版引擎)。

if (end > file.size) {
        end = file.size
    }
    //切割文件
    var blob = file.slice(start, end)
    var formData = new FormData();
    formData.append('filechunk', blob);
    formData.append('filename', file.name);
    //添加Range头
    var range = 'bytes=' + start + '-' + end
    xhr.setRequestHeader('Range', range)
    //发送
    xhr.send(formData);
复制代码

      下面看一下后端的处理逻辑:
      一、获取文件名
      二、经过Range获取文件位置,若是是0开头,说明是第一次上传,删除以前的文件
      三、写入文件
      下面是核心代码:

let path = require('path')
let fs = require('fs')
function handleFile(req, res, fields, files, filepath) {
    //获取文件名
    let name = fields.filename[0]
    //文件读取路径
    let rdPath = files.filechunk[0].path
    //文件写入路径
    let wsPath = path.join(filepath, name)
    //经过range判断上传文件的位置
    let range = req.headers['range']
    let start = 0
    if (range) {
        start = range.split('=')[1].split('-')[0]
    }
    //从multiparty插件中读取文件内容,而后写入本地文件
    let buf = fs.readFileSync(rdPath)
    fs.exists(wsPath, function (exists) {
        //若是是初次上传,删除public下的同名文件
        if (exists && start == 0) {
            fs.unlink(wsPath, function () {
                fs.writeFileSync(wsPath, buf, { flag: 'a+' })
                res.end()
            })
        } else {
            fs.writeFileSync(wsPath, buf, { flag: 'a+' })
            res.end()
        }
    })

}
module.exports = handleFile
复制代码

      我这里处理相对粗糙,实际的项目需求可能不止这么简单,但都是基于Range头作相应的处理,但愿个人描述能对你们有些帮助。

总结

      文章到这里就结束了,上文引用的都是代码片断,只是为了展现处理逻辑,若是有兴趣能够去gitHub查看,程序运行中出现任何问题也欢迎指正。

相关文章
相关标签/搜索