OKHttp重試機(jī)制剖析
OKHttp擁有網(wǎng)絡(luò)連接失敗時(shí)的重試功能:
OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and for services hosted in redundant data centers. OkHttp initiates new connections with modern TLS features (SNI, ALPN), and falls back to TLS 1.0 if the handshake fails.
要了解OKHttp的重試機(jī)制,我們最關(guān)心的就是RetryAndFollowUpInterceptor
痛悯, 在遭遇網(wǎng)絡(luò)異常時(shí)嵌巷,OKHttp的網(wǎng)絡(luò)異常相關(guān)的重試都在RetryAndFollowUpInterceptor
完成。具體我們先從RetryAndFollowUpInterceptor
的#intercept(Chain chian)
方法開(kāi)始入手丹皱,下面的代碼片段已經(jīng)去掉了非核心邏輯:
//StreamAllocation init...
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//socket連接階段妒穴,如果發(fā)生連接失敗,會(huì)統(tǒng)一封裝成該異常并拋出
`RouteException`:通過(guò)路由的嘗試失敗了摊崭,請(qǐng)求將不會(huì)被發(fā)送讼油,此時(shí)會(huì)嘗試通過(guò)調(diào)用`#recover`來(lái)恢復(fù);
// 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) {
//socket連接成功后呢簸,發(fā)生請(qǐng)求階段時(shí)拋出的各類(lèi)網(wǎng)絡(luò)異常
// 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 {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
接下來(lái)看核心的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.
*/
private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// The application layer has forbidden retries. 應(yīng)用層禁止重試則不再重試
if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again. 如果請(qǐng)求已經(jīng)發(fā)出矮台,并且請(qǐng)求的body不支持重試則不再重試
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// This exception is fatal. //致命錯(cuò)誤
if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt. 沒(méi)有更多route發(fā)起重試
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
在該方法中乏屯,首先是通過(guò)調(diào)用streamAllocation.streamFailed(e)
來(lái)記錄該次異常,進(jìn)而在RouteDatabase
中記錄錯(cuò)誤的route以降低優(yōu)先級(jí)瘦赫,避免下次相同address的請(qǐng)求依然使用這個(gè)失敗過(guò)的route辰晕。如果沒(méi)有更多可用的連接線路則不能重試連接
public final class RouteDatabase {
private final Set<Route> failedRoutes = new LinkedHashSet<>();
/** Records a failure connecting to {@code failedRoute}. */
public synchronized void failed(Route failedRoute) {
failedRoutes.add(failedRoute);
}
/** Records success connecting to {@code route}. */
public synchronized void connected(Route route) {
failedRoutes.remove(route);
}
/** Returns true if {@code route} has failed recently and should be avoided. */
public synchronized boolean shouldPostpone(Route route) {
return failedRoutes.contains(route);
}
}
接著我們重點(diǎn)再關(guān)注isRecoverable
方法:
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// If there was a protocol problem, don't recover. 協(xié)議錯(cuò)誤不再重試
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;
}
}
//使用 HostnameVerifier 來(lái)驗(yàn)證 host 是否合法,如果不合法會(huì)拋出 SSLPeerUnverifiedException
// 握手HandShake#getSeesion 拋出的異常耸彪,屬于握手過(guò)程中的一環(huán)
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;
}
常見(jiàn)網(wǎng)絡(luò)異常分析:
UnknowHostException
產(chǎn)生原因:
- 網(wǎng)絡(luò)中斷
- DNS 服務(wù)器故障
- 域名解析劫持
解決辦法:
- HttpDNS
- 合理的兜底策略
![Uploading image_079055.png . . .]
InterruptedIOException
產(chǎn)生原因:
- 請(qǐng)求讀寫(xiě)階段伞芹,請(qǐng)求線程被中斷
解決辦法:
- 檢查是否符合業(yè)務(wù)邏輯
SocketTimeoutException
產(chǎn)生原因:
- 帶寬低、延遲高
- 路徑擁堵蝉娜、服務(wù)端負(fù)載吃緊
- 路由節(jié)點(diǎn)臨時(shí)異常
解決辦法:
- 合理設(shè)置重試
- 切換ip重試
要特別注意: 請(qǐng)求時(shí)因?yàn)樽x寫(xiě)超時(shí)等原因產(chǎn)生的SocketTimeoutException唱较,OkHttp內(nèi)部是不會(huì)重試的
因此如果app層特別關(guān)心該異常,則應(yīng)該自定義intercetors召川,對(duì)該異常進(jìn)行特殊處理南缓。
SSLHandshakeException
產(chǎn)生原因:
- Tls協(xié)議協(xié)商失敗/握手格式不兼容
- 辦法服務(wù)器證書(shū)的CA未知
- 服務(wù)器證書(shū)不是由CA簽名的,而是自簽名
- 服務(wù)器配置缺少中間CA(不完整的證書(shū)鏈)
- 服務(wù)器主機(jī)名不匹配(SNI)荧呐;
- 遭遇了中間人攻擊汉形。
解決辦法:
- 指定SNI
- 證書(shū)鎖定
- 降級(jí)Http。倍阐。概疆。
- 聯(lián)系SA
SSLPeerUnverifiedException
產(chǎn)生原因:
- 證書(shū)域名校驗(yàn)錯(cuò)誤
解決辦法:
- 指定SNI
- 證書(shū)鎖定
- 降級(jí)Http。峰搪。岔冀。
- 聯(lián)系SA