天天看點

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

文章目錄

  • 前言
  • 一、HTTP協定怎麼實作緩存?
    • 1、cache-control
    • 2、expire
    • 3、last-modified
    • 4、if-modified-since
    • 5、etag
    • 6、if-none-match
    • 7、Http協定緩存流程圖
  • 二、java實作緩存協定的代碼片段
  • 總結

前言

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秒内,再次通路這個請求,直接由浏覽器取本地緩存,一旦本地過期,必須去伺服器校驗。

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

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的配置,是以這個配置其實是無效的)

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

3、last-modified

在響應頭中表示這個響應資源的最後修改時間,web伺服器在響應請求時,通過這個字段告訴浏覽器資源的最後修改時間。

如下,這個響應中,最後修改時間為2021-8-21 14:03:15

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

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。

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

5、etag

  • 響應頭Etag也和Last-Modified一樣,對檔案進行辨別的字段。不同的是,Etag的取值是一個對檔案進行辨別的特征字串。在向伺服器查詢檔案是否有更新時,浏覽器通過If-None-Match字段把特征字串發送給伺服器,由伺服器和檔案的最新特征字串進行比對,來判斷檔案是否有更新。沒有更新傳回304,有更新回包200。Etag和Last-Modified可根據需求使用一個或兩個同時使用。兩個同時使用時,隻要滿足基中一個條件,就認為檔案沒有更新。
  • Etag的生成方式挺有意思,分兩段,中間用字元“-”隔開,第一段是檔案lastModified的時間毫秒數的16

    進制字元串,第二段為檔案大小的16進制字元串。

如下,響應頭中包含Etag,傳回了304狀态。

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

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狀态。

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

7、Http協定緩存流程圖

HTTP協定(一) HTTP協定緩存請求頭和響應頭詳解前言一、HTTP協定怎麼實作緩存?二、java實作緩存協定的代碼片段總結

二、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的緩存能大大增加服務的負載能力。