本文由
玉剛說寫做平臺
提供寫做贊助java原做者:
竹千代
面試版權聲明:本文版權歸微信公衆號
玉剛說
全部,未經許可,不得以任何形式轉載數據庫
Http是咱們常常打交道的網絡應用層協議,它的重要性可能不須要再強調。可是實際上不少人,包括我本身可能對http瞭解的並不夠深。本文就我本身的學習心得,分享一下我認爲須要知道的緩存所涉及到的相關知識點。瀏覽器
首先咱們來點基礎的,看看http報文具體的格式。http報文能夠分爲請求報文和響應報文,格式大同小異。主要分爲三個部分:緩存
請求報文格式:安全
<method> <request-url> <version>
<headers>
<entity-body>
複製代碼
響應報文格式bash
<version> <status> <reason-phrase>
<headers>
<entity-body>
複製代碼
從請求報文格式和響應報文格式能夠看出,二者主要在起始行上有差別。這裏稍微解釋一下各個標籤:服務器
<method> 指請求方法,經常使用的主要是Get、 Post、Head 還有其餘一些咱們這裏就不說了,有興趣的能夠本身查閱一下
<version> 指協議版本,如今一般都是Http/1.1了
<request-url> 請求地址
<status> 指響應狀態碼, 咱們熟悉的200、404等等
<reason-phrase> 緣由短語,200 OK 、404 Not Found 這種後面的描述就是緣由短語,一般沒必要太關注。
複製代碼
咱們知道請求方法最經常使用的有Get 和Post兩種,面試時也經常會問到這二者有什麼區別,一般什麼狀況下使用。這裏咱們來簡單說一說。微信
兩個方法之間在傳輸形式上有一些區別,經過Get方法發起請求時,會將請求參數拼接在request-url尾部,格式是url?param1=xxx¶m2=xxx&[...]。網絡
咱們須要知道,這樣傳輸參數會使得參數都暴露在地址欄中。而且因爲url是ASCII編碼的,因此參數中若是有Unicode編碼的字符,例如漢字,都會編碼以後傳輸。另外值得注意的是,雖然http協議並無對url長度作限制,可是一些瀏覽器和服務器可能會有限制,因此經過GET方法發起的請求參數不可以太長。而經過POST方法發起的請求是將參數放在請求體中的,因此不會有GET參數的這些問題。
另一點差異就是方法自己的語義上的。GET方法一般是指從服務器獲取某個URL資源,其行爲能夠看做是一個讀操做,對同一個URL進行屢次GET並不會對服務器產生什麼影響。而POST方法一般是對某個URL進行添加、修改,例如一個表單提交,一般會往服務器插入一條記錄。屢次POST請求可能致使服務器的數據庫中添加了多條記錄。因此從語義上來說,二者也是不能混爲一談的。
常見的狀態碼主要有
200 OK 請求成功,實體包含請求的資源
301 Moved Permanent 請求的URL被移除了,一般會在Location首部中包含新的URL用於重定向。
304 Not Modified 條件請求進行再驗證,資源未改變。
404 Not Found 資源不存在
206 Partial Content 成功執行一個部分請求。這個在用於斷點續傳時會涉及到。
在請求報文和響應報文中均可以攜帶一些信息,經過與其餘部分配合,可以實現各類強大的功能。這些信息位於起始行之下與請求實體之間,以鍵值對的形式,稱之爲首部。每條首部以回車換行符結尾,最後一個首部額外多一個換行,與實體分隔開。
這裏咱們重點關注一下
Date
Cache-Control
Last-Modified
Etag
Expires
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match
Http的首部還有不少,但限於篇幅咱們不一一討論。這些首部都是Http緩存會涉及到的,在下文中咱們會來講說各自的做用。
請求發送的資源,或是響應返回的資源。
當咱們發起一個http請求後,服務器返回所請求的資源,這時咱們能夠將該資源的副本存儲在本地,這樣當再次對該url資源發起請求時,咱們能快速的從本地存儲設備中獲取到該url資源,這就是所謂的緩存。緩存既能夠節約沒必要要的網絡帶寬,又能迅速對http請求作出響應。
先擺出幾個概念:
- 新鮮度檢測
- 再驗證
- 再驗證命中
咱們知道,有些url所對應的資源並非一成不變的,服務器中該url的資源可能在必定時間以後會被修改。這時本地緩存中的資源將與服務器一側的資源有差別。
既然在必定時間以後可能資源會改變,那麼在某個時間以前咱們能夠認爲這個資源沒有改變,從而放心大膽的使用緩存資源,當請求時間超過來該時間,咱們認爲這個緩存資源可能再也不與服務器端一致了。因此當咱們發起一個請求時,咱們須要先對緩存的資源進行判斷,看看究竟咱們是否能夠直接使用該緩存資源,這個就叫作新鮮度檢測
。即每一個資源就像一個食品同樣,擁有一個過時時間,咱們吃以前須要先看看有沒有過時。
若是發現該緩存資源已經超過了必定的時間,咱們再次發起請求時不會直接將緩存資源返回,而是先去服務器查看該資源是否已經改變,這個就叫作再驗證
。若是服務器發現對應的url資源並無發生變化,則會返回304 Not Modified
,而且再也不返回對應的實體。這稱之爲再驗證命中
。相反若是再驗證未命中,則返回200 OK
,並將改變後的url資源返回,此時緩存能夠更新以待以後請求。
咱們看看具體的實現方式:
- 新鮮度檢測
咱們須要經過檢測資源是否超過必定的時間,來判斷緩存資源是否新鮮可用。那麼這個必定的時間怎麼決定呢?實際上是由服務器經過在響應報文中增長Cache-Control:max-age
,或是Expire
這兩個首部來實現的。值得注意的是Cache-Control是http1.1的協議規範,一般是接相對的時間,即多少秒之後,須要結合last-modified
這個首部計算出絕對時間。而Expire是http1.0的規範,後面接一個絕對時間。
- 再驗證
若是經過新鮮度檢測發現須要請求服務器進行再驗證,那麼咱們至少須要告訴服務器,咱們已經緩存了一個什麼樣的資源了,而後服務器來判斷這個緩存資源究竟是不是與當前的資源一致。邏輯是這樣沒錯。那怎麼告訴服務器我當前已經有一個備用的緩存資源了呢?咱們能夠採用一種稱之爲條件請求
的方式實現再驗證。
- Http定義了5個首部用於條件請求:
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match
If-Modified-Since 能夠結合Last-Modified
這個服務器返回的響應首部使用,當咱們發起條件請求時,將Last-Modified首部的值做爲If-Modified-Since首部的值傳遞到服務器,意思是查詢服務器的資源自從咱們上一次緩存以後是否有修改。
If-None-Match 須要結合另外一個Etag
的服務器返回的響應首部使用。Etag首部實際上能夠認爲是服務器對文檔資源定義的一個版本號。有時候一個文檔被修改了,可能所作的修改極爲微小,並不須要全部的緩存都從新下載數據。或者說某一個文檔的修改週期極爲頻繁,以致於以秒爲時間粒度的判斷已經沒法知足需求。這個時候可能就須要Etag這個首部來代表這個文檔的版號了。發起條件請求時可將緩存時保存下來的Etag的值做爲If-None-Match首部的值發送至服務器,若是服務器的資源的Etag與當前條件請求的Etag一致,代表此次再驗證命中。
其餘三個與斷點續傳涉及到的相關知識有關,本文暫時不討論。待我以後寫一篇文章來說講斷點續傳。
緩存的Http理論知識大體就是這麼些。咱們從OkHttp的源碼來看看,這些知名的開源庫是如何利用Http協議實現緩存的。這裏咱們假設讀者對OkHttp的請求執行流程有了大體的瞭解,而且只討論緩存相關的部分。對於OkHttp代碼不熟悉的同窗,建議先看看相關代碼或是其餘文章。
咱們知道OkHttp的請求在發送到服務器以前會通過一系列的Interceptor,其中有一個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 獲取到對應請求的緩存。這裏咱們不展開討論這個類的具體實現,只須要知道,若是以前緩存了該請求url的資源,那麼經過request對象能夠查找到這個緩存響應。
將獲取到的緩存響應,當前時間戳和請求傳入CacheStrategy,而後經過執行get方法執行一些邏輯最終能夠獲取到strategy.networkRequest,strategy.cacheResponse。若是經過CacheStrategy的判斷以後,咱們發現此次請求沒法直接使用緩存數據,須要向服務器發起請求,那麼咱們就經過CacheStrategy爲咱們構造的networkRequest來發起此次請求。咱們先來看看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的構造方法首先保存了傳入的參數,並將緩存響應的相關首部解析保存下來。以後調用的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不爲空,表示咱們此次請求須要發到服務器,此時若是請求的cacheControl要求本次請求只使用緩存數據。那麼此次請求恐怕只能以失敗了結了,這點咱們等會兒回到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);
}
......
}
複製代碼
上面這段代碼主要列出四種狀況下須要忽略緩存,直接想服務器發起請求的狀況:
這些狀況下直接構造一個包含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());
}
......
}
複製代碼
若是緩存響應的Cache-Control首部包含immutable,那麼說明該資源不會改變。客戶端能夠直接使用緩存結果。值得注意的是immutable並不屬於http協議的一部分,而是由facebook提出的擴展屬性。
以後分別計算ageMills、freshMills、minFreshMills、maxStaleMills這四個值。
若是響應緩存沒有經過Cache-Control:No-Cache 來禁止客戶端使用緩存,而且
ageMillis + minFreshMillis < freshMillis + maxStaleMillis
複製代碼
這個不等式成立,那麼咱們進入條件代碼塊以後最終會返回networkRequest爲空,而且使用當前緩存值構造的CacheStrtegy。
這個不等式到底是什麼含義呢?咱們看看這四個值分別表明什麼。
ageMills 指這個緩存資源自響應報文在源服務器中產生或者過時驗證的那一刻起,到如今爲止所通過的時間。用食品的保質期來比喻的話,比如當前時間距離生產日期已通過去了多久了。
freshMills 表示這個資源在多少時間內是新鮮的。也就是假設保質期18個月,那麼這個18個月就是freshMills。
minFreshMills 表示我但願這個緩存至少在多久以後依然是新鮮的。比如我是一個比較講究的人,若是某個食品只有一個月就過時了,雖然並無真的過時,但我依然以爲食品不新鮮從而不想再吃了。
maxStaleMills比如我是一個不那麼講究的人,即便食品已通過期了,只要不是過時好久了,好比2個月,那我以爲問題不大,還能夠吃。
minFreshMills 和maxStatleMills都是由請求首部取出的,請求能夠根據本身的須要,經過設置
Cache-Control:min-fresh=xxx、Cache-Control:max-statle=xxx
複製代碼
來控制緩存,以達到對緩存使用嚴格性的收緊與放鬆。
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);
}
複製代碼
若是以前的條件不知足,說明咱們的緩存響應已通過期了,這時咱們須要經過一個條件請求對服務器進行再驗證操做。接下來的代碼比較清晰來,就是經過從緩存響應中取出的Last-Modified
,Etag
,Date
首部構造一個條件請求並返回。
接下來咱們返回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
都爲空,說明咱們即沒有可用的緩存,同時請求經過Cache-Control:only-if-cached
只容許咱們使用當前的緩存數據。這個時候咱們只能返回一個504的響應。接着往下看,
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
複製代碼
若是networkRequest爲空,說明咱們不須要進行再驗證了,直接將cacheResponse做爲請求結果返回。
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存在不爲空,說明此次請求是須要發到服務器的。此時有兩種狀況,一種cacheResponse不存在,說明咱們沒有一個可用的緩存,此次請求只是一個普通的請求。若是cacheResponse存在,說明咱們有一個可能過時了的緩存,此時networkRequest是一個用來進行再驗證的條件請求。
無論哪一種狀況,咱們都須要經過networkResponse=chain.proceed(networkRequest)獲取到服務器的一個響應。不一樣的只是若是有緩存數據,那麼在獲取到再驗證的響應以後,須要cache.update(cacheResponse, response)去更新當前緩存中的數據。若是沒有緩存數據,那麼判斷這次請求是否能夠被緩存。在知足緩存的條件下,將響應緩存下來,並返回。
OkHttp緩存大體的流程就是這樣,咱們從中看出,整個流程是遵循了Http的緩存流程的。最後咱們總結一下緩存的流程:
OAuth是一個用於受權第三方獲取相應資源的協議。與以往的受權方式不一樣的是,OAuth的受權能避免用戶暴露本身的用戶密碼給第三方,從而更加的安全。OAuth協議經過設置一個受權層,以區分用戶和第三方應用。用戶自己能夠經過用戶密碼登錄服務提供商,獲取到帳戶全部的資源。而第三方應用只能經過向用戶請求受權,獲取到一個Access Token,用以登錄受權層,從而在指定時間內獲取到用戶受權訪問的部分資源。
OAuth定義的幾個角色:
Role | Description |
---|---|
Resource Owner | 能夠受權訪問某些受保護資源的實體,一般就是指用戶 |
Client | 能夠經過用戶的受權訪問受保護資源的應用,也就是第三方應用 |
Authorization server | 在認證用戶以後給第三方下發Access Token的服務器 |
Resource Server | 擁有受保護資源的服務器,能夠經過Access Token響應資源請求 |
+--------+ +---------------+
| |--(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 ---| |
+--------+ +---------------+
複製代碼
從上圖能夠看出,一個OAuth受權的流程主要能夠分爲6步:
簡單的說 Http + 加密 + 認證 + 完整性保護 = Https
傳統的Http協議是一種應用層的傳輸協議,Http直接與TCP協議通訊。其自己存在一些缺點:
所以,在一些須要保證安全性的場景下,好比涉及到銀行帳戶的請求時,Http沒法抵禦這些攻擊。
Https則能夠經過增長的SSL\TLS,支持對於通訊內容的加密,以及對通訊雙方的身份進行驗證。
近代密碼學中加密的方式主要有兩類:
對稱祕鑰加密是指加密與解密過程使用同一把祕鑰。這種方式的優勢是處理速度快,可是如何安全的從一方將祕鑰傳遞到通訊的另外一方是一個問題。
非對稱祕鑰加密是指加密與解密使用兩把不一樣的祕鑰。這兩把祕鑰,一把叫公開祕鑰,能夠隨意對外公開。一把叫私有祕鑰,只用於自己持有。獲得公開祕鑰的客戶端可使用公開祕鑰對傳輸內容進行加密,而只有私有祕鑰持有者自己能夠對公開祕鑰加密的內容進行解密。這種方式克服了祕鑰交換的問題,可是相對於對稱祕鑰加密的方式,處理速度較慢。
SSL\TLS的加密方式則是結合了兩種加密方式的優勢。首先採用非對稱祕鑰加密,將一個對稱祕鑰使用公開祕鑰加密後傳輸到對方。對方使用私有祕鑰解密,獲得傳輸的對稱祕鑰。以後雙方再使用對稱祕鑰進行通訊。這樣即解決了對稱祕鑰加密的祕鑰傳輸問題,又利用了對稱祕鑰的高效率來進行通訊內容的加密與解密。
SSL\TLS採用的混合加密的方式仍是存在一個問題,即怎麼樣確保用於加密的公開祕鑰確實是所指望的服務器所分發的呢?也許在收到公開祕鑰時,這個公開祕鑰已經被別人篡改了。所以,咱們還須要對這個祕鑰進行認證的能力,以確保咱們通訊的對方是咱們所指望的對象。
目前的作法是使用由數字證書認證機構頒發的公開祕鑰證書。服務器的運營人員能夠向認證機構提出公開祕鑰申請。認證機構在審覈以後,會將公開祕鑰與共鑰證書綁定。服務器就能夠將這個共鑰證書下發給客戶端,客戶端在收到證書後,使用認證機構的公開祕鑰進行驗證。一旦驗證成功,便可知道這個祕鑰是能夠信任的祕鑰。
總結 Https的通訊流程: