转载

一文搞懂http缓存

1、http缓存

浏览器第一次向一个web服务器发起 http 请求后,服务器会返回请求的资源,并且在响应头中添加一些有关缓存的字段如: Cache-ControlExpiresLast-ModifiedETagDate 等等。之后浏览器再向该服务器请求该资源就可以视情况使用 强缓存协商缓存

  • 强缓存:浏览器直接从本地缓存中获取数据,不与服务器进行交互。
  • 协商缓存:浏览器发送请求到服务器,服务器判定是否可使用本地缓存。
  • 联系与区别:两种缓存方式最终使用的都是本地缓存;前者无需与服务器交互,后者需要。

下面假定浏览器已经访问了服务器,服务器返回了缓存相关的头部字段且浏览器已对相关资源做好缓存。通过下图来分析强缓存和协商缓存:

一文搞懂http缓存

1.1、强缓存

强缓存由两个http响应头部字段控制, ExpiresCache-Control ,其中 Cache-Control 的优先级比 Expires 高。

一、 Cache-Control

  • max-age (单位为s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在max-age这段时间里浏览器就不会再向服务器发送请求了。

    max-age>0
    max-age<=0
    
  • no-cache :设置了no-cache之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。

    • Cache-Control: no-cache, max-age=2000 表示在2000秒内使用强缓存,超过2000秒使用协商缓存
  • no-store :禁用缓存。
  • public :表明其他用户也可使用缓存,适用于公共缓存服务器的情况。如果没有指定public还是private,则默认为public。
    一文搞懂http缓存
  • private :表明只有特定用户才能使用缓存,适用于公共缓存服务器的情况。
    一文搞懂http缓存
  • s-maxage :适用于多用户使用的公共缓存服务器,比如CDN。比如,当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。也就是说max-age用于普通缓存,而s-maxage用于代理缓存。如果存在s-maxage,则会覆盖掉max-age和Expires header。

二、 Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。但在上面我们提到过,cache-control的优先级更高。 Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

1.2、协商缓存

当浏览器发现缓存过期后,缓存并不一定不能使用了,因为服务器端的资源可能仍然没有改变,所以需要与服务器协商,让服务器判断本地缓存是否还能使用。

当第一次请求响应头中有 ETagLast-Modified 字段,那么第二次请求的请求头中就会携带 If-None-MatchIf-Modified-Since 字段,服务器收到请求后会判断 ETagIf-None-Match 以及 Last-ModifiedIf-Modified-Since 是否一致,如果一致就表示请求资源没有被修改,服务器返回304状态码,使用浏览器缓存资源。如果不一致,则服务器处理请求,返回新资源,状态码为200。

一、 ETagIf-None-Match

二者的值都是服务器为 每份资源分配的唯一标识字符串 ,相当于hash。

  • 浏览器请求资源,服务器会在响应报文头中加入 ETag 字段。 资源更新时,服务器端的 ETag 值也随之更新
  • 浏览器再次请求资源时,会在请求报文头中添加 If-None-Match 字段,它的值就是上次响应报文中的 ETag 的值;
  • 服务器会比对 ETagIf-None-Match 的值是否一致,如果不一致,服务器则接受请求,返回更新后的资源;如果一致,表明资源未更新,则返回状态码为 304 的响应,可继续使用本地缓存,要注意的是,此时响应头会加上 ETag 字段,即使它没有变化。

二、 Last-ModifiedIf-Modified-Since

二者的值都是 GMT格式的时间字符串

  • 浏览器第一次向服务器请求资源后,服务器会在响应头中加上 Last-Modified 字段, 表明该资源最后一次的修改时间
  • 浏览器再次请求该资源时,会在请求报文头中添加 If-Modified-Since 字段,它的值就是上次服务器响应报文中的 Last-Modified 的值;
  • 服务器会比对 Last-ModifiedIf-Modified-Since 的值是否一致,如果不一致,服务器则接受请求,返回更新后的资源;如果一致,表明资源未更新,则返回状态码为 304 的响应,可继续使用本地缓存,与 ETag 不同的是:此时响应头中不会再添加 Last-Modified 字段。

三、 ETag 较之 Last-Modified 的优势

以下内容引用于: http协商缓存VS强缓存

你可能会觉得使用 Last-Modified 已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要 ETag 呢? HTTP1.1ETag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新 GET
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次), If-Modified-Since 能检查到的粒度是s级的,这种修改无法判断(或者说 UNIX 记录 MTIME 只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。

这时,利用 ETag 能够更加准确的控制缓存,因为 ETag 是服务器自动生成的资源在服务器端的唯一标识符,资源每次变动,都会生成新的 ETag 值。 Last-ModifiedETag 是可以一起使用的,但服务器会优先验证 ETag

2、tomcat服务的静态资源缓存机制

先举一个例子,先在linux服务器上安装tomcat,然后上传一个文件到服务器导航,向服务器请求这个静态资源

一文搞懂http缓存

刷新再次请求

一文搞懂http缓存

我们并没有配置响应头 ETagLast-Modified ,为什么会进行协商缓存呢?我们来查看一下 tomcat 源码如何处理 http 缓存的,在 servlet-api.jar 包中有一个 HttpServlet.class 字节码文件,我们用 idea 打开可以看到反编译后的源码。

HttpServlet的功能

HttpServlet 首先必须读取 Http 请求的内容。 Servlet 容器负责创建 HttpServlet 对象,并把 Http 请求直接封装到 HttpServlet 对象中,大大简化了 HttpServlet 解析请求数据的工作量。 HttpServlet 容器响应 Web 客户请求流程如下:

  • Web 客户向 Servlet 容器发出 Http 请求;
  • Servlet 容器解析 Web 客户的 Http 请求;
  • Servlet 容器创建一个 HttpRequest 对象,在这个对象中封装 Http 请求信息;
  • Servlet 容器创建一个 HttpResponse 对象;
  • Servlet 容器调用 HttpServletservice 方法,把 HttpRequestHttpResponse 对象作为 service 方法的参数传给 HttpServlet 对象;
  • HttpServlet 调用 HttpRequest 的有关方法,获取 HTTP 请求信息;
  • HttpServlet 调用 HttpResponse 的有关方法,生成响应数据;
  • Servlet 容器把 HttpServlet 的响应结果传给 Web 客户。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package javax.servlet.http;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.ResourceBundle;
import javax.servlet.DispatcherType;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public abstract class HttpServlet extends GenericServlet {
    private static final long serialVersionUID = 1L;
    private static final String METHOD_DELETE = "DELETE";
    private static final String METHOD_HEAD = "HEAD";
    private static final String METHOD_GET = "GET";
    private static final String METHOD_OPTIONS = "OPTIONS";
    private static final String METHOD_POST = "POST";
    private static final String METHOD_PUT = "PUT";
    private static final String METHOD_TRACE = "TRACE";
    private static final String HEADER_IFMODSINCE = "If-Modified-Since";
    private static final String HEADER_LASTMOD = "Last-Modified";
    private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
    private static final ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings");

    public HttpServlet() {
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_get_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

    }

    protected long getLastModified(HttpServletRequest req) {
        return -1L;
    }

    protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
            this.doGet(req, resp);
        } else {
            NoBodyResponse response = new NoBodyResponse(resp);
            this.doGet(req, response);
            response.setContentLength();
        }

    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_post_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

    }

    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_put_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

    }

    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_delete_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

    }

    private static Method[] getAllDeclaredMethods(Class<?> c) {
        if (c.equals(HttpServlet.class)) {
            return null;
        } else {
            Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());
            Method[] thisMethods = c.getDeclaredMethods();
            if (parentMethods != null && parentMethods.length > 0) {
                Method[] allMethods = new Method[parentMethods.length + thisMethods.length];
                System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
                System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);
                thisMethods = allMethods;
            }

            return thisMethods;
        }
    }

    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Method[] methods = getAllDeclaredMethods(this.getClass());
        boolean ALLOW_GET = false;
        boolean ALLOW_HEAD = false;
        boolean ALLOW_POST = false;
        boolean ALLOW_PUT = false;
        boolean ALLOW_DELETE = false;
        boolean ALLOW_TRACE = true;
        boolean ALLOW_OPTIONS = true;
        Class clazz = null;

        try {
            clazz = Class.forName("org.apache.catalina.connector.RequestFacade");
            Method getAllowTrace = clazz.getMethod("getAllowTrace", (Class[])null);
            ALLOW_TRACE = (Boolean)getAllowTrace.invoke(req, (Object[])null);
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | ClassNotFoundException var14) {
        }

        for(int i = 0; i < methods.length; ++i) {
            Method m = methods[i];
            if (m.getName().equals("doGet")) {
                ALLOW_GET = true;
                ALLOW_HEAD = true;
            }

            if (m.getName().equals("doPost")) {
                ALLOW_POST = true;
            }

            if (m.getName().equals("doPut")) {
                ALLOW_PUT = true;
            }

            if (m.getName().equals("doDelete")) {
                ALLOW_DELETE = true;
            }
        }

        String allow = null;
        if (ALLOW_GET) {
            allow = "GET";
        }

        if (ALLOW_HEAD) {
            if (allow == null) {
                allow = "HEAD";
            } else {
                allow = allow + ", HEAD";
            }
        }

        if (ALLOW_POST) {
            if (allow == null) {
                allow = "POST";
            } else {
                allow = allow + ", POST";
            }
        }

        if (ALLOW_PUT) {
            if (allow == null) {
                allow = "PUT";
            } else {
                allow = allow + ", PUT";
            }
        }

        if (ALLOW_DELETE) {
            if (allow == null) {
                allow = "DELETE";
            } else {
                allow = allow + ", DELETE";
            }
        }

        if (ALLOW_TRACE) {
            if (allow == null) {
                allow = "TRACE";
            } else {
                allow = allow + ", TRACE";
            }
        }

        if (ALLOW_OPTIONS) {
            if (allow == null) {
                allow = "OPTIONS";
            } else {
                allow = allow + ", OPTIONS";
            }
        }

        resp.setHeader("Allow", allow);
    }

    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String CRLF = "/r/n";
        StringBuilder buffer = (new StringBuilder("TRACE ")).append(req.getRequestURI()).append(" ").append(req.getProtocol());
        Enumeration reqHeaderEnum = req.getHeaderNames();

        while(reqHeaderEnum.hasMoreElements()) {
            String headerName = (String)reqHeaderEnum.nextElement();
            buffer.append(CRLF).append(headerName).append(": ").append(req.getHeader(headerName));
        }

        buffer.append(CRLF);
        int responseLength = buffer.length();
        resp.setContentType("message/http");
        resp.setContentLength(responseLength);
        ServletOutputStream out = resp.getOutputStream();
        out.print(buffer.toString());
        out.close();
    }

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals("GET")) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader("If-Modified-Since");
                } catch (IllegalArgumentException var9) {
                    ifModifiedSince = -1L;
                }

                if (ifModifiedSince < lastModified / 1000L * 1000L) {
                    this.maybeSetLastModified(resp, lastModified);
                    this.doGet(req, resp);
                } else {
                    resp.setStatus(304);
                }
            }
        } else if (method.equals("HEAD")) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals("POST")) {
            this.doPost(req, resp);
        } else if (method.equals("PUT")) {
            this.doPut(req, resp);
        } else if (method.equals("DELETE")) {
            this.doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            this.doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

    }

    private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
        if (!resp.containsHeader("Last-Modified")) {
            if (lastModified >= 0L) {
                resp.setDateHeader("Last-Modified", lastModified);
            }

        }
    }

    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest request;
        HttpServletResponse response;
        try {
            request = (HttpServletRequest)req;
            response = (HttpServletResponse)res;
        } catch (ClassCastException var6) {
            throw new ServletException(lStrings.getString("http.non_http"));
        }

        this.service(request, response);
    }
}

可以看到在调用 service 方法时会处理 GET 请求(静态资源都是通过 get 请求),调用 getLastModified 来获取响应内容最后修改时间, service 方法可以根据这个返回值在响应消息中自动生成 Last-Modified 头字段,所以在向 tomcat 服务器请求静态资源时会使用协商缓存。这里解释一下为什么 HttpServlet 类中 getLastModified 方法返回 -1 呢?其实,在 HttpServlet 子类中可以对这个方法进行覆盖,以便返回一个代表当前输出的响应内容的修改时间。参考: https://blog.csdn.net/andydev...

3、客户端处理缓存

其实,在很多业务中都有不需要使用缓存的情况,主要因为缓存会导致资源不是最新的,比如在 html 页面中使用 script 引入第三方插件。在客户端常有以下几种处理方式:

3.1、使用meta标签中http-equiv

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />

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

<meta http-equiv="Expires" content="0" />

3.2、在请求url加上版本号

<link rel="stylesheet" type="text/css" href="./reverse.css?v=2019060301">

<script src="./reverse.js?v=2019060301"></script>

3.3、webpack打包后文件带上hsah

entry: { 
    main: path.join(__dirname, './main.js'), 
    vendor: ['react', 'antd'] 
}, 
output: { 
    path: path.join(__dirname,'./dist'), 
    publicPath: '/dist/', 
    filname: 'bundle.[chunkhash].js' 
}

webpack 给我们提供了三种哈希值计算方式,分别是 hashchunkhashcontenthash 。那么这三者有什么区别呢?

  • hash :跟整个项目的构建相关,构建生成的文件 hash 值都是一样的,只要项目里有文件更改,整个项目构建的 hash 值都会更改。
  • chunkhash :根据不同的入口文件( Entry )进行依赖文件解析、构建对应的chunk,生成对应的 hash 值。
  • contenthash :由文件内容产生的 hash 值,内容不同产生的 contenthash 值也不一样。

显然,我们是不会使用第一种的。改了一个文件,打包之后,其他文件的 hash 都变了,缓存自然都失效了。这不是我们想要的。

chunkhashcontenthash 的主要应用场景是什么呢?在实际在项目中,我们一般会把项目中的 css 都抽离出对应的 css 文件来加以引用。如果我们使用 chunkhash ,当我们改了 css 代码之后,会发现 css 文件 hash 值改变的同时, js 文件的 hash 值也会改变。这时候, contenthash 就派上用场了。

参考:

https://juejin.im/entry/56f0e...

https://segmentfault.com/a/11...

https://blog.csdn.net/qq_2995...

https://www.xp.cn/c.php/28750...

https://blog.csdn.net/andydev...

https://juejin.im/post/5c136b...
原文  https://segmentfault.com/a/1190000022258454
正文到此结束
Loading...