HTTP协议的缓存的目的是减少相应延迟和减少网络带宽消耗, 比如 css、 js、图片这类静态资源应该进行缓存。实际项目 一般使用反向代理服务器(如 nginx、 apache 等) 进行缓存。下面详细解释HTTP协议对缓存的处理支持
cache-control中可以包含的值如下:
1.Public
指示响应可被任何缓存区缓存。
2.Private
指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
3.no-cache
指示请求或响应消息不能缓存
4.no-store
用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存
5.max-age
指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
6.no-transform
不允许转换存储系统
7.must-revalidate
告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。
如下,这个响应中,cache-control的意思是在3600秒内,再次访问这个请求,直接由浏览器取本地缓存,一旦本地过期,必须去服务器校验。
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的配置,所以这个配置其实是无效的)
在响应头中表示这个响应资源的最后修改时间,web服务器在响应请求时,通过这个字段告诉浏览器资源的最后修改时间。
如下,这个响应中,最后修改时间为2021-8-21 14:03:15
如下,发送这个请求前,Cache-Control的时间已过期。这个请求中,If-Modified-Since为2021-8-21 12:48:25,表示向服务器询问当前请求文件的最后修改时间是否在这个时间之前,如果在这之前直接返回304,如果在这之后请求文件,并返回200。
请求头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状态。
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
@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的缓存能大大增加服务的负载能力。