HTTP协议(一) HTTP协议缓存请求头和响应头详解


前言

HTTP协议的缓存的目的是减少相应延迟和减少网络带宽消耗, 比如 css、 js、图片这类静态资源应该进行缓存。实际项目 一般使用反向代理服务器(如 nginx、 apache 等) 进行缓存。下面详细解释HTTP协议对缓存的处理支持


一、HTTP协议怎么实现缓存?

HTTP协议使用请求头和响应头协同作用实现了缓存,包括两种缓存:200 from memory cache和304 Not Modified。涉及到的请求头有:cache-control, expires, last-modified,etag,响应头有:if-none-match, if-modified-since 。

1、cache-control

Cache-Control用于控制文件在本地缓存的有效时长。服务器响应头:Cache-Control:max-age=100表示文件在本地应该缓存,且有效时长是100秒(从发出请求算起)。在接下来100秒内,如果有请求这个资源,浏览器不会发出HTTP请求,而是直接使用本地缓存的文件。 cache-control是http协议中常用的头部之一,顾名思义, 他是负责控制页面的缓存机制,如果该头部指示缓存, 缓存的内容也会存在本地, 操作流程和expire相似,但也有不同的地方, cache-control有更多的选项, 而且也有更多的处理方式.

cache-control中可以包含的值如下:
1.Public
指示响应可被任何缓存区缓存。

2.Private
指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。

3.no-cache
指示请求或响应消息不能缓存

4.no-store
用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存

5.max-age
指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。

6.no-transform
不允许转换存储系统

7.must-revalidate
告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。

如下,这个响应中,cache-control的意思是在3600秒内,再次访问这个请求,直接由浏览器取本地缓存,一旦本地过期,必须去服务器校验。
在这里插入图片描述

2、expire

Expires的功能与cache-control类似。Expires的值是一个绝对的时间点,如:Expires: Sat, 21 Aug 2021 05:25:06 GMT,表示在这个时间点之前,缓存都是有效的。
Expires是HTTP1.0标准中的字段,Cache-Control是HTTP1.1标准中新加的字段,功能一样,都是控制缓存的有效时间,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致。当这两个字段同时出现时,Cache-Control 是高优化级的。

如下,这个响应中,在2021-8-21 14:13:03前可以使用本地缓存。
(这里cache-control和expires同时在,优先使用cache-control的配置,所以这个配置其实是无效的)
在这里插入图片描述

3、last-modified

在响应头中表示这个响应资源的最后修改时间,web服务器在响应请求时,通过这个字段告诉浏览器资源的最后修改时间。

如下,这个响应中,最后修改时间为2021-8-21 14:03:15
在这里插入图片描述

4、if-modified-since

  • 请求头if-modified-since要配合响应头last-modified、cache-control使用。
  • 当缓存的资源过期时(通过Cache-Control标识的max-age判断),浏览器发现资源具有Last-Modified声明,则再次向web服务器请求时带上头If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。

如下,发送这个请求前,Cache-Control的时间已过期。这个请求中,If-Modified-Since为2021-8-21 12:48:25,表示向服务器询问当前请求文件的最后修改时间是否在这个时间之前,如果在这之前直接返回304,如果在这之后请求文件,并返回200。
在这里插入图片描述

5、etag

  • 响应头Etag也和Last-Modified一样,对文件进行标识的字段。不同的是,Etag的取值是一个对文件进行标识的特征字串。在向服务器查询文件是否有更新时,浏览器通过If-None-Match字段把特征字串发送给服务器,由服务器和文件的最新特征字串进行匹配,来判断文件是否有更新。没有更新返回304,有更新回包200。Etag和Last-Modified可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。
  • Etag的生成方式挺有意思,分两段,中间用字符“-”隔开,第一段是文件lastModified的时间毫秒数的16
    进制字符串,第二段为文件大小的16进制字符串。

如下,响应头中包含Etag,返回了304状态。
在这里插入图片描述

6、if-none-match

请求头if-none-match与etag配对,类似if-modified-since 与last-modified配置一样。当浏览器本地缓存失效后,将上次响应的etag的值放在请求头if-none-match中发送给服务器,服务器使用这个串判断文件是否更改,如果没有更改,返回403,如果更改,返回200.

注意的是,如果请求中,if-modified-since 与if-none-match同时粗拿在,服务器会优先验证if-modified-since请求头,再验证if-none-match,但是必须要两者头通过验证的时候才返回304,其中一个验证失败,都将返回新资源和200状态。

如下图,请求头中有if-none-match字段。返回了304状态。
在这里插入图片描述

7、Http协议缓存流程图

在这里插入图片描述

二、java实现缓存协议的代码片段

package com.iscas.sp.filter.support;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.LRUCache;
import cn.hutool.core.io.IoUtil;
import com.iscas.sp.filter.AbstractFilter;
import com.iscas.sp.filter.Filter;
import com.iscas.sp.filter.SpChain;
import com.iscas.sp.interceptor.model.SpResponse;
import com.iscas.sp.proxy.base.Constant;
import com.iscas.sp.proxy.model.ServerInfo;
import com.iscas.sp.proxy.model.SpContext;
import com.iscas.sp.proxy.util.ETagUtils;
import com.iscas.sp.proxy.util.HttpUtils;
import com.iscas.sp.proxy.util.StaticResourceUtils;
import com.iscas.templet.exception.NotFoundException;
import io.netty.handler.codec.http.*;
import org.apache.commons.lang3.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * http-cache处理过滤器
 * 目前仅支持静态资源服务器类型
 *
 * @author zhuquanwen
 * @vesion 1.0
 * @date 2021/7/12 14:50
 * @since jdk1.8
 */
@Filter(name = "httpCacheFilter", order = 74)
public class HttpCacheFilter extends AbstractFilter {
    /**
     * 静态资源服务的缓存
     */
//    LRUCache<Object, Object> CACHE = CacheUtil.newLRUCache(Constant.PROXY_CONF.getHttpCacheCapacity());
    @Override
    public void preFilter(SpChain chain, SpContext context) throws Throwable {
        FullHttpRequest httpRequest = context.getRequest();
        HttpResponse httpResponse = context.getResponse();
        HttpMethod method = httpRequest.method();
        //如果是get请求才处理
        if (method == HttpMethod.GET) {
            ServerInfo serverInfo = context.getServerInfo();
            //只有静态资源服务器才处理
            if (serverInfo != null && serverInfo.getTargetUrl().startsWith("file:")) {
                String filePath = StaticResourceUtils.getFilePath();
                //非classpath开头的才处理
                if (filePath != null && !filePath.startsWith("classpath")) {
                    //判断请求头
                    String ifModifiedSince = httpRequest.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
                    String ifNoneMatch = httpRequest.headers().get(HttpHeaderNames.IF_NONE_MATCH);

                    //请求头中至少携带一种时才处理
                    if (StringUtils.isNotEmpty(ifModifiedSince) || StringUtils.isNotEmpty(ifNoneMatch)) {
                        File file = new File(filePath);
                        if (!file.exists()) {
                            throw new NotFoundException();
                        }
                        if (file.isDirectory()) {
                            //如果是文件夹,自动寻找文件下的index.html
                            file = new File(file, "index.html");
                        }
                        //文件存在才处理
                        if (file.exists()) {
                            //如果处理了modified,从缓存获取数据并返回了数据,直接return,不走后面的流程了
                            if (handleFileModified(file, ifModifiedSince, ifNoneMatch)) {
                                return;
                            }
                        }
                    }
                }
            }
        }
        chain.doFilter(context);
    }


    @Override
    public void postFilter(SpChain chain, SpContext context) throws Throwable {
        if (context.getServerInfo() != null || Objects.equals(context.getProxyType(), "file")) {
            //处理cache-control
            handleCacheControl(context);
            //设置last-modified,并缓存
            handleLastModified(context);
        }
        chain.doFilter(context);
    }

    private boolean handleFileModified(File file, String ifModifiedSince, String ifNoneMatch) {
        //查看缓存中有没有值,如果没有值,直接不做处理了

        long fileLength = file.length();
        long lastModified = file.lastModified();
        if (ifNoneMatch != null) {
            //解析etag
            String etag = ETagUtils.createEtag(lastModified, fileLength);
            if (Objects.equals(etag, ifNoneMatch)) {
                //etag成功后,再比较lastModified
                if (getTimeMs(ifModifiedSince) + 999 >= lastModified) {
                    //未做修改
                    SpResponse spResponse = new SpResponse();
                    spResponse.setProtocol(HttpUtils.getSpContext().getRequest().protocolVersion().toString());
                    spResponse.setStatus(304);
                    HttpUtils.sendSpResponse(spResponse);
                    return true;
                }
            }
        }
        if (ifModifiedSince != null) {
            if (getTimeMs(ifModifiedSince) + 999 >= lastModified) {
                //未做修改
                SpResponse spResponse = new SpResponse();
                spResponse.setProtocol(HttpUtils.getSpContext().getRequest().protocolVersion().toString());
                spResponse.setStatus(304);
                HttpUtils.sendSpResponse(spResponse);
                return true;
            }
        }

        return false;
    }


    private long getTimeMs(String ifModifiedSince) {
        ZonedDateTime zdt = ZonedDateTime.parse(ifModifiedSince, DateTimeFormatter.RFC_1123_DATE_TIME);
        return zdt.toInstant().toEpochMilli();
    }

    private void handleCacheControl(SpContext context) {
        //静态资源服务,添加CACHE-CONTROL、expires
        HttpResponse response = context.getResponse();
        String cacheControlStr = HttpHeaderValues.MAX_AGE + "=" + Constant.PROXY_CONF.getHttpCacheMaxAge() + "," +
                Constant.PROXY_CONF.getHttpCacheControlParams();
        response.headers().set(HttpHeaderNames.CACHE_CONTROL, cacheControlStr);
        response.headers().set(HttpHeaderNames.EXPIRES, new Date(System.currentTimeMillis() + Constant.PROXY_CONF.getHttpCacheMaxAge() * 1000L));
    }

    private void handleLastModified(SpContext context) throws IOException {
        FullHttpRequest httpRequest = context.getRequest();
        HttpResponse httpResponse = context.getResponse();
        HttpMethod method = httpRequest.method();
        if (method != HttpMethod.GET) {
            //如果非get请求,不处理
            return;
        }
        //缓存数据
        Long fileLength = context.getFileLength();
        Long lastModified = context.getLastModified();
        httpResponse.headers().set(HttpHeaderNames.LAST_MODIFIED, new Date(lastModified));
        httpResponse.headers().set(HttpHeaderNames.ETAG, ETagUtils.createEtag(lastModified, fileLength));

    }


}

总结

本次总结HTTP协议缓存的处理流程是为了自己实现一个静态资源服务器,要实现跟Nginx一样的带http协议缓存的功能,通过对6个协议头:Cache-Control、Expires、Etag、Last-Modified、If-Modified-Since、If-None-Match的学习。很简单就实现了缓存的功能。通过使用HTTP的缓存能大大增加服务的负载能力。

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值