簡(jiǎn)介
okhttp的網(wǎng)絡(luò)請(qǐng)求采用interceptors鏈的模式。每一級(jí)interceptor只處理自己的工作,然后將剩余的工作廉涕,交給下一級(jí)interceptor。本文將主要閱讀okhttp中的RetryAndFollowUpInterceptor艇拍,了解它的作用和工作原理狐蜕。
RetryAndFollowUpInterceptor
顧名思義,RetryAndFollowUpInterceptor負(fù)責(zé)okhttp的請(qǐng)求失敗的恢復(fù)和重定向卸夕。
核心的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) {
transmitter.prepareToConnect(request);
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
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.
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();
}
}
...
}
}
前半段的邏輯中层释,RetryAndFollowUpInterceptor做了幾件事:
- 通過(guò)Transmitter準(zhǔn)備連接
- 執(zhí)行請(qǐng)求鏈下一級(jí)
- 處理了下一級(jí)請(qǐng)求鏈中的RouteException和IOException。
Transmitter的實(shí)現(xiàn)快集,以后的章節(jié)再單獨(dú)講解贡羔。此處略過(guò)。我們重點(diǎn)看一下个初,RetryAndFollowUpInterceptor如何處理兩個(gè)異常乖寒。
RouteException
從注釋中,我們可以看到勃黍,RouteException表示客戶端連接路由失敗宵统。此時(shí)會(huì)調(diào)用recover
方法,如果recover方法再失敗覆获,會(huì)拋出RouteException中的FirstConnectException。
我們看一下recover
方法的實(shí)現(xiàn):
/**
* 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.
*/
private boolean recover(IOException e, Transmitter transmitter,
boolean requestSendStarted, Request userRequest) {
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again.
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt.
if (!transmitter.canRetry()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
首先我們調(diào)用應(yīng)用層的失敗回調(diào)瓢省,如果應(yīng)用層返回false弄息,就不再進(jìn)行重試。
然后勤婚,我們判斷請(qǐng)求的返回摹量,如果請(qǐng)求已經(jīng)開(kāi)始或請(qǐng)求限定,只能請(qǐng)求一次,我們也不再進(jìn)行重試缨称。其中凝果,只能請(qǐng)求一次,可能是客戶端自行設(shè)定的睦尽,也可能是請(qǐng)求返回了404器净。明確告知了文件不存在,也不會(huì)再重復(fù)請(qǐng)求当凡。
接下來(lái)山害,是okhttp認(rèn)為的致命錯(cuò)誤,不會(huì)再重復(fù)請(qǐng)求的沿量,都會(huì)在isRecoverable
方法中浪慌。致命錯(cuò)誤包括:協(xié)議錯(cuò)誤、SSL校驗(yàn)錯(cuò)誤等朴则。
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.
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.
return true;
}
最后权纤,在底層中尋找是否還有其他的Router可以嘗試。
IOException
IOException表示連接已經(jīng)建立乌妒,但讀取內(nèi)容時(shí)失敗了妖碉。我們同樣會(huì)進(jìn)行recover
嘗試,由于代碼邏輯一樣芥被,不再重復(fù)閱讀欧宜。
在finally中,Transmitter會(huì)釋放所有資源拴魄。
followUpRequest
接下來(lái)冗茸,我們看一下RetryAndFollowUpInterceptor中intercept
后半段的實(shí)現(xiàn):
@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) {
...
// 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();
}
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
request = followUp;
priorResponse = response;
}
}
我們拆開(kāi)來(lái)看這段復(fù)雜的邏輯。大體上來(lái)說(shuō)匹中,這段邏輯主要是通過(guò)上次請(qǐng)求的返回夏漱,生成followUp。然后根據(jù)followUp的內(nèi)容顶捷,判斷是不是有效的返回挂绰。如果返回是有效的,就直接return請(qǐng)求的返回服赎。如果返回?zé)o效葵蒂,則request=followUp
,重走while循環(huán)重虑,重新請(qǐng)求践付。
所以這一段的核心邏輯在于followUpRequest
方法。我們來(lái)看下followUpRequest
的實(shí)現(xiàn)缺厉。
/**
* Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
* either add authentication headers, follow redirects or handle a client request timeout. If a
* follow-up is either unnecessary or not applicable, this returns 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) {
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);
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
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
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);
// 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.request().url(), url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
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()) {
// 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) {
return null;
}
return userResponse.request();
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;
}
}
這段代碼非常長(zhǎng)永高,大部分是switch/case的各種返回碼處理隧土。followUpRequest
方法從宏觀上來(lái)講,是輸入response命爬,生成新的requests曹傀。如果response的內(nèi)容不需要重試,則直接返回null饲宛。如果需要重試皆愉,則根據(jù)response的內(nèi)容,生成重試策略落萎,返回重試發(fā)出的request亥啦。
其中,重定向和超時(shí)是最主要的重試情況练链。在處理重定向和超時(shí)時(shí)翔脱,okhttp進(jìn)行了很多判斷,排除了一些不必要重試的情況媒鼓。如届吁,location不存在,或者重定向的url協(xié)議頭不一致等情況绿鸣。
而followUpCount則是為了限制okhttp的重試次數(shù)疚沐。
總結(jié)
RetryAndFollowUpInterceptor在okhttp中承擔(dān)了重試和重定向的邏輯。其中包括了潮模,建立連接亮蛔、讀取內(nèi)容失敗的重試 和 完整讀取請(qǐng)求返回后的重定向。針對(duì)各種返回碼擎厢,okhttp對(duì)無(wú)需重試的一些場(chǎng)景進(jìn)行了裁剪究流,減少了無(wú)效重試的概率。同時(shí)动遭,對(duì)不規(guī)范的重定向返回進(jìn)行的過(guò)濾和校驗(yàn)芬探。
網(wǎng)絡(luò)請(qǐng)求的場(chǎng)景復(fù)雜,在設(shè)計(jì)網(wǎng)絡(luò)框架時(shí)厘惦,對(duì)于各種未知情況的處理偷仿,是一項(xiàng)比較有挑戰(zhàn)的工作。okhttp作為一個(gè)高可用的網(wǎng)絡(luò)框架宵蕉,在RetryAndFollowUpInterceptor這一攔截器中酝静,提供了一個(gè)異常處理的優(yōu)秀范本。
當(dāng)讀者需要自己設(shè)計(jì)網(wǎng)絡(luò)庫(kù)時(shí)国裳,可以參考okhttp中RetryAndFollowUpInterceptor對(duì)于異常處理的做法形入,避免一些難以預(yù)測(cè)和重現(xiàn)的問(wèn)題。
如有問(wèn)題缝左,歡迎指正。