1.概述
上一章节我们讲解了Application拦截器和Network拦截器,主要从官网和源码的角度,分析了这两个用户可传入的拦截器工作原理、使用方法,到目前为止,我们对拦截器已经并不陌生了。根据拦截器链的顺序,我们今天学习系统拦截器的第一个:RetryAndFollowUpInterceptor(重定向拦截器),顾名思义,它主要的作用,就是做OkHttp网络请求失败重连。
2.RetryAndFollowUpInterceptor中几个重要点
在讲解RetryAndFollowUpInterceptor失败重连原理之前,我们需要掌握如下几点,才能更容易的明白RetryAndFollowUpInterceptor的工作原理。
2.1.OkHttpClient设置失败重连
用户可以通过OkHttpClient设置失败重连,或者可以判断当前OkHttpClient是否支持失败重连,我们通过OkHttpClient.Builder源码看下
public static final class Builder {
...
//失败重连标记
boolean retryOnConnectionFailure;
...
public Builder() {
..
//默认允许失败重连
retryOnConnectionFailure = true;
...
}
Builder(OkHttpClient okHttpClient) {
...
this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
...
}
//设置失败重连 true 允许失败重连,false 不允许失败重连
public Builder retryOnConnectionFailure(boolean retryOnConnectionFailure) {
this.retryOnConnectionFailure = retryOnConnectionFailure;
return this;
}
public OkHttpClient build() {
return new OkHttpClient(this);
}
}
所以可以通过OkHttpClient.Builder的retryOnConnectionFailure(boolean)方法设置是否支持失败重连,默认情况下是支持失败重连的。可以通过如下代码设置:
OkHttpClient client = new OkHttpClient.Builder().retryOnConnectionFailure(false).build();
boolean retryOnConnectionFailure = client.retryOnConnectionFailure();//查看当前是否允许失败重连
2.2.RouteException异常
RouteException(路由异常),继承于RuntimeException,属于运行时异常,在我们RetryAndFollowUpInterceptor中会使用到,我们看一下这个异常在哪里抛出过
是的,在RealConnection和StreamAllocation这两个类中,由于我们今天主要讲的是重定向拦截器,RealConnection我们会在后面详细讲解,StreamAllocation我们今天会大概说一下它的作用,后面也会详细分析。RouteException分别在RealConnection的connect()方法和StreamAllocation的newStream()方法中被抛出,都没有进行捕获,因为该异常最终统一会由RetryAndFollowUpInterceptor捕获处理。connect()方法是与服务器建立连接,newStream()是获取流,这两个方法在后面执行ConnectInterceptor时候会被调用,与服务器建立连接,所以在那里抛出“路由异常”也是情理之中。
2.3.StreamAllocation简单了解
StreamAllocation 按照Allocation Stream可以解释为“分配流”,分配与服务器数据传输的流。是用来建立HTTP请求所需网络设施组件的,比如说HttpCodec(OkHttp中的流)、刚才提到的RealConnection等,它还提供了调用RealConnection的connect()方法与服务器建立连接的方法,提供了断开连接的方法release(),提供了对路由的判断等等,StreamAllocation的实例是在RetryAndFollowUpInterceptor中创建的,会一直被拦截器链传递到ConnectInterceptor才会被真正的使用。我们看一下StreamAllocation·的部分代码,抓住重点,只看与今天所讲有关的,我们大概了解一下StreamAllocation中如下的几个方法即可:
//释放连接
public void release() {
...
closeQuietly(socket);
...
}
//关闭连接
public static void closeQuietly(Socket socket) {
...
socket.close();
...
}
//获取数据交换流
public HttpCodec codec() {
synchronized (connectionPool) {
return codec;
}
}
//连接过程出现异常会调用此方法,根据不同的异常做出不同的处理
public void streamFailed(IOException e) {
...
if (e instanceof StreamResetException) {
...
} else if (connection != null
&& (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
...
}
//关闭连接
closeQuietly(socket);
...
}
//该链接是否还有更多路线
public boolean hasMoreRoutes() {
return route != null
|| (routeSelection != null && routeSelection.hasNext())
|| routeSelector.hasNext();
}
3.RetryAndFollowUpInterceptor的工作原理
3.1.取消请求cancel()
不知道大家是否还记得,我们在将Call的时候,说过RealCall是Call的唯一实现类,实现的方法中,有一个cancel()方法,用来取消当前Call所持有的请求,其实RealCall的cancel()方法,调用的就是RetryAndFollowUpInterceptor的cancel()方法,可以通过代码看一下:
RealCall中的cancel()方法
@Override public void cancel() {
retryAndFollowUpInterceptor.cancel();
}
RetryAndFollowUpInterceptor中的cancel()方法
private volatile StreamAllocation streamAllocation;
private volatile boolean canceled;
public void cancel() {
canceled = true;
StreamAllocation streamAllocation = this.streamAllocation;
if (streamAllocation != null) streamAllocation.cancel();//最终执行了StreamAllocation的cancel方法
}
StreamAllocation的cancel()方法
//最终会执行RealConnection的cancel()方法
public void cancel() {
HttpCodec codecToCancel;
RealConnection connectionToCancel;
...
if (codecToCancel != null) {
codecToCancel.cancel();
} else if (connectionToCancel != null) {
connectionToCancel.cancel();
}
}
通过源码的继续追踪,我们会发现,取消网络请求,最终执行的,还是RealConnection的cancel()方法,我们直接看一下
RealConnection的cancel()方法
public void cancel() {
// Close the raw socket so we don't end up doing synchronous I/O.
closeQuietly(rawSocket);
}
public static void closeQuietly(Socket socket) {
if (socket != null) {
try {
socket.close();//关闭Socket连接
} catch (AssertionError e) {
if (!isAndroidGetsocknameError(e)) throw e;
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
可以看到,最后还是会关闭Socket,到这里,我们已经明白了,Call的取消请求的cancel()方法,实际上调用的就是重定向拦截器RetryAndFollowUpInterceptor的cancel(),最终会调用RealConnection的cancel()方法关闭Socket连接。
3.2.失败重连
RetryAndFollowUpInterceptor的主要功能,就是失败重连,我们接下来看一下它的工作原理,我给大家看的源码,是OkHttp3.11版本的,可能某些地方跟大家看的稍微有些不一样的地方。我们一起来看下RetryAndFollowUpInterceptor的intercept()方法,使用插入代码块的格式,会让我的注释变成灰色,阅读比较费劲,这段代码注释比较多,我就把它直接粘贴上来:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();//获取请求Request
RealInterceptorChain realChain = (RealInterceptorChain) chain;//获取拦截器链
Call call = realChain.call();//获取当前的Call对象
EventListener eventListener = realChain.eventListener();//获取事件监听
//创建了StreamAllocation对象
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
//设置重连次数初始值为0
int followUpCount = 0;
Response priorResponse = null;//创建Response的变量,待会儿会给他赋值
while (true) {//循环进入失败重连代码
if (canceled) {//如果该请求已经被取消,但还是执行到了重定向拦截器,则释streamAllocation并抛出异常结束
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;//定义网络请求结果变量Response
boolean releaseConnection = true;//是否释放连接,默认为true
try {
//调用拦截器链的proceed()方法,将请求和刚才创建的StreamAllocation对象传递下去,进行网络连接,获取Response
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;//如果没有发生异常,则设置releaseConnection 为true
} catch (RouteException e) {//后面网络请求发生RouteException异常
// The attempt to connect via a route failed. The request will not have been sent. //判断能否恢复连接,第三个参数传入的是false,这里可能跟某些OkHttp版本不同
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();//不能恢复连接则抛出异常结束
}
releaseConnection = false;//能恢复重连,则设置releaseConnection 为true,
continue;//此处回到下一次循环,继续重连
} catch (IOException e) {//后面网络请求发生IOException
// An attempt to communicate with a server failed. The request may have been sent.//是否连接中断异常,如果是 requestSendStarted为false,反之requestSendStarted为true
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
//如果不能恢复连接,则抛出异常结束,此处第三个参数是requestSendStarted,可能跟某些OkHttp版本不同。
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;//如果可以恢复连接,则设置releaseConnection为false
continue;//此处回到下一次循环,继续重连
} finally {此处为必然执行代码
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
//如果releaseConnection的值仍为true,则关闭流,释放streamAllocation
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
//走到此处,说明已经顺利完成网络请求,返回了Response,响应码Code不一定为200 OK
// Attach the prior response if it exists. Such responses never have a body.
if (priorResponse != null) {
//说明前面已经完成了一次请求,需要结合前面获取的Response构建新的Response并赋值给变量response
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Request followUp;
try {
//对返回的response进行响应码Code的判断,如果需要失败重连,则返回一个Request对象
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
//如果判断过程中被抛出异常,那就释放掉streamAllocation,并继续抛出异常结束
streamAllocation.release();
throw e;
}
//判断响应码之后,返回的是空的Request,那就没必要重新请求,直接返回Response
if (followUp == null) {则释放streamAllocation
streamAllocation.release();
}
return response;//返回请求结果
}
//关闭Response的数据源
closeQuietly(response.body());
if (++followUpCount > MAX_FOLLOW_UPS) {
//判断重连次数,如果超出默认最大值:MAX_FOLLOW_UPS(20次),则释放掉streamAllocation,并抛出协议异常结束
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
//请求体被UnrepeatableRequestBody标记,则不可重试
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();//释放streamAllocation,抛出异常结束
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
//判断要重连的接口与刚完成的请求的host、port、scheme是否一致
if (!sameConnection(response, followUp.url())) {
//不一致,则释放streamAllocation,重新建立重定向之后的StreamAllocation对象
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赋值为重定向后的Request,priorResponse 赋值为上面得到并且重新构建的response
request = followUp;
priorResponse = response;
//进入下一次循环,重新连接
}
}
RetryAndFollowUpInterceptor的intercept()方法我们已经分析完了,接下来,我们要看一下上面代码中涉及到的问题以及使用到的方法
3.2.1.StreamAllocation的release()方法
我们前面已经说过了StreamAllocation,但是我们还是要提一嘴,因为在失败重连的过程中,我们看到代码多次调用了StreamAllocation的release()方法,这里最终会关闭Socket连接,感兴趣的同学可以去看下源码。
3.2.2.recover()方法判断是否可以恢复连接
上面的intercept()代码中我们还记的这一段
try { ...
response = realChain.proceed(request, streamAllocation, null, null);
} catch (RouteException e) {//判断能否恢复连接,第三个参数传入的是false,这里可能跟某些OkHttp版本不同
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();//不能恢复连接则抛出异常结束
}
...
} catch (IOException e) {//后面网络请求发生IOException
//是否连接中断异常,如果是 requestSendStarted为false,反之requestSendStarted为true
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
//如果不能恢复连接,则抛出异常结束,此处第三个参数是requestSendStarted,可能跟某些OkHttp版本不同。
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;//如果可以恢复连接,则设置releaseConnection为false
continue;//此处回到下一次循环,继续重连
}
我们就来大概看下recover()方法中做了什么事情
//判断与服务器的连接是否为可恢复,返回false,则不可恢复,即不可重连
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
//根据抛出的异常,做出连接、连接路线的一些处理,并且释放连接,关闭连接
streamAllocation.streamFailed(e);
// 如果用户不允许失败重连,则返回false
if (!client.retryOnConnectionFailure()) return false;
// 不是连接中断异常,并且请求体被UnrepeatableRequestBody标记,返回false
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// 根据异常判断是否可以恢复连接
if (!isRecoverable(e, requestSendStarted)) return false;
//如果没有多余线路连接,则不可恢复
if (!streamAllocation.hasMoreRoutes()) return false;
return true;
}
//根据异常判断是否可以恢复连接
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
//协议问题,不可恢复
if (e instanceof ProtocolException) {
return false;
}
// IO连接中断
if (e instanceof InterruptedIOException) {
//如果线路连接超时,可以换个线路重试
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// 服务证书异常
if (e instanceof SSLHandshakeException) {
//如果是证书问题,则不可恢复连接
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//证书校验失败,不可恢复连接
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
//返回true,则可以重新连接
return true;
}
当网络连接发生RouteException和IOException时,会调用recover()方法来判断当前连接是否可以恢复,recover()方法,首先会根据当前发生的异常,对路由做一些处理,并且关闭流,释放StreamAllocation。接下来recover()方法会判断用户是否允许失败重连,如果用户允许,则会进一步分析连接时发生的异常,经过分析如果连接还可以恢复则返回true,否则返回false。
3.2.3.followUpRequest()方法添加身份验证Headers、重定向等操作
当执行到followUpRequest()方法,本次请求已经完成,并且返回了Response,followUpRequest()方法主要是会判断Response的Code,是否需要添加身份验证头、重定向等操作,从而决定是否需要重新向服务器端发起请求。
/**
*根据网络请求的响应码Code,判断当前请求是否还需要进一步添加身份验证或者重定向,如果需要则构建新
* 的Request返回,用于重新发送请求,否则返回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 (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
case HTTP_MULT_CHOICE://响应存在多种选择,需要客户端做出其中一种选择
case HTTP_MOVED_PERM://请求的资源路径永久改变
case HTTP_MOVED_TEMP://请求资源路径临时改变
case HTTP_SEE_OTHER://服务端要求客户端使用GET访问另一个URI
......
case HTTP_CLIENT_TIMEOUT://请求超时
......
case HTTP_UNAVAILABLE://服务器临时不可用
......
default:
return null;
}
}
我们大概分析了下followUpRequest()方法,不必做过深入的研究。followUpRequest()对服务器返回的Response的响应码Code进行了判断,判断是否需要添加身份验证的Headers、是否需要重定向、是否需要修改Url等,如果是,则需要重新向服务器发送请求,所以构建一个新的Request返回,否则就不需要或者不可以重新向服务器发送请求,则返回null。
3.2.4.失败重连的次数不是无限的
private static final int MAX_FOLLOW_UPS = 20; if (++followUpCount > MAX_FOLLOW_UPS) { streamAllocation.release(); throw new ProtocolException("Too many follow-up requests: " + followUpCount); }
当失败重连的次数,超出了默认的最大次数:MAX_FOLLOW_UPS = 20次时,会释放StreamAllocation关闭Socket连接,同时抛出ProtocolException并且告诉客户,重连次数太多。
3.2.5.sameConnection()方法判断连接是否一致
在经过followUpRequest()判断之后,如果需要重连,则还会进行sameConnection()方法判断
/**
* 判断Response中的Url和followUp的Host、Port、scheme是否一致
* engine.
*/
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());
}
sameConnection()方法,主要是比较刚刚完成的请求的Url和经过followUpRequest()构建的Request的Url,判断二者之间的HOST、POST、scheme协议是否一致。如果不一致则以followUpRequest()返回的Request为准,释放掉旧的StreamAllocation,创建新的StreamAllocation重新向服务器发送请求。
if (!sameConnection(response, followUp.url())) { streamAllocation.release(); streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; }
4.总结
我们来总结一下RetryAndFollowUpInterceptor的作用及失败重连的原理
-
用户调用RealCall的cancel()方法取消请求,RealCall则会调用RetryAndFollowUpInterceptor了cancel()方法,最终会关闭Socket连接;
-
RetryAndFollowUpInterceptor重定向拦截,其主要作用就是连接失败之后做重新连接,用户可以通过调用OkHttpClient.Builder的
retryOnConnectionFailure(boolean)方法设置是否允许失败重连;
-
StreamAllocation由RetryAndFollowUpInterceptor创建,通过拦截器链Chain.proceed()方法,传递给后面的拦截器;
-
当拦截器链向服务器端发送请求,抛出RouteException或者发生IOException时,RetryAndFollowUpInterceptor会根据用户的设置以及抛出的异常进行分析,判断当前请求是否可以被恢复,如果不能则释放StreamAllocation关闭Socket连接,并且抛出异常结束。如果当前请求可以被恢复,则回到while循环开始部位,进行重连;
-
当向服务器发送网络请求成功,获取到了Response对象,RetryAndFollowUpInterceptor会分析当前Response的响应码,判断是否需要身份验证、重定向、修改URL等操作,如果需要则构建新的Request,进行Url校验之后,重新向服务器端发送请求。如果不需要重新发送请求则不必构建新的Request,RetryAndFollowUpInterceptor任务结束,返回刚获取到的Response。
-
失败重连不是无限的,最多允许重连20次。
以上就是今天要跟大家分享的RetryAndFollowUpInterceptor,欢迎各位指正批评,下次我们继续分析剩下的拦截器。