你好,我是 N0tExpectErr0r,一名熱愛技術的 Android 開發
我的個人部落格:blog.N0tExpectErr0r.cn
OkHttp 原始碼剖析系列文章目錄:
OkHttp 原始碼剖析系列(一)——請求的發起及攔截器機制概述
之前的文章介紹到了 OkHttp 的攔截器機制的整體概述,現在讓我們依次研究一下其攔截器的實現。
RetryAndFollowUpInterceptor
前面提到,RetryAndFollowUpInerceptor
負責了 HTTP 請求的重定向功能,那讓我們先了解一下 HTTP 協議中的重定向。
HTTP 中的重定向
HTTP 協議提供了一種重定向的功能,它通過由伺服器返回特定格式的響應從而觸發客戶端的重定向。其對應的 Response Code 格式為 3XX,並且會在 Response Header 的 Location
欄位中放入新的 URL,這樣我們客戶端就可以根據該 Location 欄位所指定的 URL 重新請求從而得到需要的資料。
其過程如下圖所示:
其中重定向對應的狀態碼及含義如下表所示(摘自維基百科):
重定向與伺服器轉發的區別
可以發現,重定向和伺服器轉發請求是有些相似的,它們有什麼不同呢?
-
重定向是客戶端行為,而伺服器轉發則是服務端行為
-
重定向我們的客戶端發出了多次請求,而轉發我們的客戶端只發出了一次請求。
-
重定向的控制權在客戶端,轉發的控制權在服務端。
###程式碼分析
接下來讓我們研究一下 RetryAndFollowUpInterceptor
的實現原理,我們看到 RetryAndFollowUpInterceptor.intercept
方法:
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// 獲取transmitter
Transmitter transmitter = realChain.transmitter();
int followUpCount = 0;
Response priorResponse = null;
while (true) {
// 進行一些連線前的準備工作
transmitter.prepareToConnect(request);
// 處理取消事件
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
// 呼叫chain的proceed方法獲取下層得到的結果
response = realChain.proceed(request, transmitter, null);
success = true;
} catch (RouteException e) {
// 若不滿足重定向的條件,丟擲異常
if (!recover(e.getLastConnectException(), transmitter, false, request)) {
throw e.getFirstConnectException();
}
// 滿足重定向條件,重試
continue;
} catch (IOException e) {
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
// 不滿足重定向條件,丟擲異常
if (!recover(e, transmitter, requestSendStarted, request)) throw e;
// 滿足重定向條件,重試
continue;
} finally {
if (!success) {
// 若丟擲了異常,釋放資源
transmitter.exchangeDoneDueToException();
}
}
// 在本次response中設定上一次的response,其body為空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Exchange exchange = Internal.instance.exchange(response);
Route route = exchange != null ? exchange.connection().route() : null;
// 根據response code獲取重定向後的request
Request followUp = followUpRequest(response, route);
if (followUp == null) {
// 不再需要重定向,停止timeout計時並返回response
if (exchange != null && exchange.isDuplex()) {
transmitter.timeoutEarlyExit();
}
return response;
}
RequestBody followUpBody = followUp.body();
if (followUpBody != null && followUpBody.isOneShot()) {
return response;
}
closeQuietly(response.body());
if (transmitter.hasExchange()) {
exchange.detachWithViolence();
}
// 重定向不超過20次,否則丟擲異常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
// 修改下次重定向的request
request = followUp;
// 記錄上一次的response
priorResponse = response;
}
}
複製程式碼
可以看到,這裡外部通過一個迴圈,實現不斷重定向,可以看一下迴圈內主要做了什麼:
- 進行一些預處理
- 呼叫
chain.proceed
方法進行請求獲取Response
- 過程中若下層丟擲異常,則嘗試重定向
- 若不滿足重定向條件,則丟擲異常
- 若出現其他未知的異常,則通過丟擲異常釋放資源
- 在本次
Response
中設定上一次的 ResponsepriorResponse
,且body為空 - 根據
Response
中的 response code 進行重定向,呼叫followUpRequest
方法獲取重定向後的 requestfollowUp
- 若重定向後的
followUp
為 null,說明不再需要重定向,停止 timeout 計時並返回Response
- 若重定向超過指定次數(預設 20 次),則丟擲異常。
- 若仍未返回,則需要下一次重定向,對下一次的
request
等變數進行賦值。
讓我們看看 followUpRequest
方法做了什麼:
private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
case HTTP_PROXY_AUTH: // 407
// ...
// 代理身份認證
case HTTP_UNAUTHORIZED: // 401
// ...
// 身份認證
case HTTP_PERM_REDIRECT: // 308
case HTTP_TEMP_REDIRECT: // 307
// 307、308 兩種狀態碼不對 GET、HEAD 以外的請求重定向
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
case HTTP_MULT_CHOICE: // 300
case HTTP_MOVED_PERM: // 301
case HTTP_MOVED_TEMP: // 302
case HTTP_SEE_OTHER: // 303
// 若客戶端關閉了重定向,則直接返回 null
if (!client.followRedirects()) return null;
// 獲取LocationHeader以獲取重定向目標
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// ...
Request.Builder requestBuilder = userResponse.request().newBuilder();
// 處理重定向使用的method
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// 重新構建request
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT: // 408
// 408 說明需要重新傳送一次相同的請求
// ...
return userResponse.request();
case HTTP_UNAVAILABLE: // 503
// ...
return null;
default:
return null;
}
}
複製程式碼
可以看到,主要是針對重定向的幾個狀態碼進行特殊處理,從中取出 Location
欄位,構造重定向後的 request
。
BridgeInterceptor
BridgeInterceptor
的名字取的非常形象,它就像一座橋樑,連線了使用者與伺服器。在使用者向伺服器傳送請求時,它會把使用者所構建的請求轉換為向伺服器請求的真正的 Request
,而在伺服器返回了響應後,它又會將伺服器所返回的響應轉換為使用者所能夠使用的 Response
。
讓我們看到 BridgeInterceptor.intercept
方法:
@Override
public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
// 將一些userRequest中的屬性設定進builder中
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
boolean transparentGzip = false;
// 若未設定Accept-Encoding,自動設定gzip
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
// 將userRequest中的cookies設定進builder
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
// 設定user-agent
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
// 讀取服務端響應
Response networkResponse = chain.proceed(requestBuilder.build());
// 對響應的header進行處理
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
// 根據服務端的響應構建新的Response,並將userRequest設定為其request
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
// 若之前設定了gzip壓縮且response中也包含了gzip壓縮,則進行gzip解壓
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
複製程式碼
可以看到,這裡主要對 Header 進行處理,將一些原來 request
中的 Header 進行處理後設定進了新 request
,並用其進行請求。其中若呼叫者未設定 Accept-Encoding
,則它會預設設定 gzip。
而在對 response
處理時,若之前設定了 gzip,則進行 gzip 解壓。這種自動解壓會自動將 Content-Length
、Content-Encoding
欄位從 Header 中移除,因此上層可能會獲取到 -1。
而這裡關於 Cookie
的處理我們暫時不關心,後續文章中再對其作介紹。
CacheInterceptor
CacheInterceptor
主要負責了對快取的讀取以及更新,讓我們看看其 intercept
方法:
@Override
public Response intercept(Chain chain) throws IOException {
// 嘗試獲取快取的cache
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 傳入當前時間、request以及從快取中取出的cache,構建快取策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
// 通過快取策略獲取新的request
Request networkRequest = strategy.networkRequest;
// 通過快取策略獲取快取中取出的response
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn t applicable. Close it.
}
// 根據快取策略若不能使用網路且沒有快取,則請求失敗,構建一個請求失敗的Response並返回
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();
}
// 如果不需要網路請求,則直接返回
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// 網路請求獲取response
networkResponse = chain.proceed(networkRequest);
} finally {
// 如果IO的過程中出現了crash,回收資源
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 如果快取中有快取,並且請求的code為304,則結合快取及網路請求結果後返回,並且更新快取中的內容
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) { // 304
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 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;
}
複製程式碼
可以看到,這裡主要是以下步驟
- 嘗試從快取中獲取了快取的
response
- 根據 當前時間、
request
、快取的response
構建快取策略。 - 若快取策略不能使用網路(
networkRequest == null
),且無快取(cacheResponse == null
),則直接請求失敗。 - 若快取策略不能使用網路,由於前面有判斷所以可以確定有快取,直接構建快取的
response
並返回。 - 呼叫
chain.proceed
網路請求獲取response
- 對 code 304 作出處理,結合本地及網路返回資料構建
response
並返回 - 構建網路請求的所獲得的
response
,並且由於該網路請求並未進行過快取,進行快取並返回結果
而關於快取相關的具體實現這裡先不過多做介紹,後面會專門開一篇文章進行分析,這裡主要以流程為主。
ConnectInterceptor
ConnectInterceptor
主要負責的是與伺服器的連線的建立,它的程式碼非常短:
@Override
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
Transmitter transmitter = realChain.transmitter();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
// 構建Exchange
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
return realChain.proceed(request, transmitter, exchange);
}
複製程式碼
這裡主要是呼叫 transmitter.newExchange
構建一個 Exchange
,之後呼叫了 realChain.proceed(request, transmitter, exchange)
方法。
這個 Exchange
類究竟是什麼呢?我們看到它的 JavaDoc:
Transmits a single HTTP request and a response pair. This layers connection management and events on {@link ExchangeCodec}, which handles the actual I/O.
也就是說 Exchange
類可以將 ExchangeCodec
這個類的連線管理及事件進行分層,而 ExchangeCodec
是一個真正執行 I/O 的類,看來這個類主要是進行一些連線管理的事務。在 newExchange
的過程中可能就建立/複用了客戶與伺服器的連線。
這裡具體的連線獲取過程我們暫時先不做介紹,在後續文章中會詳細進行介紹,此篇文章更偏向整體流程的講解。
CallServerInterceptor
CallServerInterceptor
是整個網路請求鏈的最後一個攔截器,它真正實現了對伺服器 Response
的讀取,讓我們看看它的實現:
@Override
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
// 寫入請求頭
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// 對 100-continue 這一 header 做特殊處理
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();
responseHeadersStarted = true;
exchange.responseHeadersStart();
responseBuilder = exchange.readResponseHeaders(true);
}
if (responseBuilder == null) {
// 寫入請求體
if (request.body().isDuplex()) {
// Prepare a duplex body so that the application can send a request body later.
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
request.body().writeTo(bufferedRequestBody);
} else {
// Write the request body if the "Expect: 100-continue" expectation was met.
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false));
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
} else {
exchange.noRequestBody();
if (!exchange.connection().isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
exchange.noNewExchangesOnConnection();
}
}
} else {
exchange.noRequestBody();
}
if (request.body() == null || !request.body().isDuplex()) {
exchange.finishRequest();
}
if (!responseHeadersStarted) {
exchange.responseHeadersStart();
}
if (responseBuilder == null) {
// 讀取響應頭
responseBuilder = exchange.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
// 讀取響應頭
response = exchange.readResponseHeaders(false)
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
exchange.responseHeadersEnd(response);
// 讀取響應體
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
exchange.noNewExchangesOnConnection();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
複製程式碼
這裡程式碼量非常多,但其實核心是下面幾步:
- 寫入
Request Header
- 寫入
Request Body
- 讀取
Response Header
- 讀取
Response Body
其具體實現我們後續文章再進行介紹,到了這裡整個責任鏈的大體流程我們就分析完了。