通过手写文件服务器,说说前后端交互

前言

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

代码地址

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

安装方法

       npm install static-server2 -g

node版本

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

用法示例

操作演示.gif

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

托管静态文件

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

文件上传

      支持上传文件,可以通过暂停进行断点续传。

说说缓存

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

//缓存处理函数
    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的优先级更高。
      一般都是像我上面的代码一样,两个都设置。因为低版本浏览器不支持<code>Cache-Control</code>
      此外,Catch-Control还有更加细致的配置项,可以更加精确的进行一些控制,规则如下:

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

对比缓存

Last-Modified/If-Modified-Since

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

Etag/If-None-Match

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

1、在集群服务器上各个服务器上的文件时间可能不同。
2、有可能文件做了更新,但是内容没有变化。
3、last-modified时间精度为秒,如果文件存在毫秒级的修改,last-modified不能识别

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

说说压缩

压缩.jpg

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

//处理压缩
    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头中的<code>Range</code>来告诉服务器我所上传的文件的内容区间。当然断点续传在不同的场景下也有不同的处理方法。这里只是基于这种简单场景做个示范。
      前端逻辑是这样的:
      1、获取用户要上传的文件
      2、切割文件,获取到要上传的第一部分
      3、调用后台的上传文件接口,上传这一部分
      4、接口返回成功后再切割文件,上传第二部分
      5、每次上传用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);

      下面看一下后端的处理逻辑:
      1、获取文件名
      2、通过Range获取文件位置,如果是0开头,说明是第一次上传,删除之前的文件
      3、写入文件
      下面是核心代码:

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

      我这里处理相对粗糙,实际的项目需求可能不止这么简单,但都是基于<code>Range</code>头做相应的处理,希望我的描述能对大家有些帮助。

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容