该系列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的。