引言
上一篇部落格籠統的講了各個攔截器:
- RetryAndFollowUpInterceptor
- BridgeInterceptor
- CacheInterceptor
- ConnectInterceptor
- CallServerInterceptor
從這邊部落格開始單獨分析每個攔截器的原始碼。首先是RetryAndFollowUpInterceptor。
這個攔截器它的作用主要是負責請求的重定向操作,用於處理網路請求中,請求失敗後的重試機制。
核心功能
連線失敗重試(Retry)
在發生 RouteException 或者 IOException 後,會捕獲建聯或者讀取的一些異常,根據一定的策略判斷是否是可恢復的,如果可恢復會重新建立 StreamAllocation 開始新的一輪請求
繼續發起請求(Follow up)
主要有這幾種型別
- 3xx 重定向
- 401,407 未授權,呼叫 Authenticator 進行授權後繼續發起新的請求
- 408 客戶端請求超時,如果 Request 的請求體沒有被
UnrepeatableRequestBody
標記,會繼續發起新的請求
其中 Follow up 的次數受到MAX_FOLLOW_UP
約束,在 OkHttp 中為 20 次,這樣可以防止重定向死迴圈。
原始碼分析
這裡詳細分析重定向機制,關於RealCall的構造方法,原始碼如下所示
RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
final EventListener.Factory eventListenerFactory = client.eventListenerFactory();
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
// TODO(jwilson): this is unsafe publication and not threadsafe.
this.eventListener = eventListenerFactory.create(this);
}複製程式碼
根據之前攔截器的介紹,會執行RetryAndFollowUpInterceptor的intercept方法。
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
// (1)“建立StreamAllocation”
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
//(2)進入迴圈,檢查請求是否已被取消
Response response = null;
boolean releaseConnection = true;
try {
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
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, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
//(3)“執行RealInterceptorChain.procced,並捕獲異常來判斷是否可以重定向”
// 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();
}
Request followUp = followUpRequest(response);
//(4)“是否需要進行重定向”
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
//(5)“不需要重定向,直接返回response,結束。否則到6”
closeQuietly(response.body());
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
// (6)“重定向安全檢查”
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(followUp.url()), callStackTrace);
//(7)“根據新的請求建立連線”
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}
}複製程式碼
可以看出主要攔截處理過程是個while迴圈,如果沒有重定向則返回response,有則按迴圈內處理。
主要有以下5步:
- 建立StreamAllocation
- 進入迴圈,檢查請求是否已被取消
- 執行RealInterceptorChain.procced,並捕獲異常來判斷是否可以重定向
- 是否需要進行重定向
- 不需要重定向,直接返回response,結束。否則到6
- 重定向安全檢查
- 根據新的請求建立連線
下面逐個分析:
1.建立StreamAllocation
Request request = chain.request();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);複製程式碼
它主要用於管理客戶端與伺服器之間的連線,同時管理連線池,以及請求成功後的連線釋放等操作。
在執行StreamAllocation建立時,可以看到根據客戶端請求的地址url,還呼叫了createAddress方法。進入該方法可以看出,這裡返回了一個建立成功的Address,實際上Address就是將客戶端請求的網路地址,以及伺服器的相關資訊,進行了統一的包裝,也就是將客戶端請求的資料,轉換為OkHttp框架中所定義的伺服器規範,這樣一來,OkHttp框架就可以根據這個規範來與伺服器之間進行請求分發了。
private Address createAddress(HttpUrl url) {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
CertificatePinner certificatePinner = null;
if (url.isHttps()) {
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());
}複製程式碼
2.檢查該請求是否已被取消
這裡會進入迴圈,首先會進行一個安全檢查操作,檢查當前請求是否被取消,如果這時請求被取消了,則會通過StreamAllocation釋放連線,並丟擲異常,原始碼如下所示
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}複製程式碼
3.捕獲異常來判斷是否可以重定向
3.1 捕獲異常
如果這時沒有發生2,接下來會通過RealInterceptorChain的proceed方法處理請求,在請求過程中,只要發生異常,releaseConnection就會為true,一旦變為true,就會將StreamAllocation釋放掉。
根據之前的博文,RealInterceptorChain的proceed會呼叫後面的攔截器的攔截方法,如果有問題,會拋2種異常:
RouteException
這個異常發生在 Request 請求還沒有發出去前,就是開啟 Socket 連線失敗。這個異常是 OkHttp 自定義的異常,是一個包裹類,包裹住了建聯失敗中發生的各種 Exception
主要發生 ConnectInterceptor 建立連線環節
比如連線超時丟擲的 SocketTimeoutException,包裹在 RouteException 中
IOException
這個異常發生在 Request 請求發出並且讀取 Response 響應的過程中,TCP 已經連線,或者 TLS 已經成功握手後,連線資源準備完畢
主要發生在 CallServerInterceptor 中,通過建立好的通道,傳送請求並且讀取響應的環節
比如讀取超時丟擲的 SocketTimeoutException
3.2 重試判斷
在捕獲到這兩種異常後,OkHttp 會使用 recover 方法來判斷是否是不可以重試的。然後有兩種處理方式:
不可重試的
會把繼續把異常丟擲,呼叫 StreamAllocation 的
streamFailed
和release
方法釋放資源,結束請求。OkHttp 有個黑名單機制,用來記錄發起失敗的 Route,從而在連線發起前將之前失敗的 Route 延遲到最後再使用,streamFailed 方法可以將這個出問題的 route 記錄下來,放到黑名單(RouteDatabase)。所以下一次發起新請求的時候,上次失敗的 Route 會延遲到最後再使用,提高了響應成功率可以重試的
則繼續使用 StreamAllocation 開始新的
proceed
。是不是可以無限重試下去?並不是,每一次重試,都會呼叫 RouteSelector 的 next 方法獲取新的 Route,當沒有可用的 Route 後就不會再重試了
什麼情況下的失敗是不可以恢復的呢?
繼續看recover方法:
private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// 需要OkHttpClient配置可以重試
if (!client.retryOnConnectionFailure()) return false;
// 使用 isRecoverable 方法過濾掉不可恢復異常
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// 使用 isRecoverable 方法過濾掉不可恢復異常
if (!isRecoverable(e, requestSendStarted)) return false;
// 已經沒有其他的路由可以使用
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}複製程式碼
簡單分析 recover
的程式碼,首先會呼叫 StreamAllocation 的 streamFailed
方法釋放資源。然後通過以下4種策略來判斷這些型別是否可以重試:
(1)OkHttpClient是否配置可以重試
OkHttpClient okHttpClient = new OkHttpClient.Builder()
...
.retryOnConnectionFailure(true)
...
.build();複製程式碼
(2)不是被 RouteException 包裹的異常,並且請求的內容被 UnrepeatableRequestBody 標記
也就是不是建立連線階段發生的異常,比如發起請求和獲取響應的時候發生的IOException,同時請求的內容不可重複發起,就不能重試
(3) 使用 isRecoverable 方法過濾掉不可恢復異常
通過的話會繼續進入第 4 步。
不可重試的異常有:
ProtocolException
協議異常,主要發生在 RealConnection 中建立 HTTPS 通過 HTTP 代理進行連線重試超過 21 次。不可以重試
private void buildTunneledConnection(int connectTimeout, int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException { ... int attemptedConnections = 0; int maxAttempts = 21; while (true) { if (++attemptedConnections > maxAttempts) { throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts); } ... } ... }12345678910111213複製程式碼
InterruptedIOException
如果是建立連線時 SocketTimeoutException,即建立 TCP 連線超時,會走到第4步
如果是連線已經建立,在讀取響應的超時的 SocketTimeoutException,不可恢復
CertificateException 引起的 SSLHandshakeException
證照錯誤導致的異常,比如證照製作錯誤
SSLPeerUnverifiedException
訪問網站的證照不在你可以信任的證照列表中
前面三步條件都通過的,還需要最後一步檢驗,就是獲取可用的 Route。
public boolean hasMoreRoutes() {
return route != null || routeSelector.hasNext();
} 123複製程式碼
所以滿足下面兩個條件就可以結束請求,釋放 StreamAllocation 的資源了
- StreamAllocation 的 route 為空
- RouteSelector 沒有可選擇的路由了
- 沒有下一個 IP
- 沒有下一個代理
- 沒有下一個延遲使用的 Route(之前有失敗過的路由,會在這個列表中延遲使用)
RouteSelector 封裝了選擇可用路由進行連線的策略。重試一個重要作用,就是這個請求存在多個代理,多個 IP 情況下,OkHttp 幫我們在連線失敗後換了個代理和 IP,而不是同一個代理和 IP 反覆重試。所以,理所當然的,如果沒有其他 IP 了,那麼就會停止。比如 DNS 對域名解析後會返回多個 IP。比如有三個 IP,IP1,IP2 和 IP3,第一個連線超時了,會換成第二個;第二個又超時了,換成第三個;第三個還是不給力,那麼請求就結束了
但是 OkHttp 在執行以上策略前,也就是 RouteSelector 內部的策略前,還有一個判斷,就是該 StreamAllocation 的當前 route 是否為空,如果不為空話會繼續使用該 route 而沒有走入到 RouteSelector 的策略中。
4.是否需要進行重定向
我們知道,是否需要進行請求重定向,是根據http請求的響應碼來決定的,因此,在followUpRequest方法中,將會根據響應userResponse,獲取到響應碼,並從連線池StreamAllocation中獲取連線,然後根據當前連線,得到路由配置引數Route。觀察以下程式碼可以看出,這裡通過userResponse得到了響應碼responseCode。
private Request followUpRequest(Response userResponse) throws IOException {
if (userResponse == null) throw new IllegalStateException();
Connection connection = streamAllocation.connection();
Route route = connection != null
? connection.route()
: null;
int responseCode = userResponse.code();複製程式碼
接下來將會根據響應碼responseCode來檢查當前是否需要進行請求重定向,我們知道,在Http響應碼中,處於3XX的,都需要進行請求重定向處理。因此,接下來該方法會通過switch…case…來進行不同的響應碼處理操作。
從這段程式碼開始,都是3xx的響應碼處理,這裡就開始進行請求重定向的處理操作。
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
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:複製程式碼
執行重定向請求構建前,首先根據OkHttpClient,執行了followRedirects方法,檢查客戶端是否允許進行重定向請求。如果這時客戶端未允許重定向,則會返回null。
接下來根據響應獲取到位置location,然後根據location,得到重定向的url,程式碼如下所示。
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);複製程式碼
接下來這一大段程式碼就是重新構建Request的過程,根據重定向得到的url,重新構建Request請求,並對請求中的header和body分別進行處理,最後可以看到,通過build模式,將新構建的Request請求進行返回。
// Don't follow redirects to unsupported protocols.
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
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");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();複製程式碼
5.不需要重定向,直接返回response,結束。否則到6
執行followUpRequest方法,來檢查是否需要進行重定向操作。當不需要進行重新定向操作時,就會直接返回Response,如下所示
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}複製程式碼
6.重定向安全檢查
1.重定向次數<20
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}複製程式碼
2.然後根據重定向請求followUp,與當前的響應進行對比,檢查是否同一個連線。
通常,當發生請求重定向時,url地址將會有所不同,也就是說,請求的資源在這時已經被分配了新的url。因此,接下來的!sameConnection這個判斷將會符合,該這個方法就是用來檢查重定向請求,和當前的請求,是否為同一個連線。一般來說,客戶端進行重定向請求時,需要與新的url建立連線,而原先的連線,則需要進行銷燬。
判斷是否為同一個連線:
private boolean sameConnection(Response response, HttpUrl followUp) {
HttpUrl url = response.request().url();
return url.host().equals(followUp.host())
&& url.port() == followUp.port()
&& url.scheme().equals(followUp.scheme());
}複製程式碼
不是同一個連線,則重新建立新連線,並銷燬原來的連線:
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(followUp.url()), callStackTrace);
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}複製程式碼
至此,RetryAndFollowUpInterceptor就分析完畢了。