該系列OkHttp源碼分析基於OkHttp3.14.0版本
概述
用於對連接失敗時重新連接以及對需要重定向的響應進行重定向。
源碼分析
對於所有的攔截器而言,關鍵邏輯都在其intercept()
方法中。
重試
@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) {
...省略部分代碼
Response response;
boolean success = false;
try {
//調用後續的攔截器
response = realChain.proceed(request, transmitter, null);
success = true;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
// 嘗試通過路由連接失敗。 該請求將不會被髮送。
//這裏調用了recover()進行判斷
if (!recover(e.getLastConnectException(), transmitter, false, request)) {
throw e.getFirstConnectException();
}
continue;//重試
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
// 嘗試與服務器通信失敗。 該請求可能已發送。
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, transmitter, requestSendStarted, request)) throw e;
continue;//重試
} finally {
// The network call threw an exception. Release any resources.
// 網絡通話引發異常。 釋放所有資源。
if (!success) {
transmitter.exchangeDoneDueToException();
}
}
...省略部分重定向的代碼
}
}
根據上面的代碼我們可以看到,調用後續攔截器的代碼chain.proced()
是在一個while(true)
循環中的。如果在發生了一些異常的情況下,將會繼續該循環。
那麼哪些情況下才會進行重試呢,主要關注的是下面的幾個方法:
-
recover
-
isRecoverable
recover
/**
* Report and attempt to recover from a failure to communicate with a server. Returns true if
* {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
* be recovered if the body is buffered or if the failure occurred before the request has been
* sent.
* 報告並嘗試從與服務器通信的故障中恢復。
* 如果{@code e}是可恢復的,則返回true;如果失敗是永久的,則返回false。
* 僅在緩衝正文或在發送請求之前發生故障時,才能恢復帶有正文的請求。
* Dong:
* 該方法用於指示是否繼續重試
*/
private boolean recover(IOException e, Transmitter transmitter,
boolean requestSendStarted, Request userRequest) {
// The application layer has forbidden retries.
// 1.client不允許重試
if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again.
// 2.請求已經開始並且發生FileNotFindException,不允許重試
// 3.請求已經開始並且請求體不爲空且請求體只允許讀寫一次,不允許重試
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
// This exception is fatal.
// 4.ProtocolException 不允許重試
// 5.InterruptedIOException但是不爲SocketTimeoutException,不允許重試
// 6.CertificateException 不允許重試
// 7.SSLPeerUnverifiedException 不允許重試
if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt.
// 8.沒有更多的路由嘗試時,不允許重試
if (!transmitter.canRetry()) return false;
// For failure recovery, use the same route selector with a new connection.
// 爲了進行故障恢復,請使用具有新連接的相同路由選擇器。
return true;
}
isRecoverable
/**
* 判斷該異常是否允許重試
* @param e 異常
* @param requestSendStarted 請求是否已經開始
* @return true/false
*/
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// If there was a protocol problem, don't recover.
// 如果存在協議問題,請不要恢復。
if (e instanceof ProtocolException) {
return false;
}
// If there was an interruption don't recover, but if there was a timeout connecting to a route
// we should try the next route (if there is one).
// 如果發生中斷,則無法恢復,但是如果連接到一條路由的超時,我們應該嘗試下一條路由(如果有一條路由)。
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
// again with a different route.
// 查找不太可能通過使用其他路由重試的已知客戶端或協商錯誤。
if (e instanceof SSLHandshakeException) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry.
// 如果問題是來自X509TrustManager的CertificateException,不要重試。
if (e.getCause() instanceof CertificateException) {
return false;
}
}
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
// 例如 證書固定錯誤。
return false;
}
// An example of one we might want to retry with a different route is a problem connecting to a
// proxy and would manifest as a standard IOException. Unless it is one we know we should not
// retry, we return true and try a new route.
// 我們可能想使用其他路由重試的一個示例是連接到代理時出現問題,並表現爲標準IOException。
// 除非它是一個我們不應該重試的方法,否則我們返回true並嘗試一條新路線。
return true;
}
總結所有不允許進行重試的情況
1.配置OkHttpClient時不允許重試
2.請求已經開始並且發生FileNotFindException
3.請求已經開始並且請求體不爲空且請求體只允許讀寫一次
4.發生ProtocolException異常
5.發生InterruptedIOException但是不爲SocketTimeoutException
6.發生CertificateException異常
7.發生SSLPeerUnverifiedException異常
8.沒有更多的路由(線路)
重定向
由於代碼比較多,因此我刪除了部分和重定向無關的代碼。
@Override public Response intercept(Chain chain) throws IOException {
...省略部分代碼
int followUpCount = 0;//重定向次數
Response priorResponse = null;
while (true) {
transmitter.prepareToConnect(request);
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
response = realChain.proceed(request, transmitter, null);
success = true;
}
...省略了部分重試的代碼
// Attach the prior response if it exists. Such responses never have a 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;
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();
}
//最多允許重定向20次
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
request = followUp;
priorResponse = response;
}
}
根據源碼可以看到,重定向的操作是在一個死循環while(true)
中的,而退出這個死循環即完成重定向的條件只有下面幾個:
-
followUp
爲null -
followUpBody
不爲null且followUpBody
只允許傳輸一次 -
超過最大允許重定向的次數
MAX_FOLLOW_UPS
,20次
條件有了,那麼我們就來具體分析一下,什麼時候才能滿足這些條件,由於超過最大重定向次數會拋出異常,因此按照正常業務邏輯,我們比較關注的是前兩個條件。
followUp什麼時候爲null
從代碼中可以看到,followUp
是從followUpRequest()
這個方法返回的,那麼我們進入這個方法看看裏面有什麼。
根據源碼可以看到,整體邏輯主要是根據之前的響應的狀態碼進行不同的邏輯。主要涉及到這幾個狀態碼:
- 407(HTTP_PROXY_AUTH)
- 401(HTTP_UNAUTHORIZED)
- 308(HTTP_PERM_REDIRECT)
- 307(HTTP_TEMP_REDIRECT)
- 300(HTTP_MULT_CHOICE)
- 301(HTTP_MOVED_PERM)
- 302(HTTP_MOVED_TEMP)
- 303(HTTP_SEE_OTHER)
- 408(HTTP_CLIENT_TIMEOUT)
- 503(HTTP_UNAVAILABLE)
比較特殊的是407和408這兩個狀態碼,407代表了需要進行代理服務器的認證,408代表了需要進行認證。這兩個的認證完成都需要我們自己進行處理,否則的話將會返回null。因爲根據我們前面提到的OkHttpClient.Builder
的默認參數中可以看到,authenticator
和proxyAuthenticator
默認都是返回null的。
然後就是3xx系列的狀態碼了,熟悉http協議的可能知道,3xx系列的狀態碼就是和重定向相關的,不同的狀態碼錶示不同的重定向要求以及時效。關於它們之間的區別,可以去看看這篇文章《HTTP中的301、302、303、307、308》
根據源碼可以看到,當狀態碼爲308\308時,如果請求Method不爲”GET“也不爲"HEAD",將直接返回null。
case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
// "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"
// 如果響應GET或HEAD以外的請求而接收到307或308狀態代碼,則用戶代理務不必自動重定向該請求
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
其他的3xx系列狀態碼,會先進行一些必要的參數校驗。
// Does the client allow redirects?
// 判斷當前客戶端是否允許重定向 默認爲true
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;//響應頭中沒有聲明重定向的地址
HttpUrl url = userResponse.request().url().resolve(location);//驗證url是否合法
// Don't follow redirects to unsupported protocols.
// 不要遵循重定向到不支持的協議
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
// 如果已配置,請不要遵循SSL和非SSL之間的重定向。即http和https之間重定向
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
/**如果重定向的協議和之前請求的協議不一致並且client配置時不允許https重定向,則返回null*/
if (!sameScheme && !client.followSslRedirects()) return null;
然後就是構建一個新的Request
並返回了。
對於狀態碼408來說比較特殊,因爲它表示請求超時了,超時一般來說其實不應該叫重定向了,而應該是重試纔對。不太清楚爲何OkHttp會將408放到重定向裏面來。既然是重試,那麼這裏的檢測邏輯和重試其實是有挺相似的,都是檢測配置OkHttpClient
時是否允許重試、RequestBody
是否不爲空且只允許傳輸一次。跟重試不同的是,這裏會檢查上一次響應的狀態碼是否也是408,如果是的話那麼將不會再進行重試了。另外如果響應頭中聲明瞭"Retry-After",並且大於0的話,也不再進行重定向了,也返回null。
case HTTP_CLIENT_TIMEOUT://408
// 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.)
// 408在實踐中很少見,但是像HAProxy這樣的某些服務器使用此響應代碼。
// 規範說,我們可以重複請求而無需進行修改。
// 現代瀏覽器還會重複請求(甚至是非冪等的請求)。
if (!client.retryOnConnectionFailure()) {
// The application layer has directed us not to retry the request.
// 應用層不允許進行重試
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) {
//如果要求延遲一段時間,即響應頭中的"Retry-After">0,返回null
return null;
}
return userResponse.request();
最後是狀態碼503,這個狀態碼的邏輯就很簡單了,除非服務端要求立即重試,即響應頭中的"Retry-After"爲0,否則的話結束重定向,返回null。
followUpBody什麼時候不爲null
根據前面的分析,我們可以知道,followUpBody不爲null的情況只有在狀態碼爲3xx系列的時候。
case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
...
// fall-through
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
...
// Most redirects don't include a request body.
// 大多數重定向不包含請求正文。
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {//排除"GET"和"HEAD"請求
final boolean maintainBody = HttpMethod.redirectsWithBody(method);//是否是"PROPFIND"請求
if (HttpMethod.redirectsToGet(method)) {
//method不是"PROPFIND"
requestBuilder.method("GET", null);
} else {
//method是"PROPFIND",這裏設置了requestBody
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
根據源碼中可以看到,如果爲3xx系列請求,並且請求Method爲"PROPFIND"時,followUpBody是不爲null的。