引言
上一篇博客笼统的讲了各个拦截器:
- 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就分析完毕了。