okhttp分享三:RetryAndFollowUpInterceptor与BridgeInterceptor
之前两篇文章分析了okhttp的基本使用、线程及任务分发,我们知道,okhttp的发送请求及接收响应都是通过其interceor链实现的,现在我们就开始从源码分析每个拦截器的作用,按顺序首先我们看下最前面的两个拦截器RetryAndFollowUpInterceptor与BridgeInterceptor。
一、RetryAndFollowUpInterceptor
从名字可以看出,这个拦截器主要处理重试逻辑,重试发生的场景有两种,一是返回码表明需要重试(如302),二是请求发送或接收过程中产生非致命的异常时。
1.1、http返回码
我们知道http返回码中1xx表示信息提示,2xx表示成功,3xx表示重定向(实际上304也可以理解为一种重定向),4xx表示客户端请求错误,5xx表示服务器错误。这边我们着重看一下与重试相关错误码。
- 300 Multiple Choices
被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。
- 301 redirect 永久性转移(Permanently Moved)
永久是指原来访问的资源已经永久删除,客户端应该根据新的URI访问重定向。
- 302 redirect 暂时性转移(Temporarily Moved )
临时是指访问的资源可能暂时先用location的URI访问,但旧资源还在的,下次你再来访问的时候可能就不用重定向了。
- 303 See Other
303 的出现其实是为了规范301、302的,一开始301、302的是不允许在重定向的时候改变请求方式的(get改为post),但是各大浏览器在使用过程中并没有遵循这一规定。因此http又添加了303,303实际上就是302,只不过303可以改变请求方式。但是实际上并没有很多浏览器按照这一规定执行,于是http只能将错就错推出了307、308,307对应302,308对应301。区别是301、302由原来的不可以改变请求方式改为可以,307、308则表示重定向过程中不可以改变请求方式。
- 307 Temporary Redirect(临时重定向)
与302对应,但是不可以在重定向过程中修改请求方式
- 308 Permanent Redirect (永久重定向)
与301对应,但是不可以在重定向过程中修改请求方式
- 401 Unauthorized
当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。
- 407 Proxy Authentication Required
与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个 Proxy-Authenticate 用以进行身份询问。客户端可以返回一个 Proxy-Authorization 信息头用以验证。
- 408 Request Timeout
请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。
408请求超时错误是一个 HTTP状态代码,这意味着你的请求发送到该网站服务器(即请求加载网页)花的时间比该网站的服务器准备等待的时间要长。这个时候客户端与服务器的链接已经建立,但没发请求。
HTTP请求的步骤:
1、建立链接、套接字
2、通过该套接字写 HTTP 数据流。
3、从您的Web服务器接受响应的 HTTP 数据流。
408发生的时机:
408发生预示着1-2之间的处理超时。通常套接字开通和通过该套接字书写入 HTTP 数据流之间只有很短的时间间隔(毫秒)。
- 503 Service Unavailable
由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间。如果没有给出这个 Retry-After 信息,那么客户端应当以处理500响应的方式处理它。
1.2、RetryAndFollowUpInterceptor源码分析
从之前的文章我们知道,interceptor核心方法是其intercept方法,我们就直接看这个方法
/**
* How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
* curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
//单个请求最大恢复次数:
private static final int MAX_FOLLOW_UPS = 20;
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
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 = 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.
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
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, 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.
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Request followUp;
try {
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
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());
}
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;
}
}
简单整理下流程
- 1、根据url创建一个Address对象,初始化一个Socket连接对象。
- 2、用前面创建的address作为参数去实例化StreamAllocation(后面会着重讲这个类,大家可以暂时将它理解为用于维护网络连接、及请求流)
- 3、开启一个while(true)循环,执行如下逻辑逻辑
- 3.1 如果取消,释放资源并抛出异常,结束流程
- 3.2 执行下一个拦截器,一般是BridgeInterceptor
- 3.3 如果发生异常,走到catch里面,判断是否继续请求,不继续请求则退出
- 3.4 如果priorResponse不为空,则说明前面已经获取到了响应,这里会结合当前获取的Response和先前的Response
- 3.5 调用followUpRequest查看响应是否需要重定向,如果不需要重定向则返回当前请求
- 3.6 检查是否有相同的链接,是:释放,重建创建
- 3.7 重新设置request,并把当前的Response保存到priorResponse,继续while循环
我们看到3.5,followUpRequest方法,这个方法就是按照http协议根据返回的code来做相应的处理逻辑。我们来看下代码
/**
* 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://407
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://401
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT://307
case HTTP_TEMP_REDIRECT://308
// "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://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
// 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是根据返回的httpcode来判断是否需要重试,我们上文说到,RetryAndFollowUpInterceptor还会处理请求发送过程中出现的异常,判断是否可以重试。这块逻辑在intercept()方法中存在于catch里面,其核心方法为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, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again.
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt.
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
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.
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.
return true;
}
看上面代码可以这样理解:判断是否可以恢复如果下面几种条件符合,则返回true,代表可以恢复,如果返回false,代表不可恢复。具体逻辑不详细说了,简单说下不可恢复的情况
- 用户配置不可重试
- 请求出错不能重试
- 特定的Exception不可重试
- 协议错误(ProtocolException)
- 中断异常(InterruptedIOException)
- SSL握手异常(SSLHandshakeException && CertificateException)
- certificate pinning异常(SSLPeerUnverifiedException)
- 没用更多route可选择,不可重复
总结来说,RetryAndFollowUpInterceptor的执行逻辑为通过一个while循环获取response,若成功则通过followUpRequest方法,根据http code判断是否需要重定向,若需要则将followUpRequest方法返回的request通过下一个while循环发送出去,若不需要则直接返回response,推出循环;若在发送请求流程中出现exception则通过recover方法判断是否可以重试,若可以则重新发送,若不可则作失败处理。
二、BridgeInterceptor
这个Interceptor比较简单但是确非常重要,http协议的一些必须的请求、响应头都是通过这个拦截器添加、解析的,如果缺失这个过程,服务器与客户端都无法正确读取对方发送的内容。照例看代码
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
List cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
代码比较简单,简单说下逻辑
- 添加必须的请求头:Content-Type、Content-Length、Transfer-Encoding、Host、Connection、Accept-Encoding、User-Agent。
- 根据用户设定的Cookie策略向请求头中加入cookie
- 构建好request,调用proceed进入后续拦截器链,返回response
- 根据响应跟新cookie
- 根据返回响应头的Content-Encoding字段,判断报文是否为gzip压缩过的,若是则会先进行解压缩,移除响应中的header Content-Encoding和Content-Length,构造新的响应返回。否则直接返回response。