Android網絡接口客戶端緩存

本次博文並不貼具體實現代碼,只講方案和流程,因爲涉及的SQL、SP查詢和文件緩存都是一些基本操作,只是額外結合了一點Http協議的東西,具體還請結合自身項目框架實現。

爲了提高App的網絡請求響應速度和減輕服務器的請求壓力,比如某些接口的數據更新的並不頻繁,沒必要每次都去服務器請求數據下來,接口緩存是一個非常棒的解決方案,那麼App內的接口緩存機制如何實現呢?首先,這個緩存機制要滿足:

1、接口的正常請求和資源的正常獲取

2、如再次請求時,資源未發生更新,則不必再次返回資源

3、如再次請求時,資源已有更新,則按常規請求返回

可以看出這個緩存機制的核心是請求資源有無發生更新,那麼我們如何知道存在服務器的資源是否有無更新呢?這就得搬出Http協議的Cache-Control了,瀏覽器對於網頁的緩存就是通過它來實現的,我們App要實現同樣的功能也少不了它的參與。

Cache-Control驗證

衆所周知,網頁的緩存是由HTTP消息頭中的“Cache-Control”來控制的,常見的取值有private、no-cache、max-age、must-revalidate等,默認爲private。其作用根據不同的重新瀏覽方式分爲以下幾種情況。

Cache-directive 說明
public 所有內容都將被緩存(客戶端和代理服務器都可緩存)
private 內容只緩存到私有緩存中(僅客戶端可以緩存,代理服務器不可緩存)
no-cache 必須先與服務器確認返回的響應是否被更改,然後才能使用該響應來滿足後續對同一個網址的請求。因此,如果存在合適的驗證令牌 (ETag),no-cache 會發起往返通信來驗證緩存的響應,如果資源未被更改,可以避免下載。
no-store 所有內容都不會被緩存到緩存或 Internet 臨時文件中
must-revalidation/proxy-revalidation 如果緩存的內容失效,請求必鬚髮送到服務器/代理以進行重新驗證
max-age=xxx (xxx is numeric) 緩存的內容將在 xxx 秒後失效, 這個選項只在HTTP 1.1可用, 並如果和Last-Modified一起使用時, 優先級較高

以上六種屬性全部是針對瀏覽器緩存而設置的,而今天要實現的App接口緩存只需要用到no-cache做處理。no-cache並不是不使用緩存的意思,只是每次使用前需要跟服務器端進行驗證,詢問請求的資源是否更改過。

那麼當Cache-Control爲no-cache時,我們如何與服務器驗證呢?

Last-Modified

顧名思義,Last-Modified表示資源最後的更新時間。服務端在返回資源時,會將該資源的最後更改時間通過Last-Modified字段返回給客戶端。客戶端下次請求時通過If-Modified-Since(If-Unmodified-Since不作討論)帶上Last-Modified,服務端檢查該時間是否與服務器的最後修改時間一致:如果一致,則返回304狀態碼,不返回資源;如果不一致則返回200和修改後的資源,並帶上新的時間。

比如我請求本地一個文件,把整個請求信息打印出看看。

thread {
        val url = URL("http://127.0.0.1:8080/test/aaa.json")
        val connection = url.openConnection() as HttpURLConnection
        connection.requestMethod = "GET"
        connection.setRequestProperty("Cache-Control", "no-cache")
        connection.connect()
        println("響應碼 = ${connection.responseCode}")
        println("響應頭:")
        connection.headerFields.forEach { t, u ->
            println("$t : $u")
        }

        println("請求內容:${is2String(connection.inputStream)}")

    }.run()
響應碼 = 200
響應頭:
Accept-Ranges : [bytes]
null : [HTTP/1.1 200]
ETag : [W/"173-1566976351219"]
Accept-Ranges : [bytes]
Last-Modified : [Wed, 28 Aug 2019 07:12:31 GMT]
Content-Length : [173]
Date : [Wed, 28 Aug 2019 07:03:26 GMT]
Content-Type : [application/json]
請求內容:{"code":200,"data":{"installPackage":"","lastVersion":"","message":"已經是最新版本","needUpdate":false,"updateContent":"","updateType":null},"flag":true,"message":""}

請求成功,返回200並且附帶了請求的資源和Last-Modified字段。

接着我們再次發起請求並在請求頭帶上Last-Modified值。

connection.setRequestProperty("If-Modified-Since", "Wed, 28 Aug 2019 07:12:31 GMT")
響應碼 = 304
響應頭:
null : [HTTP/1.1 304]
ETag : [W/"173-1566976351219"]
Last-Modified : [Wed, 28 Aug 2019 07:12:31 GMT]
Date : [Wed, 28 Aug 2019 07:14:36 GMT]
請求內容:

這次返回了304並且沒有附帶資源,這種情況下我們就可以針對不同場景使用本地緩存或者不作處理了。

ETag

ETag又稱實體標籤(Entity Tag)。只是以修改時間來判斷還是有缺陷,比如文件的最後修改時間變了,但內容沒變。對於這樣的情況,ETag提供了更加有效的驗證方式。
服務器通過某個算法對資源進行計算,取得一串值(類似於文件的md5值),之後將該值通過ETag返回給客戶端,客戶端下次請求時通過If-None-Match(If-Match不作討論)帶上該值,服務器對該值進行對比校驗:如果一致則返回304,否則返回200和最新的資源。

接着上面的那個請求,第一次響應返回了200、ETag和資源。

響應碼 = 200
響應頭:
Accept-Ranges : [bytes]
null : [HTTP/1.1 200]
ETag : [W/"173-1566976351219"]
Accept-Ranges : [bytes]
Last-Modified : [Wed, 28 Aug 2019 07:12:31 GMT]
Content-Length : [173]
Date : [Wed, 28 Aug 2019 07:03:26 GMT]
Content-Type : [application/json]
請求內容:{"code":200,"data":{"installPackage":"","lastVersion":"","message":"已經是最新版本","needUpdate":false,"updateContent":"","updateType":null},"flag":true,"message":""}

在請求頭帶上ETag再來一次試試。

connection.setRequestProperty("If-None-Match", "W/\"173-1566976351219\"")
響應碼 = 304
響應頭:
null : [HTTP/1.1 304]
ETag : [W/"173-1566976351219"]
Last-Modified : [Wed, 28 Aug 2019 07:12:31 GMT]
Date : [Wed, 28 Aug 2019 07:17:13 GMT]
請求內容:

Etag和Last-Modified都可以用於對資源進行驗證,我們把這兩者都成爲驗證器(Validators),不同的是,Etag屬於強驗證(Strong Validation),因爲它期望的是資源字節級別的一致;而Last-Modified屬於弱驗證(Weak Validation),只要資源的主要內容一致即可,允許例如頁底的廣告,頁腳不同。
根據RFC 2616標準中的13.3.4小節,一個使用HTTP 1.1標準的服務端應該同時發送Etag和Last-Modified字段。同時一個支持HTTP 1.1的客戶端,比如瀏覽器,如果服務端有提供Etag的話,必須首先對Etag進行Conditional Request(If-None-Match頭信息);如果兩者都有提供,那麼應該同時對兩者進行Conditional Request(If-Modified-Since頭信息)。如果服務端對兩者的驗證結果不一致,例如通過一個條件判斷資源發生了更改,而另一個判定資源沒有發生更改,則不允許返回304狀態。但話說回來,是否返回還是通過服務端編寫的實際代碼決定的,所以具體使用哪種驗證方式可根據自身方便使用選擇。

驗證Last-Modified和ETag

修改aaa.json文件,添加刪除一個字符,相對於文件修改過但內容沒有改變,這時在通過發送之前的Etag和Last-Modified來驗證'驗證'是否可用。

Last-Modified

connection.setRequestProperty("If-Modified-Since", "Wed, 28 Aug 2019 07:12:31 GMT")
響應碼 = 200
響應頭:
Accept-Ranges : [bytes]
null : [HTTP/1.1 200]
ETag : [W/"173-1566977335527"]
Last-Modified : [Wed, 28 Aug 2019 07:28:55 GMT]
Content-Length : [173]
Date : [Wed, 28 Aug 2019 08:15:05 GMT]
Content-Type : [application/json]
請求內容:{"code":200,"data":{"installPackage":"","lastVersion":"","message":"已經是最新版本","needUpdate":false,"updateContent":"","updateType":null},"flag":true,"message":""}

Last-Modified可用✔️

ETag

connection.setRequestProperty("If-None-Match", "W/\"173-1566976351219\"")
響應碼 = 200
響應頭:
Accept-Ranges : [bytes]
null : [HTTP/1.1 200]
ETag : [W/"173-1566977335527"]
Last-Modified : [Wed, 28 Aug 2019 07:28:55 GMT]
Content-Length : [173]
Date : [Wed, 28 Aug 2019 08:19:12 GMT]
Content-Type : [application/json]
請求內容:{"code":200,"data":{"installPackage":"","lastVersion":"","message":"已經是最新版本","needUpdate":false,"updateContent":"","updateType":null},"flag":true,"message":""}

我擦嘞,不對啊,說好的內容不變返回304呢?

這個問題就是網上有人搜索的ETag不起作用,那爲啥呢?維基百科是這樣說的:

ETag機制同時支持強校驗和弱校驗。它們通過ETag標識符的開頭是否存在“W/”來區分,如:

"123456789"   -- 一個強ETag驗證符
W/"123456789"  -- 一個弱ETag驗證符

強校驗的ETag匹配要求兩個資源內容的每個字節需完全相同,包括所有其他實體字段(如Content-Language)不發生變化。

弱校驗的ETag匹配要求兩個資源在語義上相等,不需要每個字節相同。

並且ETag的生成規則沒有嚴格規定包括,可以是文檔內容的hash值,對最後修改時間的hash值,甚至是約定的版本號。

上面的ETag沒有按預期完成就是因爲它是對最後修改時間作的hash值,只要把它修改爲對內容作hash值就行了。

App接口緩存流程

這個流程看起來蠻簡單的,但是其中隱藏的問題可不少。比如,如果並不是所有的接口需要緩存,那麼如何區分需要緩存的接口呢?區分的地方又在哪裏?我又如何找到對應接口的緩存?

1、對於每次請求返回的ETag或Last-Modified,可以使用數據庫存儲或者選擇使用SP存儲,使用URl作爲標識,每次請求時先去查詢對應接口的ETag或Last-Modified。

2、在Header設置ETag或Last-Modified,推薦在一個統一的地方設置,比如攔截器,如果需要針對部分接口採取緩存,可以在Application類中創建一個緩存URL列表,在攔截器中判斷請求的URL是否在這個列表添加Header。

攔截器內部邏輯

3、請求成功後的數據,對於ETag和Last-Modified還是使用SQL或SP存儲。真正的數據則需要針對304和200做不同處理。

返回200:

將數據進行本地持久化處理,採用文件存儲,儲存在統一的緩存文件夾,文件名稱可以採用URL+後綴的形式。

返回304:

使用本地存儲數據,進入緩存文件夾打開URL+後綴的文件,讀入數據並返回。

總結

爲什麼我只講方案不貼具體代碼呢,因爲不好搞(狗頭.jpg),其實上面的緩存步驟,又可以分爲分散處理和統一處理,分散處理你只需在你需要進行緩存的請求回調中進行上面的緩存操作。統一處理既可以在攔截器中也可以在統一封裝的接口回調中進行緩存操作。因爲每個人的項目框架和結構都不相同,很難講一個例子,涵蓋所有情況,如果我只是把我項目中的情況拿出來講,可能對你根本沒什麼幫助,所以還不如講一個大概方案,再結合個人實際選擇符合自己需求的最優解。而對於那些根本沒頭緒的人來說,相信本次例子也是一個很不錯的參考。

如果你喜歡我的分享的話,還請收藏、點贊、多多留言~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章