HTTP 必知必會的那些

Http是我們經(jīng)常打交道的網(wǎng)絡(luò)應(yīng)用層協(xié)議署海,它的重要性可能不需要再強(qiáng)調(diào)。但是實(shí)際上很多人医男,包括我自己可能對http了解的并不夠深砸狞。本文就我自己的學(xué)習(xí)心得,分享一下我認(rèn)為需要知道的緩存所涉及到的相關(guān)知識點(diǎn)镀梭。

Http報(bào)文

首先我們來點(diǎn)基礎(chǔ)的刀森,看看http報(bào)文具體的格式。http報(bào)文可以分為請求報(bào)文和響應(yīng)報(bào)文报账,格式大同小異研底。主要分為三個(gè)部分:

1 起始行
2 首部
3 主體
請求報(bào)文格式:

<method> <request-url> <version>
<headers>

<entity-body>

響應(yīng)報(bào)文格式

<version> <status> <reason-phrase>
<headers>

<entity-body>

從請求報(bào)文格式和響應(yīng)報(bào)文格式可以看出,兩者主要在起始行上有差異透罢。這里稍微解釋一下各個(gè)標(biāo)簽:

<method> 指請求方法榜晦,常用的主要是Get、 Post羽圃、Head 還有其他一些我們這里就不說了乾胶,有興趣的可以自己查閱一下

<version> 指協(xié)議版本,現(xiàn)在通常都是Http/1.1了

<request-url> 請求地址

<status> 指響應(yīng)狀態(tài)碼朽寞, 我們熟悉的200识窿、404等等

<reason-phrase> 原因短語,200 OK 脑融、404 Not Found 這種后面的描述就是原因短語喻频,通常不必太關(guān)注。

method

我們知道請求方法最常用的有Get 和Post兩種吨掌,面試時(shí)也常常會問到這兩者有什么區(qū)別半抱,通常什么情況下使用脓恕。這里我們來簡單說一說膜宋。

兩個(gè)方法之間在傳輸形式上有一些區(qū)別窿侈,通過Get方法發(fā)起請求時(shí),會將請求參數(shù)拼接在request-url尾部秋茫,格式是url?param1=xxx&param2=xxx&[…]史简。

我們需要知道,這樣傳輸參數(shù)會使得參數(shù)都暴露在地址欄中肛著。并且由于url是ASCII編碼的圆兵,所以參數(shù)中如果有Unicode編碼的字符,例如漢字枢贿,都會編碼之后傳輸殉农。另外值得注意的是,雖然http協(xié)議并沒有對url長度做限制局荚,但是一些瀏覽器和服務(wù)器可能會有限制超凳,所以通過GET方法發(fā)起的請求參數(shù)不能夠太長。而通過POST方法發(fā)起的請求是將參數(shù)放在請求體中的耀态,所以不會有GET參數(shù)的這些問題轮傍。

另外一點(diǎn)差別就是方法本身的語義上的。GET方法通常是指從服務(wù)器獲取某個(gè)URL資源首装,其行為可以看作是一個(gè)讀操作创夜,對同一個(gè)URL進(jìn)行多次GET并不會對服務(wù)器產(chǎn)生什么影響。而POST方法通常是對某個(gè)URL進(jìn)行添加仙逻、修改驰吓,例如一個(gè)表單提交,通常會往服務(wù)器插入一條記錄系奉。多次POST請求可能導(dǎo)致服務(wù)器的數(shù)據(jù)庫中添加了多條記錄棚瘟。所以從語義上來講,兩者也是不能混為一談的喜最。

狀態(tài)碼

常見的狀態(tài)碼主要有
200 OK 請求成功偎蘸,實(shí)體包含請求的資源
301 Moved Permanent 請求的URL被移除了,通常會在Location首部中包含新的URL用于重定向瞬内。
304 Not Modified 條件請求進(jìn)行再驗(yàn)證迷雪,資源未改變。
404 Not Found 資源不存在
206 Partial Content 成功執(zhí)行一個(gè)部分請求虫蝶。這個(gè)在用于斷點(diǎn)續(xù)傳時(shí)會涉及到章咧。

header

在請求報(bào)文和響應(yīng)報(bào)文中都可以攜帶一些信息,通過與其他部分配合能真,能夠?qū)崿F(xiàn)各種強(qiáng)大的功能赁严。這些信息位于起始行之下與請求實(shí)體之間扰柠,以鍵值對的形式,稱之為首部疼约。每條首部以回車換行符結(jié)尾卤档,最后一個(gè)首部額外多一個(gè)換行,與實(shí)體分隔開程剥。

這里我們重點(diǎn)關(guān)注一下
Date
Cache-Control
Last-Modified
Etag
Expires
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match

Http的首部還有很多劝枣,但限于篇幅我們不一一討論。這些首部都是Http緩存會涉及到的织鲸,在下文中我們會來說說各自的作用舔腾。

實(shí)體

請求發(fā)送的資源,或是響應(yīng)返回的資源搂擦。

Http緩存

當(dāng)我們發(fā)起一個(gè)http請求后稳诚,服務(wù)器返回所請求的資源,這時(shí)我們可以將該資源的副本存儲在本地瀑踢,這樣當(dāng)再次對該url資源發(fā)起請求時(shí)扳还,我們能快速的從本地存儲設(shè)備中獲取到該url資源,這就是所謂的緩存丘损。緩存既可以節(jié)約不必要的網(wǎng)絡(luò)帶寬普办,又能迅速對http請求做出響應(yīng)。

先擺出幾個(gè)概念:
1 新鮮度檢測
2 再驗(yàn)證
3 再驗(yàn)證命中

我們知道徘钥,有些url所對應(yīng)的資源并不是一成不變的衔蹲,服務(wù)器中該url的資源可能在一定時(shí)間之后會被修改。這時(shí)本地緩存中的資源將與服務(wù)器一側(cè)的資源有差異呈础。

既然在一定時(shí)間之后可能資源會改變舆驶,那么在某個(gè)時(shí)間之前我們可以認(rèn)為這個(gè)資源沒有改變,從而放心大膽的使用緩存資源而钞,當(dāng)請求時(shí)間超過來該時(shí)間沙廉,我們認(rèn)為這個(gè)緩存資源可能不再與服務(wù)器端一致了。所以當(dāng)我們發(fā)起一個(gè)請求時(shí)臼节,我們需要先對緩存的資源進(jìn)行判斷撬陵,看看究竟我們是否可以直接使用該緩存資源,這個(gè)就叫做新鮮度檢測网缝。即每個(gè)資源就像一個(gè)食品一樣巨税,擁有一個(gè)過期時(shí)間,我們吃之前需要先看看有沒有過期粉臊。

如果發(fā)現(xiàn)該緩存資源已經(jīng)超過了一定的時(shí)間草添,我們再次發(fā)起請求時(shí)不會直接將緩存資源返回,而是先去服務(wù)器查看該資源是否已經(jīng)改變扼仲,這個(gè)就叫做再驗(yàn)證远寸。如果服務(wù)器發(fā)現(xiàn)對應(yīng)的url資源并沒有發(fā)生變化抄淑,則會返回304 Not Modified,并且不再返回對應(yīng)的實(shí)體驰后。這稱之為再驗(yàn)證命中
肆资。相反如果再驗(yàn)證未命中,則返回200 OK
倡怎,并將改變后的url資源返回迅耘,此時(shí)緩存可以更新以待之后請求贱枣。

我們看看具體的實(shí)現(xiàn)方式:

1 新鮮度檢測
我們需要通過檢測資源是否超過一定的時(shí)間监署,來判斷緩存資源是否新鮮可用。那么這個(gè)一定的時(shí)間怎么決定呢纽哥?其實(shí)是由服務(wù)器通過在響應(yīng)報(bào)文中增加Cache-Control:max-age钠乏,或是Expire這兩個(gè)首部來實(shí)現(xiàn)的。值得注意的是Cache-Control是http1.1的協(xié)議規(guī)范春塌,通常是接相對的時(shí)間晓避,即多少秒以后,需要結(jié)合last-modified這個(gè)首部計(jì)算出絕對時(shí)間只壳。而Expire是http1.0的規(guī)范俏拱,后面接一個(gè)絕對時(shí)間。
2 再驗(yàn)證
如果通過新鮮度檢測發(fā)現(xiàn)需要請求服務(wù)器進(jìn)行再驗(yàn)證吼句,那么我們至少需要告訴服務(wù)器锅必,我們已經(jīng)緩存了一個(gè)什么樣的資源了,然后服務(wù)器來判斷這個(gè)緩存資源到底是不是與當(dāng)前的資源一致惕艳。邏輯是這樣沒錯(cuò)搞隐。那怎么告訴服務(wù)器我當(dāng)前已經(jīng)有一個(gè)備用的緩存資源了呢?我們可以采用一種稱之為條件請求的方式實(shí)現(xiàn)再驗(yàn)證远搪。
3 Http定義了5個(gè)首部用于條件請求:
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match

If-Modified-Since 可以結(jié)合Last-Modified這個(gè)服務(wù)器返回的響應(yīng)首部使用劣纲,當(dāng)我們發(fā)起條件請求時(shí),將Last-Modified首部的值作為If-Modified-Since首部的值傳遞到服務(wù)器谁鳍,意思是查詢服務(wù)器的資源自從我們上一次緩存之后是否有修改

If-None-Match 需要結(jié)合另一個(gè)Etag的服務(wù)器返回的響應(yīng)首部使用癞季。Etag首部實(shí)際上可以認(rèn)為是服務(wù)器對文檔資源定義的一個(gè)版本號。有時(shí)候一個(gè)文檔被修改了倘潜,可能所做的修改極為微小绷柒,并不需要所有的緩存都重新下載數(shù)據(jù)∏嫌或者說某一個(gè)文檔的修改周期極為頻繁伸刃,以至于以秒為時(shí)間粒度的判斷已經(jīng)無法滿足需求。這個(gè)時(shí)候可能就需要Etag這個(gè)首部來表明這個(gè)文檔的版號了吟榴。發(fā)起條件請求時(shí)可將緩存時(shí)保存下來的Etag的值作為If-None-Match首部的值發(fā)送至服務(wù)器,如果服務(wù)器的資源的Etag與當(dāng)前條件請求的Etag一致憔恳,表明這次再驗(yàn)證命中。

其他三個(gè)與斷點(diǎn)續(xù)傳涉及到的相關(guān)知識有關(guān)净蚤,本文暫時(shí)不討論钥组。待我之后寫一篇文章來講講斷點(diǎn)續(xù)傳。

OkHttp的緩存

緩存的Http理論知識大致就是這么些今瀑。我們從OkHttp的源碼來看看程梦,這些知名的開源庫是如何利用Http協(xié)議實(shí)現(xiàn)緩存的。這里我們假設(shè)讀者對OkHttp的請求執(zhí)行流程有了大致的了解橘荠,并且只討論緩存相關(guān)的部分屿附。對于OkHttp代碼不熟悉的同學(xué),建議先看看相關(guān)代碼或是其他文章哥童。

我們知道OkHttp的請求在發(fā)送到服務(wù)器之前會經(jīng)過一系列的Interceptor挺份,其中有一個(gè)CacheInterceptor即是我們需要分析的代碼。

final InternalCache cache;

@Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    ......

 }

方法首先通過InternalCache 獲取到對應(yīng)請求的緩存贮懈。這里我們不展開討論這個(gè)類的具體實(shí)現(xiàn)匀泊,只需要知道,如果之前緩存了該請求url的資源朵你,那么通過request對象可以查找到這個(gè)緩存響應(yīng)各聘。

將獲取到的緩存響應(yīng),當(dāng)前時(shí)間戳和請求傳入CacheStrategy抡医,然后通過執(zhí)行g(shù)et方法執(zhí)行一些邏輯最終可以獲取到strategy.networkRequest,strategy.cacheResponse躲因。如果通過CacheStrategy的判斷之后,我們發(fā)現(xiàn)這次請求無法直接使用緩存數(shù)據(jù)魂拦,需要向服務(wù)器發(fā)起請求毛仪,那么我們就通過CacheStrategy為我們構(gòu)造的networkRequest來發(fā)起這次請求。我們先來看看CacheStrategy做了哪些事情芯勘。


CacheStrategy.Factory.java

public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

CacheStrategy.Factory的構(gòu)造方法首先保存了傳入的參數(shù)箱靴,并將緩存響應(yīng)的相關(guān)首部解析保存下來。之后調(diào)用的get方法如下

    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

get方法很簡單荷愕,主要邏輯在getCandidate中衡怀,這里的邏輯是如果返回的candidate所持有的networkRequest不為空,表示我們這次請求需要發(fā)到服務(wù)器安疗,此時(shí)如果請求的cacheControl要求本次請求只使用緩存數(shù)據(jù)抛杨。那么這次請求恐怕只能以失敗告終了,這點(diǎn)我們等會兒回到CacheInterceptor中可以看到荐类。接著我們看看主要getCandidate的主要邏輯怖现。


 private CacheStrategy getCandidate() {
      // No cached response.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
        ......
    }

上面這段代碼主要列出四種情況下需要忽略緩存,直接想服務(wù)器發(fā)起請求的情況:

1 緩存本身不存在

2 請求是采用https 并且緩存沒有進(jìn)行握手的數(shù)據(jù)。

3 緩存本身不應(yīng)該不保存下來屈嗤∨瞬Γ可能是緩存本身實(shí)現(xiàn)有問題,把一些不應(yīng)該緩存的數(shù)據(jù)保留了下來饶号。

4 如果請求本身添加了 Cache-Control: No-Cache铁追,或是一些條件請求首部,說明請求不希望使用緩存數(shù)據(jù)茫船。

這些情況下直接構(gòu)造一個(gè)包含networkRequest琅束,但是cacheResponse為空的CacheStrategy對象返回。


 private CacheStrategy getCandidate() {
      ......

      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      long ageMillis = cacheResponseAge();
      long freshMillis = computeFreshnessLifetime();

      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }




      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
      }

        ......     
    }

如果緩存響應(yīng)的Cache-Control首部包含immutable,那么說明該資源不會改變算谈∩鳎客戶端可以直接使用緩存結(jié)果。值得注意的是immutable并不屬于http協(xié)議的一部分濒生,而是由facebook提出的擴(kuò)展屬性埋泵。

之后分別計(jì)算ageMills幔欧、freshMills罪治、minFreshMills、maxStaleMills這四個(gè)值礁蔗。
如果響應(yīng)緩存沒有通過Cache-Control:No-Cache 來禁止客戶端使用緩存觉义,并且

ageMillis + minFreshMillis < freshMillis + maxStaleMillis

這個(gè)不等式成立,那么我們進(jìn)入條件代碼塊之后最終會返回networkRequest為空浴井,并且使用當(dāng)前緩存值構(gòu)造的CacheStrtegy晒骇。

這個(gè)不等式究竟是什么含義呢?我們看看這四個(gè)值分別代表什么磺浙。

ageMills 指這個(gè)緩存資源自響應(yīng)報(bào)文在源服務(wù)器中產(chǎn)生或者過期驗(yàn)證的那一刻起洪囤,到現(xiàn)在為止所經(jīng)過的時(shí)間。用食品的保質(zhì)期來比喻的話撕氧,好比當(dāng)前時(shí)間距離生產(chǎn)日期已經(jīng)過去了多久了瘤缩。

freshMills 表示這個(gè)資源在多少時(shí)間內(nèi)是新鮮的。也就是假設(shè)保質(zhì)期18個(gè)月伦泥,那么這個(gè)18個(gè)月就是freshMills剥啤。

minFreshMills 表示我希望這個(gè)緩存至少在多久之后依然是新鮮的。好比我是一個(gè)比較講究的人不脯,如果某個(gè)食品只有一個(gè)月就過期了府怯,雖然并沒有真的過期,但我依然覺得食品不新鮮從而不想再吃了防楷。

maxStaleMills 好比我是一個(gè)不那么講究的人牺丙,即使食品已經(jīng)過期了,只要不是過期很久了复局,比如2個(gè)月冲簿,那我覺得問題不大是整,還可以吃。

minFreshMillsmaxStatleMills都是由請求首部取出的民假,請求可以根據(jù)自己的需要浮入,通過設(shè)置

Cache-Control:min-fresh=xxx、Cache-Control:max-statle=xxx

來控制緩存羊异,以達(dá)到對緩存使用嚴(yán)格性的收緊與放松事秀。

   private CacheStrategy getCandidate() {
        ......

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      String conditionName;
      String conditionValue;
      if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }

      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

如果之前的條件不滿足,說明我們的緩存響應(yīng)已經(jīng)過期了野舶,這時(shí)我們需要通過一個(gè)條件請求對服務(wù)器進(jìn)行再驗(yàn)證操作易迹。接下來的代碼比較清晰來,就是通過從緩存響應(yīng)中取出的Last-Modified,Etag,Date首部構(gòu)造一個(gè)條件請求并返回平道。

接下來我們返回CacheInterceptor

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

可以看到睹欲,如果我們返回的networkRequest和cacheResponse都為空,說明我們即沒有可用的緩存一屋,同時(shí)請求通過Cache-Control:only-if-cached只允許我們使用當(dāng)前的緩存數(shù)據(jù)窘疮。這個(gè)時(shí)候我們只能返回一個(gè)504的響應(yīng)。接著往下看冀墨,

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    } 

如果networkRequest為空闸衫,說明我們不需要進(jìn)行再驗(yàn)證了,直接將cacheResponse作為請求結(jié)果返回诽嘉。

Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }


     Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;

如果networkRequest存在不為空蔚出,說明這次請求是需要發(fā)到服務(wù)器的。此時(shí)有兩種情況虫腋,一種cacheResponse不存在骄酗,說明我們沒有一個(gè)可用的緩存,這次請求只是一個(gè)普通的請求悦冀。如果cacheResponse存在趋翻,說明我們有一個(gè)可能過期了的緩存,此時(shí)networkRequest是一個(gè)用來進(jìn)行再驗(yàn)證的條件請求雏门。

不管哪種情況嘿歌,我們都需要通過networkResponse=chain.proceed(networkRequest)獲取到服務(wù)器的一個(gè)響應(yīng)。不同的只是如果有緩存數(shù)據(jù)茁影,那么在獲取到再驗(yàn)證的響應(yīng)之后宙帝,需要cache.update(cacheResponse, response)去更新當(dāng)前緩存中的數(shù)據(jù)。如果沒有緩存數(shù)據(jù)募闲,那么判斷此次請求是否可以被緩存步脓。在滿足緩存的條件下,將響應(yīng)緩存下來,并返回靴患。

OkHttp緩存大致的流程就是這樣仍侥,我們從中看出,整個(gè)流程是遵循了Http的緩存流程的鸳君。最后我們總結(jié)一下緩存的流程:

1 從接收到的請求中农渊,解析出Url和各個(gè)首部。

2 查詢本地是否有緩存副本可以使用或颊。

3 如果有緩存砸紊,則進(jìn)行新鮮度檢測,如果緩存足夠新鮮囱挑,則使用緩存作為響應(yīng)返回醉顽,如果不夠新鮮了,則構(gòu)造條件請求平挑,發(fā)往服務(wù)器再驗(yàn)證游添。如果沒有緩存,就直接將請求發(fā)往服務(wù)器通熄。

4 把從服務(wù)器返回的響應(yīng)唆涝,更新或是新增到緩存中。

OAuth

OAuth是一個(gè)用于授權(quán)第三方獲取相應(yīng)資源的協(xié)議棠隐。與以往的授權(quán)方式不同的是石抡,OAuth的授權(quán)能避免用戶暴露自己的用戶密碼給第三方,從而更加的安全助泽。OAuth協(xié)議通過設(shè)置一個(gè)授權(quán)層,以區(qū)分用戶和第三方應(yīng)用嚎京。用戶本身可以通過用戶密碼登陸服務(wù)提供商嗡贺,獲取到賬戶所有的資源。而第三方應(yīng)用只能通過向用戶請求授權(quán)鞍帝,獲取到一個(gè)Access Token诫睬,用以登陸授權(quán)層,從而在指定時(shí)間內(nèi)獲取到用戶授權(quán)訪問的部分資源帕涌。

OAuth定義的幾個(gè)角色:

Role Description
Resource Owner 可以授權(quán)訪問某些受保護(hù)資源的實(shí)體摄凡,通常就是指用戶
Client 可以通過用戶的授權(quán)訪問受保護(hù)資源的應(yīng)用,也就是第三方應(yīng)用
Authorization server 在認(rèn)證用戶之后給第三方下發(fā)Access Token的服務(wù)器
Resource Server 擁有受保護(hù)資源的服務(wù)器,可以通過Access Token響應(yīng)資源請求

   +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

從上圖可以看出蚓曼,一個(gè)OAuth授權(quán)的流程主要可以分為6步:

1 客戶端向用戶申請授權(quán)亲澡。

2 用戶同意授權(quán)。

3 客戶端通過獲取的授權(quán)纫版,向認(rèn)證服務(wù)器申請Access Token床绪。

4 認(rèn)證服務(wù)器通過授權(quán)認(rèn)證后,下發(fā)Access Token。

5 客戶端通過獲取的到Access Token向資源服務(wù)器發(fā)起請求癞己。

6 資源服務(wù)器核對Access Token后下發(fā)請求資源膀斋。

Https

簡單的說 Http + 加密 + 認(rèn)證 + 完整性保護(hù) = Https

傳統(tǒng)的Http協(xié)議是一種應(yīng)用層的傳輸協(xié)議,Http直接與TCP協(xié)議通信痹雅。其本身存在一些缺點(diǎn):

1 Http協(xié)議使用明文傳輸仰担,容易遭到竊聽。

2 Http對于通信雙方都沒有進(jìn)行身份驗(yàn)證绩社,通信的雙方無法確認(rèn)對方是否是偽裝的客戶端或者服務(wù)端惰匙。

3 Http對于傳輸內(nèi)容的完整性沒有確認(rèn)的辦法,往往容易在傳輸過程中被劫持篡改铃将。

因此项鬼,在一些需要保證安全性的場景下,比如涉及到銀行賬戶的請求時(shí)劲阎,Http無法抵御這些攻擊绘盟。
Https則可以通過增加的SSL\TLS,支持對于通信內(nèi)容的加密悯仙,以及對通信雙方的身份進(jìn)行驗(yàn)證龄毡。

Https的加密

近代密碼學(xué)中加密的方式主要有兩類:

對稱秘鑰加密

非對稱秘鑰加密

對稱秘鑰加密是指加密與解密過程使用同一把秘鑰。這種方式的優(yōu)點(diǎn)是處理速度快锡垄,但是如何安全的從一方將秘鑰傳遞到通信的另一方是一個(gè)問題沦零。

非對稱秘鑰加密是指加密與解密使用兩把不同的秘鑰。這兩把秘鑰货岭,一把叫公開秘鑰路操,可以隨意對外公開。一把叫私有秘鑰千贯,只用于本身持有屯仗。得到公開秘鑰的客戶端可以使用公開秘鑰對傳輸內(nèi)容進(jìn)行加密,而只有私有秘鑰持有者本身可以對公開秘鑰加密的內(nèi)容進(jìn)行解密搔谴。這種方式克服了秘鑰交換的問題魁袜,但是相對于對稱秘鑰加密的方式,處理速度較慢敦第。

SSL\TLS的加密方式則是結(jié)合了兩種加密方式的優(yōu)點(diǎn)峰弹。首先采用非對稱秘鑰加密,將一個(gè)對稱秘鑰使用公開秘鑰加密后傳輸?shù)綄Ψ轿吖Ψ绞褂盟接忻罔€解密鞠呈,得到傳輸?shù)膶ΨQ秘鑰。之后雙方再使用對稱秘鑰進(jìn)行通信师幕。這樣即解決了對稱秘鑰加密的秘鑰傳輸問題粟按,又利用了對稱秘鑰的高效率來進(jìn)行通信內(nèi)容的加密與解密诬滩。

Https的認(rèn)證

SSL\TLS采用的混合加密的方式還是存在一個(gè)問題,即怎么樣確保用于加密的公開秘鑰確實(shí)是所期望的服務(wù)器所分發(fā)的呢灭将?也許在收到公開秘鑰時(shí)疼鸟,這個(gè)公開秘鑰已經(jīng)被別人篡改了。因此庙曙,我們還需要對這個(gè)秘鑰進(jìn)行認(rèn)證的能力空镜,以確保我們通信的對方是我們所期望的對象。

目前的做法是使用由數(shù)字證書認(rèn)證機(jī)構(gòu)頒發(fā)的公開秘鑰證書捌朴。服務(wù)器的運(yùn)營人員可以向認(rèn)證機(jī)構(gòu)提出公開秘鑰申請吴攒。認(rèn)證機(jī)構(gòu)在審核之后,會將公開秘鑰與共鑰證書綁定砂蔽。服務(wù)器就可以將這個(gè)共鑰證書下發(fā)給客戶端洼怔,客戶端在收到證書后,使用認(rèn)證機(jī)構(gòu)的公開秘鑰進(jìn)行驗(yàn)證左驾。一旦驗(yàn)證成功镣隶,即可知道這個(gè)秘鑰是可以信任的秘鑰。

總結(jié)
Https的通信流程:

1 Client發(fā)起請求

2 Server端響應(yīng)請求诡右,并在之后將證書發(fā)送至Client

3 Client使用認(rèn)證機(jī)構(gòu)的共鑰認(rèn)證證書安岂,并從證書中取出Server端共鑰。

4 Client使用共鑰加密一個(gè)隨機(jī)秘鑰帆吻,并傳到Server

5 Server使用私鑰解密出隨機(jī)秘鑰

6 通信雙方使用隨機(jī)秘鑰最為對稱秘鑰進(jìn)行加密解密域那。

                                                 — — — END — — —

原文地址:
https://mp.weixin.qq.com/s?__biz=MzIwMTAzMTMxMg==&mid=2649492918&idx=1&sn=40030cd46cbba2bc639602aa341f3739&chksm=8eec8649b99b0f5f09aedc590901f61e1729ff03e6381a3127890381841dfcdd14af282a13fc&mpshare=1&scene=23&srcid=0712FvOBFaCt3hq4igtnMTu4#rd

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市猜煮,隨后出現(xiàn)的幾起案子次员,更是在濱河造成了極大的恐慌,老刑警劉巖友瘤,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翠肘,死亡現(xiàn)場離奇詭異,居然都是意外死亡辫秧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門被丧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盟戏,“玉大人,你說我怎么就攤上這事甥桂∈辆浚” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵黄选,是天一觀的道長蝇摸。 經(jīng)常有香客問我婶肩,道長,這世上最難降的妖魔是什么貌夕? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任律歼,我火速辦了婚禮,結(jié)果婚禮上啡专,老公的妹妹穿的比我還像新娘险毁。我一直安慰自己,他們只是感情好们童,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布畔况。 她就那樣靜靜地躺著,像睡著了一般慧库。 火紅的嫁衣襯著肌膚如雪跷跪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天齐板,我揣著相機(jī)與錄音吵瞻,去河邊找鬼。 笑死覆积,一個(gè)胖子當(dāng)著我的面吹牛听皿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宽档,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼尉姨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吗冤?” 一聲冷哼從身側(cè)響起又厉,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎椎瘟,沒想到半個(gè)月后覆致,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肺蔚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年煌妈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宣羊。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡璧诵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仇冯,到底是詐尸還是另有隱情之宿,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布苛坚,位于F島的核電站比被,受9級特大地震影響色难,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜等缀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一枷莉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧项滑,春花似錦依沮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至州疾,卻和暖如春辜限,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背严蓖。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工薄嫡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颗胡。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓毫深,卻偏偏與公主長得像,于是被迫代替她去往敵國和親毒姨。 傳聞我的和親對象是個(gè)殘疾皇子哑蔫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容