上一篇我们了解了OkHttp的拦截器链是如何形成连式结构,并且如何工作的,接下来开始逐个的分析学习OkHttp内置的几个拦截器。首先第一个拦截器:重试和重定向拦截器RetryAndFollowUpInterceptor
简介
在拦截器链的链式结构中,除了用户自定义的拦截器,位于最上层的拦截器就是该拦截器了
主要功能
连接失败重试(Retry)
在发生 RouteException 或者 IOException 后,会捕获建联或者读取的一些异常,根据一定的策略判断是否是可恢复的,如果可恢复会重新创建 StreamAllocation 开始新的一轮请求
继续发起请求(FollowUp)
主要有这几种类型
- 3xx 重定向
- 401,407 未授权,调用
Authenticator
进行授权后继续发起新的请求 - 408 客户端请求超时,如果 Request 的请求体没有被 UnrepeatableRequestBody 标记,会继续发起新的请求
代码实现
拦截器的工作核心主要是intercpt方法,所以我们直接看intercpt方法
intercpt方法
直接上代码,里边有详细的注释
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
/**初始化StreamAllcation类,这个类十分重要, 用来管理connections、stream、routerSelector的类
* 在RetryAndFollowUpInterceptor中并没有用到初始化StreamAllcation类的核心功能,只是初始化和释放资源
* 而已,它的作用会在ConnectInterceptor拦截器中体现,当前只需要知道它是用来建立连接的就可以了
*/
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
/**计数器,记录重连次数*/
int followUpCount = 0;
Response priorResponse = null;
while (true) {
/**判断当前请求是否已经取消,是:释放链接资源**/
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
/**是否要释放链接,这个标记位后边会频繁使用*/
boolean releaseConnection = true;
try {
/**将创建链接和请求最为参数调用下一组拦截器链,最终的得到想要的response*/
response = realChain.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.
/**路由异常RouteException*/
/**检测路由异常是否能重新连接*/
if (!recover(e.getLastConnectException(), streamAllocation, 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.
/**IO异常*/
/**检测IO异常是否能进行重新链接,不可以的话直接抛出该异常*/
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
/**可以重新连接的话 不必释放资源*/
releaseConnection = false;
/**如果可以重新链接,跳出本次循环,继续网络请求*/
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
/**最后释放所有资源*/
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// Attach the prior response if it exists. Such responses never have a body.
/**判断priorResponse是否为空,priorResponse的赋值在方法最后边*/
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
/**响应码判断,代码执行到这里说明请求已经成功了,但是服务器返回的响应码不一定是200
* 这里还需要对该请求进行检测,主要是通过响应码进行判断
*/
Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
/**释放资源*/
closeQuietly(response.body());
/**重试次数判断,大于20次则释放链接并抛出异常*/
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());
}
/** 如果重试的host等发生改变,如代理,重定向等情况,重新实例化StreamAllocation*/
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} 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;
}
}
该方法里边有个重要的角色初始化工作StreamAllcation
,这个类重要性主要体现在ConnectInterceptor,后面会详细介绍,右上边代码可以看出,重新建立链接的逻辑判断大部分都是在异常处理当中,IOException
和RouteException
,IOException是编译时异常,需要在编译时期就要捕获或者抛出。RouteException是运行时异常,不需要显式的去驳货或者抛出。无论是RouteException
还是IOException
,再处理是都调用了recover方法进行判断
RouteException异常的重连机制
/**
* 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.
*/
/**
* 不在继续连接的情况:
* 1. 应用层配置不在连接,默认为true
* 2. 请求Request出错不能继续使用
* 3. 是否可以恢复的
* 3.1、协议错误(ProtocolException)
3.2、中断异常(InterruptedIOException)
3.3、SSL握手错误(SSLHandshakeException && CertificateException)
3.4、certificate pinning错误(SSLPeerUnverifiedException)
* 4. 没用更多线路可供选择
*/
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// 1. 应用层配置不在连接,默认为true
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
// 2. 请求Request出错不能继续使用
// We can't send the request body again.
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// 3.是否可以恢复的,在isRecoverable()方法中会判断注释3.1~3.4的异常
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;
// 4. 没用更多线路可供选择
// No more routes to attempt.
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
通过 recover 方法检测该 RouteException 是否能重新连接; 可以重新连接,那么就不要释放连接 releaseConnection = false; continue
进入下一次循环,进行网络请求; 不可以重新连接就直接走 finally 代码块释放连接。
IOException异常重连机制
IOException 异常的检测实际上和 RouteException 是一样的,只是传入 recover 方法的第二个参数为 false 而已,表示该异常不是 RouteException ,这里就不分析了。
followUpRequest
/**
* 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, 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, 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;
}
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
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;
}
}
followUpRequest主要是对响应码进行了检测判断,里边各个响应码对应的数值和意义在HttpURLConnection类中都有定义和说明,在这就不赘述了。
重试次数的判断
在 RetryAndFollowUpInterceptor
内部有一个 MAX_FOLLOW_UPS 常量,它表示该请求可以重试多少次,在 OKHTTP 内部中是不能超过 20 次,如果超过 20 次,那么就不会再请求了。
private static final int MAX_FOLLOW_UPS = 20;
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
总结
RetryAndFollowUpInterceptor
拦截器是最上层的内置拦截器,它的核心功能就是初始化StreamAllocation
和判断重试机制的判断。大致业务流程如下:
- 初始化StreamAllocation
- 开启循环,执行下一个调用链(拦截器),等待返回结果(Response)
- 如果发生错误,判断是否继续请求,否:退出(抛出异常)
- 检查响应是否符合要求,是:返回
- 关闭响应结果
- 判断是否达到最大限制数,是:退出
- 检查是否有相同连接,是:释放,重建连接
- 重复以上流程