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¶m2=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è)月冲簿,那我覺得問題不大是整,還可以吃。
minFreshMills 和maxStatleMills都是由請求首部取出的民假,請求可以根據(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 — — —