RetryAndFollowUpInterceptor 详解
该拦截器主要是负责失败重连和重定向,我们先了解一下 HTTP
协议中的重定向。
HTTP 协议中的重定向
HTTP
协议提供了一种重定向的功能,它通过由服务器返回特定格式的响应从而触发客户端的重定向。其对应的响应码格式为 3XX
,并且会在响应头的 Location
字段中放入新的 URL
,这样我们客户端就可以根据该 Location
字段所指定的 URL
重新请求从而得到需要的数据。
源码分析
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 创建 StreamAllocation 类,三个参数分别对应:全局的连接池,连接线路Address, 堆栈对象
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
// 释放资源并抛出异常,结束流程
streamAllocation.release();
throw new IOException("Canceled");
}
Response response = null;
boolean releaseConnection = true;
try {
// 执行下一个拦截器,即BridgeInterceptor
// 这里有个很重要的信息,即会将初始化好的连接对象传递给下一个拦截器,
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// 如果有异常,判断是否要恢复
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
// 可以恢复请求,继续循环
continue;
} catch (IOException e) {
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
// 检查是否符合要求
Request followUp = followUpRequest(response);
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()), callStackTrace);
} 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;
}
}
总共分为如下步骤:
Request 部分:
- 根据
url
来构建一个Address
对象,该对象描述了建立连接的所有配置信息,初始化一个Socket
连接对象,基于Okio
。 - 将
Address
对象、 连接池和堆栈对象传入并构建StreamAllocation
对象,这里只是初始化了一个连接对象,还没真正地去建立连接。 - 开启一个死循环,实现不断地重定向。
- 如果请求被取消了,则释放资源并抛出异常,循环结束。
- 将初始化好的连接对象传递给下一个拦截器(BridgeInterceptor)并执行。
Response 部分:
- 如果请求发生异常,判断是否可以恢复请求,不能恢复则抛异常,结果循环。
- 如果前一个响应不为空,则结合前一个响应和当前响应。
- 根据响应码进行重定向判断,如果重定向后的请求为 null ,即不需要重定向,直接返回响应结果。
- 重定向次数 +1 ,并判断是否超过最大次数限制,是则释放资源并抛出异常。
- 把当前重定向的请求和响应保存起来,并根据重定向的请求继续循环。
让我们来看一下重定向的方法 followUpRequest
到底做了什么操作:
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();
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: // 308
case HTTP_TEMP_REDIRECT: //307
// 只对 GET 和 HEAD 请求做重定向处理
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?
// 如果客户端关闭了重定向,则直接返回 null
if (!client.followRedirects()) return null;
// 获取重定向的目标
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
if (url == null) return null;
if (!sameScheme && !client.followSslRedirects()) return null;
Request.Builder requestBuilder = userResponse.request().newBuilder();
// 处理重定向使用的 method
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");
}
// 重新构建新的 request
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT: // 408 ,需要重新发送一次相同的请求
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
return userResponse.request();
default:
return null;
}
}
主要是根据响应码和响应头,查看是否需要重定向,并重新设置请求。
BridgeInterceptor 详解
BridgeInterceptor
的名字取的非常形象,它就像一座桥梁,连接了用户与服务器。在用户向服务器发送请求时,它会把用户所构建的请求转换为向服务器请求的真正的 Request
,而在服务器返回了响应后,它又会将服务器所返回的响应转换为用户所能够使用的 Response
。
@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");
}
boolean transparentGzip = false;
// 若未设置Accept-Encoding,自动设置gzip
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
// 获取 cookieJar 返回的 cookie 列表
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
// 根据 Okhpptclitent时候配置的 cookieJar 添加 Cookie 头信息
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
// 上述的处理,都是请求头的处理
// 把处理了请求头的请求传给下一个拦截器做处理
Response networkResponse = chain.proceed(requestBuilder.build());
// 保存响应的 cookie 信息
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
// 下面是响应头信息处理
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
// 如果服务器返回的响应content是以gzip压缩过的,则会交给Okio先进行解压缩,
// 移除响应中的header Content-Encoding和Content-Length,构造新的响应返回。
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);
// 处理完成后,重新生成一个 response
responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}
// 返回新的 response
return responseBuilder.build();
}
总共分为如下步骤:
Request 部分:
- 给请求头添加一些
head
信息,包括 Content-Type、 Content-Length、 Transfer-Encoding、 Host、 Connection、 Accept-Encoding、 User-Agent 等。 - 若调用者未设置 Accept-Encoding ,则它会默认设置 gzip ,进行 gzip 压缩。
- 如果有 Cookie ,则添加 Cookie 头信息。
- 将包装过的请求对象传递给下一个拦截器(CacheInterceptor)并执行。
Response 部分:
- 将返回的响应里面的 Cookie 保存起来。
- 如果服务器返回的响应 content 是以 gzip 压缩过的,则会交给 Okio 先进行解压缩并移除响应中的 header Content-Encoding 和 Content-Length ,构造新的响应返回。
- 否则,直接返回 response 。
其中 CookieJar 来自 OkHttpClient , 它是 OKHttp 的 Cookie 管理类,负责 Cookie 的存取。由于 CookieJar 是一个接口类,我们在构建 OkHttpClient 时,可以通过 builder 输入 CookieJar 的实现类。下面我们看一下 CookieJar 的源码:
public interface CookieJar {
// 默认没有 Cookie 的实现类
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
}
@Override public List<Cookie> loadForRequest(HttpUrl url) {
return Collections.emptyList();
}
};
void saveFromResponse(HttpUrl url, List<Cookie> cookies);
List<Cookie> loadForRequest(HttpUrl url);
}
由源码可知,OkHttp
默认不处理 Cookie
,所以如果想对 Cookie
进行管理,需要自己实现该实现,在对应的接口方法里面对 Cookie
进行存取操作。需要注意的是,loadForRequest()
方法的返回值不能为 null
。