http缓存_http缓存机制 - CSDN
精华内容
参与话题
  • HTTP缓存机制

    千次阅读 2016-07-25 11:03:44
    缓存对于移动端是非常重要的存在。 减少请求次数,减小服务器压力.本地数据读取速度更快,让页面不会空白几百毫秒。在无网络的情况下提供数据。 缓存一般由服务器控制(通过某些方式可以本地控制缓存,比如向...

    缓存对于移动端是非常重要的存在。

    • 减少请求次数,减小服务器压力.
    • 本地数据读取速度更快,让页面不会空白几百毫秒。
    • 在无网络的情况下提供数据。

    缓存一般由服务器控制(通过某些方式可以本地控制缓存,比如向过滤器添加缓存控制信息)。通过在请求头添加下面几个字端:

    Request

    请求头字段 意义
    If-Modified-Since: Sun, 03 Jan 2016 03:47:16 GMT 缓存文件的最后修改时间。
    If-None-Match: "3415g77s19tc3:0" 缓存文件的Etag(Hash)值
    Cache-Control: no-cache 不使用缓存
    Pragma: no-cache 不使用缓存

    Response

    响应头字段 意义
    Cache-Control: public 响应被共有缓存,移动端无用
    Cache-Control: private 响应被私有缓存,移动端无用
    Cache-Control:no-cache 不缓存
    Cache-Control:no-store 不缓存
    Cache-Control: max-age=60 60秒之后缓存过期(相对时间)
    Date: Sun, 03 Jan 2016 04:07:01 GMT 当前response发送的时间
    Expires: Sun, 03 Jan 2016 07:07:01 GMT 缓存过期的时间(绝对时间)
    Last-Modified: Sun, 03 Jan 2016 04:07:01 GMT 服务器端文件的最后修改时间
    ETag: "3415g77s19tc3:0" 服务器端文件的Etag[Hash]值

    正式使用时按需求也许只包含其中部分字段。
    客户端要根据这些信息储存这次请求信息。
    然后在客户端发起请求的时候要检查缓存。遵循下面步骤:

     
    浏览器缓存机制


    注意服务器返回304意思是数据没有变动滚去读缓存信息。
    曾经年轻的我为自己写的网络请求框架添加完善了缓存机制,还沾沾自喜,直到有一天我看到了下面2个东西。(/TДT)/

    Volley&OkHttp

    Volley&OkHttp应该是现在最常用的网络请求库。用法也非常相似。都是用构造请求加入请求队列的方式管理网络请求。

    先说Volley:
    Volley可以通过这个库进行依赖.
    Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及以下版本,使用的是HttpClient。
    Volley的基本用法,网上资料无数,这里推荐郭霖大神的博客
    Volley存在一个缓存线程,一个网络请求线程池(默认4个线程)。
    Volley这样直接用开发效率会比较低,我将我使用Volley时的各种技巧封装成了一个库RequestVolly.
    我在这个库中将构造请求的方式封装为了函数式调用。维持一个全局的请求队列,拓展一些方便的API。

    不过再怎么封装Volley在功能拓展性上始终无法与OkHttp相比。
    Volley停止了更新,而OkHttp得到了官方的认可,并在不断优化。
    因此我最终替换为了OkHttp

    OkHttp用法见这里
    很友好的API与详尽的文档。
    这篇文章也写的很详细了。
    OkHttp使用Okio进行数据传输。都是Square家的。
    但并不是直接用OkHttp。Square公司还出了一个Retrofit库配合OkHttp战斗力翻倍。

    Retrofit&RestAPI

    Retrofit极大的简化了网络请求的操作,它应该说只是一个Rest API管理库,它是直接使用OKHttp进行网络请求并不影响你对OkHttp进行配置。毕竟都是Square公司出品。
    RestAPI是一种软件设计风格。
    服务器作为资源存放地。客户端去请求GET,PUT, POST,DELETE资源。并且是无状态的,没有session的参与。
    移动端与服务器交互最重要的就是API的设计。比如这是一个标准的登录接口。


    Paste_Image.png

    你们应该看的出这个接口对应的请求包与响应包大概是什么样子吧。
    请求方式,请求参数,响应数据,都很清晰。
    使用Retrofit这些API可以直观的体现在代码中。

     
    Paste_Image.png

    然后使用Retrofit提供给你的这个接口的实现类 就能直接进行网络请求获得结构数据。

    注意Retrofit2.0相较1.9进行了大量不兼容更新。google上大部分教程都是基于1.9的。这里有个2.0的教程。

    教程里进行异步请求是使用Call。Retrofit最强大的地方在于支持RxJava。就像我上图中返回的是一个Observable。RxJava上手难度比较高,但用过就再也离不开了。Retrofit+OkHttp+RxJava配合框架打出成吨的输出,这里不再多说。

    网络请求学习到这里我觉得已经到顶了。。

    网络图片加载优化

    对于图片的传输,就像上面的登录接口的avatar字段,并不会直接把图片写在返回内容里,而是给一个图片的地址。需要时再去加载。

    如果你直接用HttpURLConnection去取一张图片,你办得到,不过没优化就只是个BUG不断demo。绝对不能正式使用。
    注意网络图片有些特点:

    1. 它永远不会变
      一个链接对应的图片一般永远不会变,所以当第一次加载了图片时,就应该予以永久缓存,以后就不再网络请求。
    2. 它很占内存
      一张图片小的几十k多的几M高清无码。尺寸也是64*64到2k图。你不能就这样直接显示到UI,甚至不能直接放进内存。
    3. 它要加载很久
      加载一张图片需要几百ms到几m。这期间的UI占位图功能也是必须考虑的。

    说说我在上面提到的RequestVolley里做的图片请求处理(没错我做了,这部分的代码可以去github里看源码)。

    三级缓存

    网上常说三级缓存--服务器,文件,内存。不过我觉得服务器不算是一级缓存,那就是数据源嘛。

    • 内存缓存
      首先内存缓存使用LruCache。LRU是Least Recently Used 近期最少使用算法,这里确定一个大小,当Map里对象大小总和大于这个大小时将使用频率最低的对象释放。我将内存大小限制为进程可用内存的1/8.
      内存缓存里读得到的数据就直接返回,读不到的向硬盘缓存要数据。

    • 硬盘缓存
      硬盘缓存使用DiskLruCache。这个类不在API中。得复制使用。
      看见LRU就明白了吧。我将硬盘缓存大小设置为100M。

        @Override
        public void putBitmap(String url, Bitmap bitmap) {
            put(url, bitmap);
            //向内存Lru缓存存放数据时,主动放进硬盘缓存里
            try {
                Editor editor = mDiskLruCache.edit(hashKeyForDisk(url));
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));
                editor.commit();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
      
        //当内存Lru缓存中没有所需数据时,调用创造。
        @Override
        protected Bitmap create(String url) {
            //获取key
            String key = hashKeyForDisk(url);
            //从硬盘读取数据
            Bitmap bitmap = null;
            try {
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                if(snapShot!=null){
                    bitmap = BitmapFactory.decodeStream(snapShot.getInputStream(0));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }

      DiskLruCache的原理不再解释了(我还解决了它存在的一个BUG,向Log中添加的数据增删记录时,最后一条没有输出,导致最后一条缓存一直失效。)

    • 硬盘缓存也没有数据就返回空,然后就向服务器请求数据。

    这就是整个流程。
    但我这样的处理方案还是有很多局限。

    • 图片未经压缩处理直接存储使用
    • 文件操作在主线程
    • 没有完善的图片处理API

    以前也觉得这样已经足够好直到我遇到下面俩。

    Fresco&Glide

    不用想也知道它们都做了非常完善的优化,重复造轮子的行为很蠢。
    Fresco是Facebook公司的黑科技。光看功能介绍就看出非常强大。使用方法官方博客说的够详细了。
    真三级缓存,变换后的BItmap(内存),变换前的原始图片(内存),硬盘缓存。
    在内存管理上做到了极致。对于重度图片使用的APP应该是非常好的。
    它一般是直接使用SimpleDraweeView来替换ImageView,呃~侵入性较强,依赖上它apk包直接大1M。代码量惊人。

    所以我更喜欢Glide,作者是bumptech。这个库被广泛的运用在google的开源项目中,包括2014年google I/O大会上发布的官方app。
    这里有详细介绍。直接使用ImageView即可,无需初始化,极简的API,丰富的拓展,链式调用都是我喜欢的。
    丰富的拓展指的就是这个
    另外我也用过Picasso。API与Glide简直一模一样,功能略少,且有半年未修复的BUG。

    图片管理方案

    再说说图片存储。不要存在自己服务器上面,徒增流量压力,还没有图片处理功能。
    推荐七牛阿里云存储(没用过其它 π__π )。它们都有很重要的一项图片处理。在图片Url上加上参数来对图片进行一些处理再传输。
    于是(七牛的处理代码)

        public static String getSmallImage(String image){
            if (image==null)return null;
            if (isQiniuAddress(image)) image+="?imageView2/0/w/"+IMAGE_SIZE_SMALL;
            return image;
        }
    
        public static String getLargeImage(String image){
            if (image==null)return null;
            if (isQiniuAddress(image)) image+="?imageView2/0/w/"+IMAGE_SIZE_LARGE;
            return image;
        }
    
        public static String getSizeImage(String image,int width){
            if (image==null)return null;
            if (isQiniuAddress(image)) image+="?imageView2/0/w/"+width;
            return image;
        }

    既可以加快请求速度,又能减少流量。再配合Fresco或Glide。完美的图片加载方案。
    不过这就需要你把所有图片都存放在七牛或阿里云,这样也不错。

    图片/文件上传也都是使用它们第三方存储,它们都有SDK与官方文档教你。
    不过图片一定要压缩过后上传。上传1-2M大的高清照片没意义。

    来自: http://www.jianshu.com/p/3141d4e46240

    展开全文
  • HTTP 缓存机制详解

    千次阅读 2018-08-19 13:08:16
    1、三种方式设置服务器告知浏览器缓存过期时间 2、两种方式校验资源过期 Markdown及扩展 表格 定义列表 代码块 脚注 目录 数学公式 UML 图: 离线写博客 浏览器兼容 什么是 HTTP Cache 我们知道通过...

    HTTP Cache


    什么是 HTTP Cache

    • 我们知道通过网络获取资源缓慢且耗时,需要三次握手等协议与远程服务器建立通信,对于大点的数据需要多次往返通信大大增加了时间开销,并且当今流量依旧没有理想的快速与便宜。对于开发者来说,长久缓存复用重复不变的资源是性能优化的重要组成部分。
    • HTTP 缓存机制就是,配置服务器响应头来告诉浏览器是否应该缓存资源、是否强制校验缓存、缓存多长时间;浏览器非首次请求根据响应头是否应该取缓存、缓存过期发送请求头验证缓存是否可用还是重新获取资源的过程。下面我们就来结合简单的 node 服务器代码(文末)来介绍其中原理。

    关键字

    响应头 (常用)值 说明
    Cache-Control no-cache, no-store, must-revalidate, max-age, public, private 控制浏览器是否可以缓存资源、强制缓存校验、缓存时间
    ETag 文件指纹(hash码、时间戳等可以标识文件是否更新) 强校验,根据文件内容生成精确
    Last-Modified 请求的资源最近更新时间 弱校验, 根据文件修改时间,可能内容未变,不精确
    Expires 资源缓存过期时间 与响应头中的 Date 对比
    请求头 说明
    If-None-Match 缓存响应头中的 ETag 值 发送给服务器比对文件是否更新(精确)
    If-Modified-Since 缓存响应头中的 Last-Modified 值 发送给服务器比对文件是否更新(不精确)

    简单流程图

    这里写图片描述

    代码准备

    • index.html

    • img.png

    • server.js

      为了不影响阅读代码贴在页尾,注意需要自行安装 mime npm包。

    不设置

    • 不设置响应头,则浏览器并不能知道是否应该缓存资源,而是每次都发送新的请求,接受新的资源。
    // strategy['no-cache'](req, res, filePath, stat);
    // strategy['no-store'](req, res, filePath, stat);
    // strategy['cache'](req, res, filePath, stat);
    strategy['nothing'](req, res, filePath, stat);
    

    $ node server.js
    浏览器里输入:localhost:8080/index.html

    • 首次加载
      这里写图片描述
      这里写图片描述
    • 刷新,每次和上面一样的效果,都是重新获取资源。

    明确禁止缓存

    • 设置响应头

    Cache-Control: no-store

    Cache-Control: no-cache, no-store, must-revalidate

    strategy['no-store'](req, res, filePath, stat);
    

    这里写图片描述
    效果和不设置一样,只是明确告诉浏览器禁止缓存资源。

    private与public

    在这里插入图片描述

    • Cache-Control: public 表示一些中间代理、CDN等可以缓存资源,即便是带有一些敏感 HTTP 验证身份信息甚至响应状态代码通常无法缓存的也可以缓存。通常 public 是非必须的,因为响应头 max-age 信息已经明确告知可以缓存了。
    • Cache-Control: private 明确告知此资源只能单个用户可以缓存,其他中间代理不能缓存。原始发起的浏览器可以缓存,中间代理不能缓存。例如:百度搜索时,特定搜索信息只能被发起请求的浏览器缓存。
      这里写图片描述

    缓存过期策略

    一般缓存机制只作用于 get 请求

    1、三种方式设置服务器告知浏览器缓存过期时间

    设置响应头(注意浏览器有自己的缓存替换策略,即便资源过期,不一定被浏览器删除。同样资源未过期,可能由于缓存空间不足而被其他网页新的缓存资源所替换而被删除。):

    • 1、设置 Cache-Control: max-age=1000 //响应头中的 Date 经过 1000s 过期
    • 2、设置 Expires //此时间与本地时间(响应头中的 Date )对比,小于本地时间表示过期,由于本地时钟与服务器时钟无法保持一致,导致比较不精确
    • 3、如果以上均未设置,却设置了 Last-Modified ,浏览器隐式的设置资源过期时间为 (Date - Last-Modified) * 10% 缓存过期时间。

    2、两种方式校验资源过期

    设置请求头:

    • 1、If-None-Match 如果缓存资源过期,浏览器发起请求会自动把原来缓存响应头里的 ETag 值设置为请求头 If-None-Match 的值发送给服务器用于比较。一般设置为文件的 hash 码或其他标识能够精确判断文件是否被更新,为强校验。
    • 2、If-Modified-Since 同样对应缓存响应头里的 Last-Modified 的值。此值可能取得 ctime 的值,该值可能被修改但文件内容未变,导致对比不准确,为弱校验。

    下面以常用设置了 Cache-Control: max-age=100If-None-Match 的图示说明:
    这里写图片描述

    • 1、(以下便于测试,未准确设置为 100s 。)浏览器首次发起请求,缓存为空,服务器响应:
      这里写图片描述

    浏览器缓存此响应,缓存寿命为接收到此响应开始计时 100s 。

    • 2、10s 过后,浏览器再次发起请求,检测缓存未过期,浏览器计算 Age: 10 ,然后直接使用缓存,这里是直接去内存中的缓存,from disk 是取磁盘上的缓存。(这里不清楚为什么,同样的配置,index.html 文件即便有缓存也 304。
      这里写图片描述
    • 3、100s 过后,浏览器再次发起请求,检测缓存过期,向服务器发起验证缓存请求。如果服务器对比文件已发生改变,则如 1;否则不返回文件数据报文,直接返回 304。返回 304 时设置 Age: 0 与不设置效果一样, 猜测是浏览器会自动维护。
      这里写图片描述

    强制校验缓存

    有时我们既想享受缓存带来的性能优势,可有时又不确认资源内容的更新频度或是其他资源的入口,我们想此服务器资源一旦更新能立马更新浏览器的缓存,这时我们可以设置

    Cache-Control: no-cache

    这里写图片描述
    再次发起请求,无论缓存资源有没有过期都发起验证请求,未更新返回 304,否则返回新资源。

    性能优化

    现在一些单页面技术,构建工具十分流行。一般一个 html 文件,每次打包构建工具都会动态默认把众多脚本样式文件打包成一个 bundle.hashxxx.js 。虽然一个 js 文件看似减少了 HTTP 请求数量,但对于有些三方库资源等长期不变的资源可以拆分出来,并设置长期缓存,充分利用缓存性能优势。这时我们完全可以对经常变动的 html 设置 Cache-Control: no-cahce 实时验证是否更新。而对于链接在 html 文件的资源名称均带上唯一的文件指纹(时间戳、版本号、文件hash等),设置 max-age 足够大。资源一旦变动即标识码也会变动,作为入口的 html 文件外链改变,html 变动验证返回全新的资源,拉取最新的外链资源,达到及时更新的效果。老的资源会被浏览器缓存替换机制清除。流程如下:
    这里写图片描述

    期中总结:HTTP 缓存性能检查清单

    • 确保网址唯一:一般浏览器以 Request URL 为键值(区分大小写)缓存资源,不同的网址提供相同的内容会导致多次获取缓存相同的资源。ps:常见的更新缓存的方式:在网址后面来加个 v=1,例如 https://xxx.com?v=1 来更新新的资源,但是这样的更新方式有极大的弊端。
    • 确保服务器提供了验证令牌 ETag :提供资源对比机制。ps:服务器每次验证文件的话,太耗性能,现代前端构建工具都能自动更新文件hash,不需要设置Tag了,直接设置长缓存时间。
    • 确定中间代理可以缓存哪些资源:对于个人隐私信息可以设置 private,对于公共资源例如 CDN 资源可以设置 public
    • 为每个资源设置最佳的缓存寿命:max-age 或 Expires,对于不经常变动或不变的资源设置尽可能大的缓存时间,充分利用缓存性能。
    • 确认网站的层次机构:例如单页面技术,对于频繁更新的主入口 index.html 文件设置较短的缓存周期或 no-cache 强制缓存验证,以确保外链资源的及时更新。
    • 最大限度减少文件搅动:对于要打包合并的文件,应该尽量区分频繁、不频繁变动的文件,避免频繁变动的内容导致大文件的文件指纹变动(后台服务器计算文件 hash 很耗性能),尽量频繁变动的文件小而美,不经常变动例如第三方库资源可以合并减少HTTP请求数量。

    前端工程化

    在这里插入图片描述
    相当,前端的概念只是编写一些页面、样式、简单脚本,然后丢给服务器就可以了,真是简单有趣…
    在这里插入图片描述
    进步青年想,对于不变的资源能不能利用缓存呢,于是有:
    在这里插入图片描述

    304有时也觉得浪费,这个请求我也想省了,未过期就不发请求:
    在这里插入图片描述
    完美!那么问题来了,有新资源我如何实时发布更新呢?
    方案一:查询字符加版本号
    在这里插入图片描述
    更新资源只要更新版本号
    在这里插入图片描述
    问题来了:我可能每次只有一两个文件修改了,我得更新多有文件版本号?!
    在这里插入图片描述
    弊端:更新若干资源必须全部文件版本升级,未变动资源缓存也不能利用了。
    方案二:查询字符加文件哈希
    在这里插入图片描述
    大点的公司服务器肯定不止一个,静态资源需要做集群部署、CDN等。
    在这里插入图片描述
    问题来了:我是先发动态页面,还是先发静态资源?
    在这里插入图片描述

    答案是:都不行!
    弊端:
    1、先发页面,这时用户正好打开了新页面,此时新资源未更新,拉取老的静态资源,并缓存,导致错误。
    2、先发资源,这时新用户正好打开老的页面,拉取新资源,导致出错。
    (熟悉的声音:是你缓存有问题吧,清下缓存…)。
    这就是为什么要等到三更半夜,等用户休息了,先发静态资源,再发动态页面。

    方案三:根据文件内容生成文件名,这就完美解决了文件更新的问题:
    在这里插入图片描述

    非覆盖式更新,改变某文件,生成新的文件并更新页面引用链接一并上传服务新文件,不影响以前用户,又能实时更新文件,完美!

    问题来了,那我怎么写代码,图片、CSS、JS等静态资源怎么去维护,修改了生成新的文件,更新新的外链。。。这就不是人力所能为了。

    前端工程化议题应运而生,欢迎补玉。

    参考

    mozilla:HTTP 缓存

    谷歌有关性能的文字:HTTP 缓存

    node中的缓存机制

    w3c Header定义

    彻底弄懂 Http 缓存机制 - 基于缓存策略三要素分解法

    听说你用webpack处理文件名的hash?那么建议你看看你生成的hash对不对

    附代码

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>HTTP Cache</title>
    </head>
    <body>
        <img src="img.png" alt="流程图">
        <!-- <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> -->
    </body>
    </html>
    

    server.js

    let http = require('http');
    let url = require('url');
    let path = require('path');
    let fs = require('fs');
    let mime = require('mime');// 非 node 内核包,需 npm install
    let crypto = require('crypto');
    
    // 缓存策略
    const strategy = {
        'nothing': (req, res, filePath) => {
            fs.createReadStream(filePath).pipe(res);
        },
        'no-store': (req, res, filePath, stat) => {
            // 禁止缓存
            res.setHeader('Cache-Control', 'no-store');
            // res.setHeader('Cache-Control', ['no-cache', 'no-store', 'must-revalidate']);
            // res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());
            // res.setHeader('Last-Modified', stat.ctime.toGMTString());
    
            fs.createReadStream(filePath).pipe(res);
        },
        'no-cache': (req, res, filePath, stat) => {
            // 强制确认缓存
            // res.setHeader('Cache-Control', 'no-cache');
            strategy['cache'](req, res, filePath, stat, true);
            // fs.createReadStream(filePath).pipe(res);
        },
        'cache': async (req, res, filePath, stat, revalidate) => {
            let ifNoneMatch = req.headers['if-none-match'];
            let ifModifiedSince = req.headers['if-modified-since'];
            let LastModified = stat.ctime.toGMTString();
            let maxAge = 30;
    
            let etag = await new Promise((resolve, reject) => {
                // 生成文件 hash
                let out = fs.createReadStream(filePath);
                let md5 = crypto.createHash('md5');
                out.on('data', function (data) {
                    md5.update(data)
                });
                out.on('end', function () {
                    resolve( md5.digest('hex') );
                });
            });
            console.log(etag);
            if (ifNoneMatch) {
                if (ifNoneMatch == etag) {
                    console.log('304');
                    // res.setHeader('Cache-Control', 'max-age=' + maxAge);
                    // res.setHeader('Age', 0);
                    res.writeHead('304');
                    res.end();
                } else {
                    // 设置缓存寿命
                    res.setHeader('Cache-Control', 'max-age=' + maxAge);
                    res.setHeader('Etag', etag);
                    fs.createReadStream(filePath).pipe(res);
                }
            }
            /*else if ( ifModifiedSince ) {
                if (ifModifiedSince == LastModified) {
                    res.writeHead('304');
                    res.end();
                } else {
                    res.setHeader('Last-Modified', stat.ctime.toGMTString());
                    fs.createReadStream(filePath).pipe(res);
                }
            }*/
            else {
                // 设置缓存寿命
                // console.log('首次响应!');
                res.setHeader('Cache-Control', 'max-age=' + maxAge);
                res.setHeader('Etag', etag);
                // res.setHeader('Last-Modified', stat.ctime.toGMTString());
    
                revalidate && res.setHeader('Cache-Control', [
                    'max-age=' + maxAge,
                    'no-cache'
                ]);
                fs.createReadStream(filePath).pipe(res);
            }
        }
    
    };
    
    http.createServer((req, res) => {
        console.log( new Date().toLocaleTimeString() + ':收到请求')
        let { pathname } = url.parse(req.url, true);
        let filePath = path.join(__dirname, pathname);
        // console.log(filePath);
        fs.stat(filePath, (err, stat) => {
            if (err) {
                res.setHeader('Content-Type', 'text/html');
                res.setHeader('404', 'Not Found');
                res.end('404 Not Found');
            } else {
                res.setHeader('Content-Type', mime.getType(filePath));
    
                // strategy['no-cache'](req, res, filePath, stat);
                // strategy['no-store'](req, res, filePath, stat);
                strategy['cache'](req, res, filePath, stat);
                // strategy['nothing'](req, res, filePath, stat);
            }
        });
    })
    .on('clientError', (err, socket) => {
        socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
    })
    .listen(8080);
    
    
    

    标准配置

    'use strict';
    
    /** deps */
    var path = require('path'),
    	express = require('express'),
    	mime = require('express/lib/express').mime,
    
    	/** cache values */
    	ONE_HOUR = 60 * 60,
    	ONE_WEEK = ONE_HOUR * 24 * 7,
    	ONE_MONTH = ONE_WEEK * 4,
    	ONE_YEAR = ONE_MONTH * 12,
    
    	/** mime type regexps */
    	RE_MIME_IMAGE = /^image/,
    	RE_MIME_FONT = /^(?:application\/(?:font-woff|x-font-ttf|vnd\.ms-fontobject)|font\/opentype)$/,
    	RE_MIME_DATA = /^(?:text\/(?:cache-manifest|html|xml)|application\/(?:(?:rdf\+)?xml|json))/,
    	RE_MIME_FEED = /^application\/(?:rss|atom)\+xml$/,
    	RE_MIME_FAVICON = /^image\/x-icon$/,
    	RE_MIME_MEDIA = /(image|video|audio|text\/x-component|application\/(?:font-woff|x-font-ttf|vnd\.ms-fontobject)|font\/opentype)/,
    	RE_MIME_CSSJS = /^(?:text\/(?:css|x-component)|application\/javascript)/,
    
    	/** misc regexps */
    	RE_WWW = /^www\./,
    	RE_MSIE = /MSIE/,
    	RE_HIDDEN = /(^|\/)\./,
    	RE_SRCBAK = /\.(?:bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)|~/;
    
    // load additional node mime types
    mime.load(path.join(__dirname, 'node.types'));
    
    // apply `ServerResponse` patch
    require('../patch');
    
    /**
     * Configures headers layer.
     * @type {Function}
     */
    module.exports = function(options) {
    	/**
    	 * The actual headers layer, invoked for each request hit.
    	 * Applies all h5bp goodness relative to response headers.
    	 */
    	return function headersLayer(req, res, next) {
    		var url = req.url,
    			pathname = req.path || '/',
    			host = req.headers.host,
    			ua = req.headers['user-agent'],
    			cc = '',
    			type;
    
    		// Block access to "hidden" directories or files whose names begin with a
    		// period. This includes directories used by version control systems such as
    		// Subversion or Git.
    
    		// 隐藏文件,403拒绝访问
    		if (!options.dotfiles && RE_HIDDEN.test(pathname)) {
    			next(403);
    			return;
    		}
    
    		// Block access to backup and source files. These files may be left by some
    		// text/html editors and pose a great security danger, when anyone can access
    		// them.
    
    		// 备份、源文件,403拒绝访问
    		if (RE_SRCBAK.test(pathname)) {
    			next(403);
    			return;
    		}
    
    		/**
    		 * Suppress or force the "www." at the beginning of URLs
    		 */
    
    		// The same content should never be available under two different URLs -
    		// especially not with and without "www." at the beginning, since this can cause
    		// SEO problems (duplicate content). That's why you should choose one of the
    		// alternatives and redirect the other one.
    
    		// By default option 1 (no "www.") is activated.
    		// no-www.org/faq.php?q=class_b
    
    		// If you'd prefer to use option 2, just comment out all option 1 lines
    		// and uncomment option 2.
    
    		// ----------------------------------------------------------------------
    
    		// Option 1:
    		// Rewrite "www.example.com -> example.com".
    
    		// 重定向
    		if (false === options.www && RE_WWW.test(host)) {
    			res.setHeader('Location', '//' + host.replace(RE_WWW, '') + url);
    			next(301);
    			return;
    		}
    
    		// ----------------------------------------------------------------------
    
    		// Option 2:
    		// Rewrite "example.com -> www.example.com".
    		// Be aware that the following rule might not be a good idea if you use "real"
    		// subdomains for certain parts of your website.
    
    		if (true === options.www && !RE_WWW.test(host)) {
    			res.setHeader('Location', '//www.' + host.replace(RE_WWW, '') + url);
    			next(301);
    			return;
    		}
    
    		/**
    		 * Built-in filename-based cache busting
    		 */
    
    		// If you're not using the build script to manage your filename version revving,
    		// you might want to consider enabling this, which will route requests for
    		// /css/style.20110203.css to /css/style.css
    
    		// To understand why this is important and a better idea than all.css?v1231,
    		// read: github.com/h5bp/html5-boilerplate/wiki/cachebusting
    
    		req.baseUrl = req.url;
    		req.url = req.url.replace(/^(.+)\.(\d+)\.(js|css|png|jpg|gif)$/, '$1.$3');
    
    		// Headers stuff!!
    		// Subscribes to the `header` event in order to:
    		//   - let content generator middlewares set the appropriate content-type.
    		//   - "ensures" that `h5bp` is the last to write headers.
    
    		res.on('header', function() {
    			/**
    			 * Proper MIME type for all files
    			 */
    
    			// Here we delegate it to `node-mime` which already does that for us and maintain a list of fresh
    			// content types.
    			//  https://github.com/broofa/node-mime
    
    			type = res.getHeader('Content-Type');
    			// normalize unknown types to empty string
    			if (!type || !mime.extension(type.split(';')[0])) {
    				type = '';
    			}
    
    			/**
    			 * Better website experience for IE users
    			 */
    
    			// Force the latest IE version, in various cases when it may fall back to IE7 mode
    			//  github.com/rails/rails/commit/123eb25#commitcomment-118920
    			// https://www.cnblogs.com/menyiin/p/6527339.html
    
    			// chrome IE壳
    			if (RE_MSIE.test(ua) && ~type.indexOf('text/html')) {
    				res.setHeader('X-UA-Compatible', 'IE=Edge,chrome=1');
    			}
    
    			/**
    			 * Cross-domain AJAX requests
    			 */
    
    			// Serve cross-domain Ajax requests, disabled by default.
    			// enable-cors.org
    			// code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
    
    			// cors 跨域
    			if (options.cors) {
    				res.setHeader('Access-Control-Allow-Origin', '*');
    			}
    
    			/**
    			 * CORS-enabled images (@crossorigin)
    			 */
    
    			// Send CORS headers if browsers request them; enabled by default for images.
    			// developer.mozilla.org/en/CORS_Enabled_Image
    			// blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
    			// hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
    			// wiki.mozilla.org/Security/Reviews/crossoriginAttribute
    
    			// 图片可跨域
    			if (RE_MIME_IMAGE.test(type)) {
    				res.setHeader('Access-Control-Allow-Origin', '*');
    			}
    
    			/**
    			 * Webfont access
    			 */
    
    			// Allow access from all domains for webfonts.
    			// Alternatively you could only whitelist your
    			// subdomains like "subdomain.example.com".
    
    			// 字体可跨域
    			if (RE_MIME_FONT.test(type) || '/font.css' == pathname) {
    				res.setHeader('Access-Control-Allow-Origin', '*');
    			}
    
    			/**
    			 * Expires headers (for better cache control)
    			 */
    
    			// These are pretty far-future expires headers.
    			// They assume you control versioning with filename-based cache busting
    			// Additionally, consider that outdated proxies may miscache
    			//   www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
    
    			// If you don't use filenames to version, lower the CSS and JS to something like
    			// "access plus 1 week".
    
    			// note: we don't use express.static maxAge feature because it does not allow fine tune
    
    			// Perhaps better to whitelist expires rules? Perhaps.
    
    			// cache.appcache needs re-requests in FF 3.6 (thanks Remy ~Introducing HTML5)
    			// Your document html
    			// Data
    			if (!type || RE_MIME_DATA.test(type)) {
    				cc = 'public,max-age=0';
    			}
    			// Feed
    			else if (RE_MIME_FEED.test(type)) {
    				cc = 'public,max-age=' + ONE_HOUR;
    			}
    			// Favicon (cannot be renamed)
    			else if (RE_MIME_FAVICON.test(type)) {
    				cc = 'public,max-age=' + ONE_WEEK;
    			}
    			// Media: images, video, audio
    			// HTC files  (css3pie)
    			// Webfonts
    			else if (RE_MIME_MEDIA.test(type)) {
    				cc = 'public,max-age=' + ONE_MONTH;
    			}
    			// CSS and JavaScript
    			else if (RE_MIME_CSSJS.test(type)) {
    				cc = 'public,max-age=' + ONE_YEAR;
    			}
    			// Misc
    			else {
    				cc = 'public,max-age=' + ONE_MONTH;
    			}
    
    			/**
    			 * Prevent mobile network providers from modifying your site
    			 */
    
    			// The following header prevents modification of your code over 3G on some
    			// European providers.
    			// This is the official 'bypass' suggested by O2 in the UK.
    
    			//no-siteapp
    			// 禁止网站转码
    			cc += (cc ? ',' : '') + 'no-transform';
    			res.setHeader('Cache-Control', cc);
    
    			/**
    			 * ETag removal
    			 */
    
    			// Since we're sending far-future expires, we don't need ETags for
    			// static content.
    			//   developer.yahoo.com/performance/rules.html#etags
    
    			// 干掉Tag,避免浪费服务资源,良好的缓存机制既能做到实时正确更新又能尽可能利用缓存优势
    			res.removeHeader('ETag');
    
    			/**
    			 * Stop screen flicker in IE on CSS rollovers
    			 */
    
    			// The following directives stop screen flicker in IE on CSS rollovers - in
    			// combination with the "ExpiresByType" rules for images (see above).
    
    			// TODO
    
    			/**
    			 * Set Keep-Alive Header
    			 */
    
    				// Keep-Alive allows the server to send multiple requests through one
    				// TCP-expression. Be aware of possible disadvantages of this setting. Turn on
    				// if you serve a lot of static content.
    
    			// 保持长联,减少多回三次握手带来的性能损失,但要有可靠的超时机制
    			res.setHeader('Connection', 'keep-alive');
    
    			/**
    			 * Cookie setting from iframes
    			 */
    
    			// Allow cookies to be set from iframes (for IE only)
    			// If needed, specify a path or regex in the Location directive.
    
    			// TODO
    
    			/**
    			 * A little more security
    			 */
    
    			// do we want to advertise what kind of server we're running?
    
    			if ('express' == options.server) {
    				res.removeHeader('X-Powered-By');
    			}
    		});
    
    		next(null, req, res);
    	};
    };
    
    
    展开全文
  • HTTP三种缓存方式

    千次阅读 2018-09-04 12:50:35
    转载自 三种缓存方式,再也不用麻烦运维小哥哥了!!! 转载自浏览器缓存技术介绍 依然在学习node的艰辛过程中,最近学习了http相关的知识,学到了东西当然第一时间就来和大家分享分享,今天呢就教大家来看看利用...

    转载自 三种缓存方式,再也不用麻烦运维小哥哥了!!!

    转载自浏览器缓存技术介绍

    依然在学习node的艰辛过程中,最近学习了http相关的知识,学到了东西当然第一时间就来和大家分享分享,今天呢就教大家来看看利用node中的http模块去实现不同的缓存策略!!!

    我们都知道,对于我们前端开发来说,缓存是一个十分重要的东西,即希望用户不能每次请求过来都要重复下载我们的页面内容,希望为用户节省流量,并且能提高我们页面的浏览流畅度,但是同时当我们修改了一个bug后,又希望线上能够及时更新,这时候就要求爷爷告奶奶让运维小哥哥帮我们刷新一下缓存了,那么有没有一些比较好的缓存策略可以针对我们修改bug又能不麻烦运维及时更新呢,今天我们就利用node来看一下后端中的缓存策略是如何设置的。

    强制缓存

    通常我们对于强制缓存的设置是服务端告诉客户端你刚刚已经请求过一次了,我们约定好十分钟内你再过来请求都直接读取缓存吧,意思也就是当客户端在十分钟内多次请求的话只有第一次会下载页面内容,其他的请求都是直接走缓存,不管我们页面在这期间有没有变化都不会影响客户端读取缓存。 那我们来看一下代码的实现

    let http = require('http');
    let path = require('path');
    let fs = require('fs');
    let url = require('url');
    // 创建一个服务
    let server = http.createServer();
    // 监听请求
    server.on('request',(req,res)=>{
        // 获取到请求的路径
        let {pathname,query} = url.parse(req.url,true);
        // 将路径拼接成服务器上对应得文件路径
        let readPath = path.join(__dirname, 'public',pathname);
        console.log(readPath)
        try {
            // 获取路径状态
            let statObj = fs.statSync(readPath);
            // 服务端设置响应头 Cache-Control 也就是缓存多久以秒为单位
            res.setHeader('Cache-Control','max-age=10');
            // 服务器设置响应头Expires 过期时间 获取当前时间加上刚刚设置的缓存秒数
            res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString());
            //判断如果路径是一件文件夹 就默认查找该文件下的index.html
            if(statObj.isDirectory()){
                let p = path.join(readPath,'index.html');
                console.log(p);
                // 判断是否有index.html 没有就返回404
                fs.statSync(p);
                // 创建文件可读流 并且pipe到响应res可写流中
                fs.createReadStream(p).pipe(res)
            }else{
                // 如果请求的就是一个文件 那么久直接返回
                fs.createReadStream(readPath).pipe(res)
            }
        } catch (error) {
            // 读取不到 返回404 
            console.log(error)
            res.setHeader('Content-Type','text/html;charset=utf8')
            res.statusCode = 404;
            res.end(`未发现文件`)
        }
    })
    // 监听3000端口
    server.listen(3000)
    复制代码复制代码

    通过上面代码测试我们会发现当我们在10秒内进行对同一文件的请求,那么我们浏览器就会直接走缓存 通过上图可以看到我们重复请求的时候我们会看到css变成from memory cache,我们也看到我们刚刚的响应头也被设置上了

    协商缓存

    上面的强制缓存我们就发现了 就是我们平时改完bug上线要苦苦等待的一个原因了,那么有没有其他的好的缓存处理方法呢,我们设想一下 假如我们能够知道我们文件有没有修改,假如我们修改了服务器就返回最新的内容假如没有修改 就一直默认缓存 ,这样是不是听起来十分的棒!那我们就想如果我们能够知道文件的最后修改时间是不是就可以实现了!

    通过文件最后修改时间来缓存

    let http = require('http');
    let path = require('path');
    let fs = require('fs');
    let url = require('url');
    let server = http.createServer();
    server.on('request',(req,res)=>{
        // 获取到请求的路径
        let {pathname,query} = url.parse(req.url,true);
        // 将路径拼接成服务器上对应得文件路径
        let readPath = path.join(__dirname, 'public',pathname);
        try {
            // 获取路径状态
            let statObj = fs.statSync(readPath);
            // 为了方便测试 我们告诉客户端不要走强制缓存了
            res.setHeader('Cache-Control','no-cache');
            if(statObj.isDirectory()){
                let p = path.join(readPath,'index.html');
                let statObj = fs.statSync(p);
                // 我们通过获取到文件状态来拿到文件的最后修改时间 也就是ctime 我们把这个时间通过响应头Last-Modified来告诉客户端,客户端再下一次请求的时候会通过请求头If-Modified-Since把这个值带给服务端,我们只要判断这两个值是否相等,假如相等那么也就是说 文件没有被修改那么我们就告诉客户端304 你直接读缓存吧
                res.setHeader('Last-Modified',statObj.ctime.toGMTString());
                if(req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
                    res.statusCode = 304;
                    res.end();
                    return
                }
                // 修改了那么我们就直接返回新的内容
                fs.createReadStream(p).pipe(res)
            }else{
                res.setHeader('Last-Modified',statObj.ctime.toGMTString());
                if(req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
                    res.statusCode = 304;
                    res.end();
                    return
                }
                fs.createReadStream(readPath).pipe(res)
            }
        } catch (error) {
            console.log(error)
            res.setHeader('Content-Type','text/html;charset=utf8')
            res.statusCode = 404;
            res.end(`未发现文件`)
        }
    })
    复制代码

    server.listen(3000) 复制代码复制代码

    我们通过请求可以看到,当我们第一次请求过后,无论怎么刷新请求都是304 直接读取的缓存,假如我们在服务端把这个文件修改了 那么我们就能看到又能请求到最新的内容了,这就是我们通过协商缓存来处理的,我们通过获取到文件状态来拿到文件的最后修改时间 也就是ctime 我们把这个时间通过响应头Last-Modified来告诉客户端,客户端再下一次请求的时候会通过请求头If-Modified-Since把这个值带给服务端,我们只要判断这两个值是否相等,假如相等那么也就是说 文件没有被修改那么我们就告诉客户端304 你直接读缓存吧

    通过文件内容来缓存

    再再再再再假如我们在文件中删除了字符a然后又还原了,那么这时候保存我们的文件的修改时间其实也发生了变化,但是其实我们文件的真正内容并没有发生变化,所以这时候其实客户端继续走缓存也是可以的 ,我们来看看这样的缓存策略如何实现。

    let http = require('http');
    let path = require('path');
    let fs = require('fs');
    let url = require('url');
    let crypto = require('crypto');
    let server = http.createServer();
    server.on('request',(req,res)=>{
        // 获取到请求的路径
        let {pathname,query} = url.parse(req.url,true);
        // 将路径拼接成服务器上对应得文件路径
        let readPath = path.join(__dirname, 'public',pathname);
        try {
            // 获取路径状态
            let statObj = fs.statSync(readPath);
            // 为了方便测试 我们告诉客户端不要走强制缓存了
            res.setHeader('Cache-Control','no-cache');
            if(statObj.isDirectory()){
                let p = path.join(readPath,'index.html');
                let statObj = fs.statSync(p);
                // 我们通过流把文件读取出来 然后对读取问来的内容进行md5加密 得到一个base64加密hash值
                let rs = fs.createReadStream(p);
                let md5 = crypto.createHash('md5');
                let arr = [];
                rs.on('data',(data)=>{
                    arr.push(data);
                    md5.update(data);
                })
                rs.on('end',(data)=>{
                    let r = md5.digest('base64');
                    // 然后我们将这个hash值通过响应头Etag传给客户端,客户端再下一次请求的时候会把上一次的Etag值通过请求头if-none-match带过来,然后我们就可以继续比对文件生成的hash值和上次产生的hash是否一样 如果一样说明文件内容没有发生变化 就告诉客户端304 读取缓存
                    res.setHeader('Etag',r);
                    if(req.headers['if-none-match']===r){
                        res.statusCode=304;
                        res.end();
                        return;
                    }
                    res.end(Buffer.concat(arr))
                })
            }else{
                let rs = fs.createReadStream(readPath);
                let md5 = crypto.createHash('md5');
                let arr = [];
                rs.on('data',(data)=>{
                    arr.push(data);
                    md5.update(data);
                })
                rs.on('end',(data)=>{
                    let r = md5.digest('base64');
                    res.setHeader('Etag',r);
                    if(req.headers['if-none-match']===r){
                        res.statusCode=304;
                        res.end();
                        return;
                    }
                    res.end(Buffer.concat(arr))
                })
            }
        } catch (error) {
            console.log(error)
            res.setHeader('Content-Type','text/html;charset=utf8')
            res.statusCode = 404;
            res.end(`未发现文件`)
        }
    })
    复制代码

    server.listen(3000) 复制代码复制代码

    通过控制台我们可以看出来 请求头和响应头中都有我们上面所说的对应的值,但是从代码里我们也能看出来,我们每次在请求到来的时候都会把文件全部读取出来并且进行加密生产hash然后再做对比,这样其实十分的消耗性能,因此这种缓存方式也有他自己的缺点

    总结

    我们通过node来亲自实现了三种缓存方式,我们可以总结出每种缓存方式对应的实现:

    • 强制缓存 服务端设置响应头Cache-Control:max-age=xxx,并且设置Expires响应头过期时间,客户端自行判断是否读取缓存
    • 协商缓存 通过状态码304告诉客户端该走缓存
      • 修改时间:通过文件的最后修改时间判断该不该读取缓存,服务端设置响应头Last-Modified,客户端把上次服务端响应头中的Last-modified值通过if-modified-since 传递给服务端 , 服务端通过比较当前文件的修改时间和上次修改时间(上次传给客户端的值),如果相等那么说明文件修改时间没变也就是没变化
      • 文件内容:通过文件的内容来判断该不该读取缓存,服务端通过把文件内容读取出来,通过md5进行base64加密得出hash值,把这个值设置响应头Etag,客户端下一次请求通过if-none-match带过来,服务端再比对当前文件内容加密得出的hash值和上次是否一样,如果一样说明文件内容没有发生改变,这种方式是最准确的方式,但是也是最耗性能
    展开全文
  • HTTP缓存

    2018-09-14 08:39:47
    起源:通过网络获取内容既缓慢,成本又高。大的响应需要在客户端和服务器之间进行多次往返通信,...借助HTTP缓存,web站点变得更具有响应性。 缓存:缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。...

    起源:通过网络获取内容既缓慢,成本又高。大的响应需要在客户端和服务器之间进行多次往返通信,这样拖延了浏览器可以使用和处理内容的时间,同时也增加了访问者的数据成本。所以缓存和重用已获取的资源能够有效地提升网站与应用的性能。web缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助HTTP缓存,web站点变得更具有响应性。

    缓存:缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。对于网站来说,缓存是达到高性能的重要组成部分。

    HTTP报文中与缓存相关的首部字段(RFC2616):

    1.通用首部字段(即请求报文和响应报文都能用的字段)

    字段名称   说明
    Cache-Control 控制缓存的行为
    Pragma http1.0,值为“no-cache”时禁用缓存

    2.请求首部字段

    字段名称 说明
    If-Match 比较ETag是否一致
    If-None-Match 比较ETag是否不一致
    If-Modified-Since 比较资源最后更新的时间是否一致
    If-Unmodified-Since 比较资源最后更新的时间是否不一致

    3.响应头部字段

    字段名称 说明
    ETag 资源的匹配信息

    4.实体首部字段

    字段名称 说明
    Expires http1.0,实体主体过期的时间
    Last-Modified 资源的最后一次修改的时间

    当下大多数浏览器在点击刷新按钮或者按F5时会自行加上“Cache-Control:max-age=0”请求字段,后文提及的“刷新”多指的是选中URL地址栏并按回车键(这样不会被强行加上Cache-Control)。

    石器时代的缓存方式(HTTP1.0)

    在http1.0时代,给客户端设定缓存方式可通过两个字段——“Pragma”和“Expires”来规范。虽然这两个字段早可抛弃,但为了做http协议的向下兼容,很多网站依旧会带上这两个字段。

    1.Pragma

    当该字段值为“no-cache”的时候,会告诉客户端不要对该资源读缓存,即每次都得向服务器发一次请求。Pragma属于通用首部字段,在客户端上使用时,常规要求我们往html上加上这段meta元标签:

    <meta http-equiv="Pragma" content="no-cache">

    它告诉浏览器每次请求页面时不要读取缓存。但是这种禁用缓存的形式有以下缺陷:

    (1)仅IE能识别这段meta标签含义,其他主流浏览器仅能识别“Cache-Control:no-store”的meta标签

    (2)在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发送新请求。

    因此,这种客户端定义Pragma的形式作用不大。但是在响应报文上加上该字段有效。

    2.Expires

    有了Pragma来禁用缓存,就需要有个东西来启用缓存和定义缓存时间,对于http1.0来说,Expires就是做这件事的首部字段。

    Expires的值对应一个GMT(格林尼治时间),比如“Mon,22 Jul 2002 11:22:01 GMT”告诉浏览器资源缓存过期时间,如果没过该时间,则不发请求。

    在客户端我们同样可以使用meta标签来告诉IE(只有IE识别)页面缓存时间:

    <meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

    如果希望在IE页面不走缓存,每次刷新页面都能发送新请求,那么可以把“content”的值写成“-1”或“0”。同样的,你并不能在请求或响应报文中找到Expires字段。但是如果在服务端设置Expires字段,则在任何浏览器中都能正确设置资源缓存的时间。

    同时在服务端定义Pragma字段和Expires字段时,Pragma的优先级更高。但响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致,那缓存时间可能就没有意义了。

    HTTP1.1

    1.Cache-Control

    针对http1.0中“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了Cache-Control来定义缓存过期时间,若报文中同时出现了Pragma、Expires、Cache-Control,会以Pragma为准。也就是说优先级从高到低:Pragma<-Cache-Control<-Expires

    Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。RFC规范Cache-Control的格式为:

    "Cache-Control"":"cache-directive

    作为请求首部,cache-directive的可选值有:

    字段名称 说明
    no-cache 告知(代理)服务器不直接使用缓存,要求向原服务器发起请求
    no-store 所有内容都不会被保存到缓存或internet临时文件中
    max-age=delta-seconds 告知服务器客户端希望接收一个存在时间(Age)不大于delta-seconds秒的资源
    max-stale=[=delta-seconds] 告知(代理)服务器客户端愿意接收一个超过缓存时间的资源,若有定义delta-seconds则为delta-seconds秒,若没有则为任意超出的时间
    min-fresh=delta-seconds 告知(代理)服务器客户端希望接收一个在小于delta-seconds秒内被更新过的资源
    no-transform 告知(代理)服务器客户端希望获取实体数据没有被转换(比如被压缩)过的资源
    only-if-cache 告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求
    cache-extension 自定义扩展值,若服务器不识别改值将被忽略掉

    作为响应首部时,cache-directive的可选值为:

    字段名称    说明
    public 表明任何情况下都得缓存该资源(即使是需要HTTP认证的资源)
    Private[="field-name"] 表明返回报文中全部或部分仅开放给某些用户做缓存使用,其他用户不能缓存这些数据
    no-cache 不直接使用缓存,要求向服务器发起请求
    no-store 所有内容都不会被保存到缓存或Internet临时文件中
    no-transform 告知客户端缓存文件时不得对实体数据做任何改变
    only-if-cached 告知(代理)服务器客户端希望获取缓存的内容,而不用向原服务器发起请求
    must-revalidate 当前资源一定是向原服务器发去验证请求的,若请求失败会返回504(Gateway Time-out)
    proxy-revalidate    与must-revalidate类似,但仅能应用于共享缓存
    max-age=delta-seconds 告知客户端该资源在delta-seconds秒内是新鲜的,无需向服务器发请求
    s-maxage=delta-seconds 同max-age,但仅应用于共享缓存
    cache-extension 自定义扩展值,若服务器不识别该值就会被忽略

    我们依旧可以在HTML的meta标签来给请求报头加上Cache-Control字段,另外,Cache-Control允许自由组合可选值:

    Cache-Control:max-age:3600,must-revalidate

    它表示该资源是从原服务器上获取的,且其缓存(新鲜度)的有效时间为1小时,在后续1小时内,用户重新访问该资源无需发送请求。

    2.缓存校验字段

    上述的首部字段均能让客户端决定是否向服务器发送请求。如果设置的缓存时间未过期,则直接从本地缓存读取数据即可,若缓存时间过期了或资源不直接走缓存,则会发送请求道服务器。

    现在的问题是,如果客户端向服务器发送了请求,是否就一定要读取该资源的整个实体内容?针对案例——客户端上某个资源保存的缓存时间过期了,但这时候其实服务器并没有更新过这个资源,如果这个资源数据量很大,客户端要求服务器再把这个东西重新发送一遍,就非常浪费时间和带宽。但是,如果想办法让服务器知道客户端存有的缓存文件其实是跟自己所有的文件是一致的,然后高速客户端“这东西你直接用缓存里就可以了,我这边还没更新,就不再传一次过去了”。

    为了让客户端和服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,Http1.1新增了几个首部字段来做这件事。

    2.1Last-Modified

    服务器将资源传递给客户端时,会将字原最后更改的时间以“Last-Modified GMT”的形式加在实体首部上一起返回给客户端。

    客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并发给服务器做检查,若传递的时间值与服务器上该资源最终修改时间一致,这说明该资源没有被修改过,直接返回304状态码(Not-Modified)

    (1)If-Modified-Since:Last-Modified-value

    该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上一直,则直接返回304和响应包头即可

    (2)If-Unmodified-Since:Last-Modified-value

    高速服务器若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应返回412(Precondition Failed).

    2.2ETag

    Last-Modified并不是没有缺点,如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回整个实体给客户端。

    为了解决上述Last-Modified可能存在的不准确的问题,Http1.1推出了ETag实体首部字段。

    服务器会通过某种算法给资源计算出一个唯一的标志符,再把资源响应给客户端的时候,在实体首部加上“ETag:唯一标识符”一起返回给客户端。

    客户端会保留该ETag字段,并在下一次请求时将其一并发给服务器。服务器只需比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能判断出该资源相对客户端是否被修改过。若服务器匹配ETag不一致,则直接以常规GET 200将新的资源发给客户端;若匹配一致,则直接返回304告知客户端直接使用本地缓存即可。

    (1)If-None-Match:ETag-value

    客户端请求首部字段,告诉服务器如果ETag没有匹配上需要重新发送资源数据,否则直接返回304和响应报头

    (2)If-Match:ETag-value

    客户端请求首部字段,告诉服务器如果没有匹配到ETag,或收到了“*”而当前没有该资源实体,则返回412(Preconditioned Failed)给你客户端,否则服务器直接忽略该字段。

    缓存实践

    在实际做http缓存的应用时,我们还是会把上述的大多数首部字段均用上,使用Expires来兼容旧的浏览器,使用Cache-Control来更精确的利用缓存,然后开启ETag和Last-Modified功能进一步复用缓存减少流量。

    缓存头部对比

    头部 优势和特点 劣势和问题
    Expires

    1.HTTP1.0产物,可以在HTTP1.0和HTTP1.1中使用,简单易用

    2.以时刻标识失效时间

    1.时间是由服务器发送的(UTC),如果服务器时间和客户端事件存在不一致,可能出现问题

    2.存在版本问题,到期之前的修改客户端是不可知的

    Cache-Control

    1.HTTP1.1的产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题

    2.比Expires多了很多选项设置

    1.HTTP1.1才有的内容,不适用于HTTP1.0

    2.存在版本问题,到期之前的修改客户端是不可知的

    Last-Modified 1.不存在版本问题,每次请求都会去服务器进行校验,服务器对比最后修改时间,如果相同则返回304,否则返回200以及资源内容

    1.只要资源修改,无论内容是否发生实质性的变化, 都会将该资源返回给客户端

    2.以时刻作为标识,无法识别一秒内进行多次修改的情况

    3.某些服务器不能精确的得到文件最后修改时间

    ETag

    1.可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况

    2.不存在版本问题,每次请求都会去服务器进行校验

    1.计算ETag值需要性能损耗

    2.分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另一台服务器上进行验证时发现ETag不匹配的情况

    用户刷新/访问行为

    我们可以把刷新/访问界面的手段分成三类:

    (1)在URL输入栏中输入然后回车/通过书签访问

    (2)F5/点击工具栏中的刷新按钮/右键菜单重新加载

    (3)Ctrl+F5

    针对不同的刷新行为返回状态码总结:

    • 200(OK)——当浏览器本地没有缓存或者下一层失效,或者用户点击了Ctrl+F5时,浏览器直接去服务器下载最新数据
    • 304(Not-Modified)——这一层由Last-Modified/ETag控制。当下一层失效时或用户点击刷新,或者F5时,浏览器就会发送请求给服务器,如果服务端没有变化,则返回304给浏览器
    • 200(OK, from cache)——这一层由Expires/Cache-Control控制。Expires(http1.0有效)是绝对时间,Cache-Control(http1.1有效)是相对时间,两者都存在时,Cache-Control覆盖Expires,只要没有失效,浏览器只访问自己的缓存。

     

    from cache/304

    Expires和Cache-Control都有一个问题就是服务端作为的修改,如果还在缓存时效里,那么客户端是不会去请求服务端资源的(非刷新),这就存在一个资源版本不符的问题。而强制刷新一定会发起HTTP请求并返回资源内容,无论该内容在这段时间内是否修改过;而Last-Modified和ETag每次请求资源都会发起请求,哪怕是很久都不会有修改的资源,都至少有一次请求响应的消耗。

    对于所有可缓存资源,指定一个Expires或Cache-Control max-age以及一个Last-Modified或ETag至关重要。同时使用前者和后者可以很好的相互适应。前者不需要每次都发起一次请求来校验资源的时效性,后者保证当资源未出现修改的时候不需要重新发送该资源。而在用户的不同刷新页面行为中,二者的结合也能很好地利用HTTP缓存控制特性,无论在地址栏输入URL然后输入回车键进行访问,还是点击刷新,浏览器都能充分利用缓存内容,避免进行不必要的请求与数据传输。

    避免304

    把服务侧ETag的那套理论搬到前端来使用。页面的静态资源以版本形式发布,常用的方法是在文件名或参数带上一串md5或时间标记符。

    结论:

    • 需要兼容HTTP1.0的时候需要使用Expires,不然可以直接考虑使用Cache-Control
    • 需要处理一秒内多次修改的情况,或者其他Last-Modified处理不了的情况,才使用ETag,否则使用Last-Modified
    • 对于所有可缓存资源,需要指定一个Expires或Cache-Control或者ETag
    • 可以通过标识文件版本名、加长缓存时间的方式来减少304响应

     

    /爱的分割线\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

    啊,上面讲了那么多,都是HTTP相关,下面来总结一下各种类型的缓存吧!

    正如本文开头所说,缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。

    各种类型的缓存

    根据是否需要重新向服务器发起请求,可以把HTTP缓存分为:强制缓存(也叫本地缓存,强缓存)协商缓存(也叫对比缓存,比较缓存)

    1)浏览器加载资源时,先根据这个资源的一些http header判断它是否命中强制缓存(Cache-Control(优先级高),Expires),强制缓存如果命中,浏览器直接从自己的缓存中读取资源,不会发送请求到服务器。

    2)当强制缓存没有命中(缓存过期)时,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些http header(Last-Modified/If-Modified-Since,ETag/If-None-Match(优先级高))验证这个资源是否命中协商缓存,如果协商缓存命中(服务器返回状态码304),服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就会从自己的缓存中加载这个资源

    3)强制缓存与协商缓存的共同点是:如果命中,都是从客户端中加载资源,而不是从服务器加载资源数据;区别是:强制缓存不发请求到服务器,协商缓存会发请求到服务器。

    4)当协商缓存也没有命中(服务器返回状态码200 OK)时,浏览器直接从服务器加载资源数据。

    浏览器第一次请求时:

    浏览器再次请求时:

    协商缓存

    顾名思义,就是需要进行比较判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识和数据一起返回给客户端,客户端将二者备份到缓存数据库中。再次请求数据时,客户端将备份的缓存标识(Last-Modified/If-Modified-Since,ETag/If-None-Match(优先级高于Last-Modified))发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

    强制缓存

    服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存。不在时间内,进行协商缓存策略。

    ///

    另外,根据缓存是否能够被多个用户使用,可归为两类:私有缓存共享缓存。共享缓存存储的响应能够被多个用户使用。私有缓存只能用于单独用户。

    其他分类:

    1.数据库缓存

    memcached是一种数据库层面的缓存方案。

    数据库缓存是指,当web应用的关系比较复杂,数据库中的表很多的时候,如果频繁地进行数据库查询,很容易导致数据库不堪重负。为了提供查询的性能,将查询后的数据放到内存中进行缓存,下次查询可以直接从内存中返回,提供响应效率。

    2.CDN缓存

    CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。通常情况下,浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,浏览器和服务器之间的缓存机制,在这种架构下同样适用

    3.代理服务器缓存

    代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。

    4.浏览器缓存

    每个浏览器都实现了 HTTP 缓存,我们通过浏览器使用HTTP协议与服务器交互的时候,浏览器就会根据一套与服务器约定的规则进行缓存工作。

    5.应用层缓存

    应用层缓存是指我们在代码层面上做的缓存。通过代码逻辑,把曾经请求过的数据或资源等,缓存起来,再次需要数据时通过逻辑上的处理选择可用的缓存的数据。

    //

    写在最后,本文照搬了多篇博主的文章,望海涵!

    展开全文
  • HTTP协议缓存原理解析

    千次阅读 2017-06-22 19:26:35
    导语 在web项目开发中,我们可能都曾... 有些web开发经验的同学应该马上会想到,可能是资源发布出了岔子导致没有实际发布成功,更大的可能是老的资源被缓存了。说到web缓存,首先我们要弄清它是什么。Web缓存可以理解
  • HTTP 缓存详解

    千次阅读 2018-04-29 01:40:42
    HTTP缓存主要用在对一些实时性要求不高的静态文件进行的缓存,往往都是存在浏览器端,防止这些“多余”的请求重复的访问服务器,对服务器造成压力,从而提高网站的性能。 原理 现有两端,浏览器C和服务器端S。 ...
  • Http 30x

    千次阅读 2014-12-01 16:47:31
    301重定向是指当用户或搜索引擎向网站服务器发出浏览请求时,服务器返回的HTTP数据流中头信息(header)中的状态码的一种,表示本网页永久性转移到另一个地址。       301重定向可促进搜索引擎优化效果...
  • http 缓存策略

    2019-10-24 13:27:49
    浏览器第一次向一个web服务器发起http请求后,服务器会返回请求的资源,并且在响应头中添加一些有关缓存的字段如:Cache-Control、Expires、Last-Modified、ETag、Date等等。之后浏览器再向该服务器请求该资源就可以...
  • 彻底弄懂HTTP缓存机制及原理

    千次阅读 2017-09-01 11:04:15
    前言Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,...在此,我会尝试用简单明了的文字,像大家系统的介绍HTTP缓存机制,期望对各位正确的理解前端
  • 浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,所以在分析浏览器缓存机制之前,我们先使用图文简单介绍一下HTTP报文,HTTP报文分为两种: 同步sau交流学习社区(首发):...
  • 在工作之余阅读缓存相关的书籍时,看到了http缓存相关的知识,HTTP 缓存机制是一个 web 性能优化的重要手段,无论是做前端还是做web后台,都可能会用得到它,应该是知识体系库中的一个基础环节,以前这一块学的不是...
  • Http缓存机制与原理

    千次阅读 多人点赞 2018-04-21 19:01:00
    Http缓存基本概念 1.1 Http报文 在浏览器和服务器进行Http通信时发送的数据即为Http报文,其中分为两部分: 1. header - 报文的首部或头部,其中保存着各类请求的属性字段,关于Http的缓存相关规则信息均...
  • Volley HTTP 缓存机制

    千次阅读 2016-05-30 20:05:46
    Volley HTTP 缓存规则在介绍Volley的HTTP缓存机制之前,我们首先来看一下HTTP HEADER中和缓存有关的字段有: 规则 字段 示例值 类型 作用 新鲜度 Expires Sat, 23 Jul 2016 03:34:17 GMT 响应 告诉客户端在...
  • Http 缓存机制作为 web 性能优化的重要手段,对从事 Web 开发的小伙伴们来说是必须要掌握的知识,但最近我遇到了几个缓存头设置相关的题目,发现有好几道题答错了,有的甚至在知道了正确答案后依然不明白其原因,...
  • HTTP缓存过程详解

    千次阅读 2020-02-25 11:37:00
    HTTP缓存过程详解 http缓存作为面试常考题目,我们必须理解他的详细过程,接下来请大家务必理解并牢记这篇博客的所有内容,大家也不要闲内容多,因为http不管是在面试还是在工作中,真的很重要。而且在面试中,我们...
  • 面试的时候被人问到了这个,实在是答不出来,自己只...所谓的http缓存,就是浏览器自己给你的一个功能,一个缓存数据库,夹在服务端和客户端中间,你只需要设置一些参数即可实现 缓存/不缓存/时效内缓存/时效外缓...
  • http缓存与cdn缓存配置指南

    千次阅读 2018-11-08 08:33:50
    合理的http缓存与cdn缓存配置可以起到减轻服务器压力,缓解网络瓶颈,提升用户体验等作用,不当的缓存配置却会导致资源无法及时更新,用户体验差异,甚至流程出错等问题。本文主要讲解http缓存与cdn缓存的原理和配置...
  • http缓存策略之强缓存与协商缓存

    千次阅读 2020-04-11 16:45:25
    前言: web中有些场景下很多内容是不需要更改的,如果每次请求都向服务器请求那些一段时间内不会变动的内容数据,会造成不必要的带宽浪费。...http缓存指的是:当客户端向服务器请求资源时,会先查...
  • Http 缓存机制作为 web 性能优化的重要手段,对从事 Web 开发的小伙伴们来说是必须要掌握的知识,但最近我遇到了几个缓存头设置相关的题目,发现有好几道题答错了,有的甚至在知道了正确答案后依然不明白其原因,...
  • java http缓存

    千次阅读 2015-09-23 10:28:55
    HTTP/1.1中缓存的目的是为了在很多情况下减少发送请求,也即直接返回缓存;同时在许多情况下可以不需要发送完整响应。前者减少了网络回路的数量,挺高响应速度,HTTP利用一个“过期(expiration)”机制来为此目的。...
1 2 3 4 5 ... 20
收藏数 909,057
精华内容 363,622
关键字:

http缓存