OkHttp源码解析(二)重定向
RetryAndFollowUpInterceptor-> intercept
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;、
// 获取Transmitter
Transmitter transmitter = realChain.transmitter();
int followUpCount = 0;
Response priorResponse = null;
while (true) {
// 准备连接
transmitter.prepareToConnect(request);
// 处理取消事件
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
// 从下层获取Response
response = realChain.proceed(request, transmitter, null);
// 标记获取成功
success = true;
} catch (RouteException e) {
// 判断是否满足重定向条件,满足则重试,不满足就抛异常
if (!recover(e.getLastConnectException(), transmitter, false, request)) {
throw e.getFirstConnectException();
}
continue;
} catch (IOException e) {
// 与服务器连接失败,检查是否满足重定向条件,满足则重试,不满足就抛异常
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, transmitter, requestSendStarted, request)) throw e;
continue;
} finally {
// 检测到其他未知异常,则释放连接和资源
if (!success) {
transmitter.exchangeDoneDueToException();
}
}
// 绑定上一个Response,指定body为空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Exchange exchange = Internal.instance.exchange(response);
Route route = exchange != null ? exchange.connection().route() : null;
// 根据响应码处理请求,返回Request不为空时则进行重定向处理
Request followUp = followUpRequest(response, route);
if (followUp == null) {
if (exchange != null && exchange.isDuplex()) {
transmitter.timeoutEarlyExit();
}
return response;
}
RequestBody followUpBody = followUp.body();
if (followUpBody != null && followUpBody.isOneShot()) {
return response;
}
closeQuietly(response.body());
if (transmitter.hasExchange()) {
exchange.detachWithViolence();
}
// 限制重定向次数最多为20,超过则抛出异常
if (++followUpCount > 20) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
request = followUp;
priorResponse = response;
}
}
首先看到的是Transmitter的处理预备连接处理,这里其实会对url进行判断,如果存在相同主机号,端口号,协议的url,那么就可以复用连接了
主要的逻辑就是从下层拿到一个Response,然后判断是否满足重定向的条件,如果满足就重试,如果不满足就直接抛出异常
然后会根据上一步拿到的Response绑定上一个Response,然后转调followUpRequest方法根据相应拿到一个新的Request
而当重试的次数超过最大次数,也就是20,就会抛出异常
RetryAndFollowUpInterceptor-> followUp
private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
// 407
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);
// 401
case HTTP_UNAUTHORIZED:
// 身份认证
return client.authenticator().authenticate(route, userResponse);
// 308
case HTTP_PERM_REDIRECT:
// 307
case HTTP_TEMP_REDIRECT:
// 如果是307、308 这两种状态码
// 不对GET、HEAD 以外的请求重定向
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
// 300
case HTTP_MULT_CHOICE:
// 301
case HTTP_MOVED_PERM:
// 302
case HTTP_MOVED_TEMP:
// 303
case HTTP_SEE_OTHER:
// 客户端关闭重定向
if (!client.followRedirects()) return null;
// 从响应头中获取Location字段
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;
// 重建一个新的Request
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.request().url(), url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408
case HTTP_CLIENT_TIMEOUT:
// 需要发送一次相同的请求
// ......
// 503
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;
}
}
主要的逻辑就是根据不同的响应码做不同的处理
包含了407的代理认证和401的身份认证
而307、308两种响应码,只对get、head两种请求方式做重定向
然后针对30x的响应码其实处理就是从Response中拿到Location字段,然后构建一个新的请求重新发送出去
总结
其实感觉Ok的实现就是通过拦截器机制由上往下,由下往上的自己实现Http协议,看到Ok就顺便回顾Http协议中的知识
重定向这个拦截器主要是针对Http提供的重定向这种功能的具体实现,通过在服务器的响应中设置3xx的响应码来触发客户端的重定向操作,并且在相应头中设置了一个Location填写所指向的url,然后重新请求得到所需的数据
针对不同的响应码处理为:
- 301 Moved Permanently:表示请求的资源已经永久转移成了Location字段中的URL,客户端不应该再使用旧的URL,而是应该使用Location字段中的URL。搜索引擎在发现该状态码后也会触发更新操作,在索引库中修改其对应的数据
- 302 Found:表示请求的资源暂时转移成了Location字段中的URL,客户端在这次请求中应该去请求Location字段中的URL,不过在以后的请求中可能仍然会使用现在的URL。搜索引擎发现该状态码后不会更新数据
- 303 See Other:含义同302,但是该状态码会强制将非GET请求的方法变成GET方法(请求主体会丢失)
- 304 Not Modified:这个状态码在上一篇HTTP缓存机制中提到过,这个状态码实际是用于HTTP缓存使用的,不过它其实同样也是重定向的含义:服务器指示客户端将当前的请求重定向到缓存中的资源数据中。因为是指示客户端使用缓存,所以这个状态码是不会返回Location字段的
- 307 Temporary Redirect:含义同302,不过区别在于这个状态码不会改变请求的方法和实体数据
- 308 Permanent Redirect:含义同301,不过区别在于这个状态码不会改变请求的方法和实体数据
在上面的状态码介绍中提到了307和302, 308和301的区别,具体说来,301和302在大多数情况下不会改变请求的方法和实体,不过在某些旧的用户代理上,可能会发生意想不到的改变,除了GET和HEAD方法以外的请求方法在使用301和302进行重定向时可能会被改变成GET方法,并且请求的实体也会被清空。因此,如果需要在使用非GET方法的请求中进行重定向的话,那么307和308是要优于301和302的。