Hello小夥伴們,現在公司已經恢復了正常辦公,但是疫情依舊還在繼續。最近工作實在是有點小忙,導致更新有點緩慢,實在抱歉,本文是OkHttp原理解析的第二篇, 主要針對OkHttp中幾個 默認攔截器 的具體實現邏輯進行分析。
因爲OkHttp的很大一部分邏輯都在攔截器中,因此本文會比較長,同時採用連載更新的方式進行描述,每完成一個攔截器的邏輯分析都會進行更新。
如有對OkHttp的框架流程不太瞭解的可優先閱讀網我上篇博客 OkHttp原理解析1(框架流程篇)
我又要開始表演了~~~
但爲了方便後續描述,我還是簡單對上文做了個總結。大概分爲以下幾步。
-
創建OkHttpClient(可通過OkHttpClient.Builder創建,內部設置一些基礎參數)
-
創建 Request 對象設置 url,請求類型等參數
-
通過OkHttpClient.newCall()創建RealCall對象,同時創建了Transmitter對象。
-
通過RealCall.enqueue()或者RealCall.execute()觸發 異步或同步 請求。
-
調用OkHttpClient.dispatcher().enqueue(new AsyncCall(responseCallback))做請求準備,循環runningAsyncCalls和readyAsyncCalls 隊列找出host相同的AsyncCall進行重用,並將readyAsyncCalls中AsyncCall轉移到runningAsyncCalls中,如果runningAsyncCalls超過64則終止轉移,如相同主機計數器>5則終止轉移本AsyncCall。
-
循環runningAsyncCalls調用AsyncCall.executeOn(executorService())
6.1. AsyncCall爲Runnable,執行run()方法,調用AsyncCall.execute()連帶調用AsyncCall.getResponseWithInterceptorChain()設置攔截器List,首先設置用戶自定義的攔截器。最後通過RealInterceptorChain.proceed()啓動攔截器。
而攔截器的啓動與運行依賴 責任鏈 模式,大概分爲以下3步。
-
首先創建RealInterceptorChain對象,通過procee()判斷各種異常,並獲取當前Interceptor對象。
-
然後 通過Interceptor.intercept(RealInterceptorChain)啓動當前攔截器邏輯,並且觸發下一個攔截器啓動。
-
如果當前攔截器出現異常等錯誤,則終止責任鏈。
我們從添加攔截的的位置開始本文介紹,其實上文已經做了描述,源碼如下所示。
//RealCall.getResponseWithInterceptorChain();
Response getResponseWithInterceptorChain() throws IOException {
// 建立一個完整的攔截器堆棧
List<Interceptor> interceptors = new ArrayList<>();
//將創建okhttpclient時的攔截器添加到interceptors
interceptors.addAll(client.interceptors());
//重試攔截器,負責處理失敗後的重試與重定向
interceptors.add(new RetryAndFollowUpInterceptor(client));
//請求轉化攔截器(用戶請求轉爲服務器請求,服務器響應轉爲用戶響應)
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//緩存攔截器。負責
//1.根據條件,緩存配置,有效期等返回緩存響應,也可增加到緩存。
//2.設置請求頭(If-None-Match、If-Modified-Since等) 服務器可能返回304(未修改)
//3.可配置自定義的緩存攔截器。
interceptors.add(new CacheInterceptor(client.internalCache()));
//網絡連接攔截器,主要負責和服務器建立連接。
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//創建okhttpclient時設置的networkInterceptors
interceptors.addAll(client.networkInterceptors());
}
//數據流攔截器,主要負責像服務器發送和讀取數據,請求報文封裝和解析。
interceptors.add(new CallServerInterceptor(forWebSocket));
//責任鏈模式的創建。
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
//啓動責任鏈
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
主要看我註釋的部分,OkHttp主要涉及了以下幾個默認攔截器
-
client.interceptors();//用戶自定義攔截器
-
RetryAndFollowUpInterceptor(client)//失敗重試攔截器
-
BridgeInterceptor(client.cookieJar())//請求轉化攔截器
-
CacheInterceptor(client.internalCache())//緩存攔截器
-
ConnectInterceptor(client)//網絡連接攔截器
-
CallServerInterceptor(forWebSocket)//數據流攔截器
OkHttp會把用戶自定義的攔截器默認放到攔截器列表的頭部,以方面優先執行,然後通過創建RealInterceptorChain對象,並調用RealInterceptorChain.proceed()啓動第一個攔截器,然後調用攔截器的interceptor.intercept(next)執行第一個攔截器的邏輯並將下一個攔截器RealInterceptorChain對象傳入依此類推。接下來我們就一個個進行分析。
1. RetryAndFollowUpInterceptor(client) 連接失敗重試攔截器
首先開始的是RetryAndFollowUpInterceptor失敗重試攔截器,這個攔截器是可以在OkHttpClient.Builder對象中通過retryOnConnectionFailure(boolean retryOnConnectionFailure)設置是否開啓,默認構建的OkHttpClient對象是開啓的。
該攔截器主要的職責官方註釋解釋了這麼幾點。
-
無法訪問ip地址,如果URL的主機有多個IP地址,則無法訪問任何單個IP地址不會使整個請求失敗。這可以提高多宿服務的可用性。
-
過時的池連接,通過ConnectionPool重用套接字以減少請求延遲,但這些連接偶爾會超時。
-
無法訪問的代理服務器,可以使用ProxySelector,最終返回到直接連接。
這描述略顯抽象,咱們還是通過源碼看下真像吧。
//我們應該嘗試多少重定向和驗證挑戰?Chrome遵循21個重定向;Firefox、curl和wget遵循20;Safari遵循16;HTTP/1.0建議5。
private static final int MAX_FOLLOW_UPS = 20;
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Transmitter transmitter = realChain.transmitter();
int followUpCount = 0;
Response priorResponse = null;
while (true) {
//通過transmitter創建ExchangeFinder,Address,RouteSelector三個對象
transmitter.prepareToConnect(request);
//判斷如果當前請求結束了則拋出異常,可通過transmitter終止請求。
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
//啓動下一個攔截器
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();
}
}
// 如果body不爲空
if (priorResponse != null) {
//獲得新的response
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Exchange exchange = Internal.instance.exchange(response);
Route route = exchange != null ? exchange.connection().route() : null;
// 調用followUpRequest()查看響應是否需要重定向,不需要就返回當前請求,如果需要返回新的請求
Request followUp = followUpRequest(response, route);
// 不需要重定向或無法重定向
if (followUp == null) {
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();
}
//如果重定向次數超過最大次數拋出異常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
request = followUp;
priorResponse = response;
}
}
//RetryAndFollowUpInterceptor.recover()
//報告並嘗試從與服務器通信失敗中恢復。如果{@code e}可恢復,則返回true;
//如果失敗是永久性的,則返回false。只有在緩衝了正文或在發送請求之前發生故障時,纔可以恢復具有正文的請求。
private boolean recover(IOException e, Transmitter transmitter,
boolean requestSendStarted, Request userRequest) {
// 用戶設置的禁止重試
if (!client.retryOnConnectionFailure()) return false;
// 不能再發送請求體
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
// 異常是致命的
if (!isRecoverable(e, requestSendStarted)) return false;
// 沒有更多的路線可以嘗試。
if (!transmitter.canRetry()) return false;
//對於故障恢復,請對新連接使用同一路由選擇器
return true;
}
/**
* 查看響應是否需要重定向,不需要就返回當前請求,如果需要返回新的請求。
* 找出接收{@code userResponse}時要發出的HTTP請求。這將添加身份驗證頭、遵循重定向或處理客戶端請求超時。
* 如果後續操作不必要或不適用,則返回null。
*/
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) {
//407 代理需要身份認證
case HTTP_PROXY_AUTH:
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
//401 需要身份認證
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
// 300多種選擇。請求的資源可包括多個位置,相應可返回一個資源特徵與地址的列表用於用戶終端(例如:瀏覽器)選擇
case HTTP_MULT_CHOICE:
// 301永久移動。請求的資源已被永久的移動到新URI,返回信息會包括新的URI,瀏覽器會自動定向到新URI。
case HTTP_MOVED_PERM:
// 302臨時移動。與301類似。但資源只是臨時被移動。
case HTTP_MOVED_TEMP:
// 303查看其它地址。與301類似。使用GET和POST請求查看
case HTTP_SEE_OTHER:
// Does the client allow redirects?
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// 不要遵循重定向到不支持的協議。
if (url == null) return null;
//如果已配置,請不要遵循SSL和非SSL之間的重定向。
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// 大多數重定向不包括請求正文。
Request.Builder requestBuilder = userResponse.request().newBuilder();
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");
}
}
// 跨主機重定向時,刪除所有身份驗證頭。這對應用程序層來說可能很煩人,因爲它們無法保留它們。
if (!sameConnection(userResponse.request().url(), url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
//408 超時
case HTTP_CLIENT_TIMEOUT:
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (!client.retryOnConnectionFailure()) {
// 應用層指示我們不要重試請求
return null;
}
RequestBody requestBody = userResponse.request().body();
if (requestBody != null && requestBody.isOneShot()) {
return null;
}
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
// 503 由於超載或系統維護,服務器暫時的無法處理客戶端的請求。延時的長度可包含在服務器的Retry-After頭信息中
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request();
}
return null;
default:
return null;
}
}
//Transmitter.prepareToConnect()
public void prepareToConnect(Request request) {
if (this.request != null) {
//判斷是不是相同的連接,如果是則用之前的。
if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
return; // Already ready.
}
if (exchange != null) throw new IllegalStateException();
//exchangeFinder
if (exchangeFinder != null) {
maybeReleaseConnection(null, true);
exchangeFinder = null;
}
}
this.request = request;
this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
call, eventListener);
}
//Transmitter.createAddress()
private Address createAddress(HttpUrl url) {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
CertificatePinner certificatePinner = null;
if (url.isHttps()) {
//https的設置
sslSocketFactory = client.sslSocketFactory();
hostnameVerifier = client.hostnameVerifier();
certificatePinner = client.certificatePinner();
}
return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
}
//ExchangeFinder.ExchangeFinder()
ExchangeFinder(Transmitter transmitter, RealConnectionPool connectionPool,
Address address, Call call, EventListener eventListener) {
this.transmitter = transmitter;
this.connectionPool = connectionPool;
this.address = address;
this.call = call;
this.eventListener = eventListener;
//創建路由選擇器
this.routeSelector = new RouteSelector(
address, connectionPool.routeDatabase, call, eventListener);
}
//RouteSelector.RouteSelector()
RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
EventListener eventListener) {
this.address = address;
this.routeDatabase = routeDatabase;
this.call = call;
this.eventListener = eventListener;
resetNextProxy(address.url(), address.proxy());
}
該攔截器的邏輯很多,但其實主要做兩件事,一個是重試,一個是重定向,邏輯流程大概是這樣的。
-
通過realChain.transmitter獲取了transmitter對象,並啓用一個while死循環,
-
然後通過transmitter.prepareToConnect(request)transmitter創建ExchangeFinder,Address,RouteSelector三個對象,並判斷了是否是相同的連接,是否需要maybeReleaseConnection(),重置ExchangeFinder。
2.1 . ExchangeFinder此攔截器只是創建ExchangeFinder,但ExchangeFinder中有個find()方法主要通過內部的 findHealthyConnection() 從 connectionPool 中找到一個可用的連接,這個連接可能是複用的,並 connect(),從而得到 輸入/輸出 流 (source/sink) ,返回一個 Exchange 給 CallServerIntercepter , 通過這個 Exchange 就可以添加請求頭和請求體,並讀取響應頭和響應體,來交給上面的 Intercepter,層層向上傳遞。
2.2 . Address爲請求參數的封裝類,包含url,端口,DNS,SSL,Proxy,ProxySelector,SocketFactory,主機名驗證,證書校驗等邏輯。
2.3 . RouteSelector主要來選擇路由,主要做三件事。1.收集所有的可用路由。2.選擇可用路由。3.藉助RouteDatabase內的Set對象來維護連接失敗的路由信息,防止去連接失敗路由浪費時間。
-
啓動攔截器列表的下一個攔截器。
-
判斷全局變量priorResponse是否爲null,如果不爲空則代表請求成功了。
-
執行 followUpRequest()查看響應是否需要重定向,如果不需要重定向則返回當前請求
-
判斷重定向次數,如果超過最大值則退出。
-
重置request,並把當前的Response保存到priorResponse,進入下一次的while循環。
總結來說:
通過while死循環來獲取response,每次循環開始都根據條件獲取下一個request,如果沒有request,則返回response,退出循環。而獲取的request 的條件是根據上一次請求的response 狀態碼確定的,在循環體中同時創建了一些後續需要的對象
感謝觀看,覺得不錯可以點贊關注一下。