「时光不负,创作不停,本文正在参加2021年终总结征文大赛」
导语
学过Android开发的同学都知道,我们使用网络请求获取服务器的数据时,通常使用的是封装好的Retrofit框架,这个框架很好的帮助我们对网络的发起,以及返回的数据进行操作,我们使用起来十分方便,对于Retrofit来说,我们仅仅看到了它的表面,如何正确使用等,其内部还是要借助OkHtttp来请求网络的,Retrofit只是对OkHttp进行了再次的封装,而且Retrofit不具备网络请求功能,只是在OkHtttp的外表又套了一层,对返回的数据支持RxJava转换和Gson解析。真正起到网络请求的还是OkHttp,所以要了解里面的实质,我们还是着手从OkHttp出发,来探索它对网络的认知和对数据的传递。
OkHttp使用方式
要想了解其原理,首先得学会使用它,OkHttp的使用也非常简单。
从请求方式来讲,分为 get 和 post
get请求的同步和异步
// 构建一个OkHttpClient对象
OkHttpClient client = new OkHttpClient.Builder()
.build();
// 创建一个Request请求对象
Request request = new Request.Builder()
.get()
.url("https://www.baidu.com/")
.build();
// 把request对象 通过 newCall 转换成call
Call call = client.newCall(request);
try {
// 通过call来发起网络请求
// 同步请求
Response response = call.execute();
//返回响应体
ResponseBody body = response.body();
System.out.println(body.string());
} catch (IOException e) {
e.printStackTrace();
}
// 通过call来发起网络请求
// 异步请求
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 返回响应体
ResponseBody body = response.body();
System.out.println(body.string());
}
});
上面的步骤也非常清楚:
- 构建一个OkHttpClient对象,通过Builder来构建我们的client,我们自由配置client。(添加拦截器,超时时间等)
- 创建一个Request请求对象,Request对象通过构建者模式创建,我们可以自由地配置Request对象。
- 把request对象 通过 newCall 转换成call。
- 通过call来发起网络请求,同步请求使用execute,异步请求使用enqueue。
- 通过response.body()来获取响应体。
post请求的同步和异步
OkHttpClient client = new OkHttpClient.Builder()
.build();
// 表单格式构建 RequestBody
RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
Request request = new Request.Builder()
.post(requestBody)
.url("https://www.baidu.com/")
.build();
Call call = client.newCall(request);
try {
Response response = call.execute();
ResponseBody responseBody = response.body();
System.out.println(responseBody.string());
} catch (IOException e) {
e.printStackTrace();
}
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
ResponseBody responseBody = response.body();
System.out.println(responseBody.string());
}
});
post请求与get请求的唯一区别就是:post请求通过RequestBody来构建一个请求体,Body里面带上我们要请求的参数;而get请求的参数是拼接在url后面。
了解了OkHttp的使用,我们梳理下整个OkHttp的调用过程。
不管我们通过execute还是enqueue,都会经过Dispatcher(分发器)和 Interceptors(拦截器)来获得Response。
分发器到底做了什么事情,接下来我们深入地了解分发器内部的原理。
分发器—Dispatcher
我们从上面的代码中知道,真正请求网络的是一个call对象,call是一个接口,通过RealCall实现,我们通过newCall(request)构建的这个call,通过execute或enqueue就能获得response,为何,进入execute看下:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
可以看到,调用了分发器的executed,然后通过getResponseWithInterceptorChain()获得响应。
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
将call加入running队列。
对于同步请求,分发器并没有做什么事情。我们分析下异步请求的分发器干了什么事情。
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
调用了分发器的enqueue,注意这里:分发器将Callback包装成AsyncCall来传给了enqueue。AsyncCall继承自NamedRunnable,而NamedRunnable又实现了Runnable,所以AsyncCall本质上就是一个Runnable。AsyncCall交给了分发器:
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
这里通过两个条件判断:
-
正在执行的running队列里面的任务数小于最大请求数(64)以及同一Host的请求数小于最大同一Host请求数(5)时,将call加入running队列,然后交给线程池处理。否则,将AsyncCall加入ready队列。
分别解释下这两个条件:
- 第一个条件:充分考虑到客户端的压力,如果没有这个64的限制,客户端不停的进行网络请求,这样会让客户端的压力特别大。
- 第二个条件:充分考虑到服务器的压力,如果同一个Host的服务器,没有这个限制,对于一个客户端就建立64次连接,如果有多个客户端同时建立连接的话会撑爆服务器。
-
如果把任务放在ready队列后,这个队列里的任务怎么执行,什么时候执行?
前面说到,正在执行的任务会交给线程池处理,当线程池处理完之后,会finish掉这个任务。由于AsyncCall本质上就是一个Runnable,所以会调用run方法,而run方法里面又调用了execute方法,execute方法是一个抽象方法,所以在分发器里实现如下:
public abstract class NamedRunnable implements Runnable {
protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}
@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
@Override protected void execute() {
boolean signalledCallback = false;
try {
// 执行请求(拦截器)
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
我们注意一点:无论请求成功还是失败,都会执行finally里的代码,(看这个try....catch代码块),在finally里会调用分发器的finished方法:
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
到这里,我们分析下这个方法:
记录这两个参数:calls就是runningAsyncCalls,promoteCalls是true
首先执行calls.remove(call),说明这个call完成了,从队列里面移除,然后调用promoteCalls()方法。
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
来看promoteCalls,这个方法首先做两个判断,如果running队列任务数大于maxRequests,直接返回;如果ready队列为空,直接返回。
接下来,开始从ready执行队列里遍历任务,通过next()取到下一个任务,再对这个任务进行判断,如果同一Host请求数已经有5个了,那就不会从ready队列取出任务到running队列,否则,从ready队列取出任务放入running队列,交给线程池,同时移除掉ready队列的任务。
用流程图看下分发器的流程:
讲到这里,我们说一下OkHttp里用到的线程池。
线程池
线程池的工作原理
当一个任务通过execute(Runnable)方法添加到线程池时:
- 当线程数小于corepoolsize时,创建新的线程来处理被添加的任务
- 当线程数大于等于corepoolsize时,如果有空闲线程,则使用空闲线程开处理被添加的任务
- 当线程数大于等于corepoolsize时,如果没有空闲线程,则任务放入等待队列,添加成功则等待空闲线程,添加失败:
- 当线程数小于maxpoolsize时,创建新的线程来执行任务,当线程数大于maxpoolsize时,拒绝任务.
OkHttp如何创建线程池
前面我们说到,running队列的任务直接交给线程池处理,那我们看下线程池是如何处理这么多任务的。
OkHttp通过executorService()构建一个线程池
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
带着两个问题分析:
- 注意到这个线程池和其他线程池有什么区别?
- OkHttp为什么会构建这样一个线程池?
首先看第一个问题:
我们知道,构建线程池的几大参数是:核心线程数、最大线程数、线程存活时间、存活时间单位、阻塞队列、线程工厂。
对应到这个线程池中:核心线程数为0,最大线程数为0x7fffffff(2147483647),存活时间是60s,阻塞队列使用了SynchronousQueue。
如果了解线程池的工作原理的话,这个线程池没有核心线程数,来一个任务加入队列,那么看看这个队列:SynchronousQueue
这是一个没有容量的双端队列,说明一个问题,这个队列里存放不了任务,而最大线程数是如此的庞大,那么,来一个任务就会立马新建线程来执行,但是并不是每一个任务都会新建线程,线程有60s的存活时间,如果这个线程执行完任务后,下一个任务来时,就会复用线程,所以这样设计,就是为了提高执行效率,这是一个高并发,最大吞吐量的线程池。
第二个问题:
OkHttp这么设计,就是为了能够最大限度地执行任务请求,任务无需等待,立马执行。
拦截器—Interceptors
上面分析到,分发器会将任务交给线程池处理,然后调用getResponseWithInterceptorChain()获取响应结果,这个方法字面意思就是:通过拦截器责任链获取响应,看一下这个方法:
Response result = getResponseWithInterceptorChain();
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors()); // 自定义拦截器加入到集合
interceptors.add(retryAndFollowUpInterceptor); // 重试重定向拦截器
interceptors.add(new BridgeInterceptor(client.cookieJar())); // 桥接拦截器
interceptors.add(new CacheInterceptor(client.internalCache())); // 缓存拦截器
interceptors.add(new ConnectInterceptor(client)); // 连接拦截器
if (!forWebSocket) { // 如果不是webSocket,将自定义的网络拦截器添加进去
interceptors.addAll(client.networkInterceptors()); // 网络拦截器
}
interceptors.add(new CallServerInterceptor(forWebSocket)); // 服务器通讯拦截器
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
这个方法里,创建一个存放Interceptor的List集合,首先通过addAll方式将拦截器添加进list集合中,这里的拦截器是我们自定义的拦截器。以及下面的client.networkInterceptors()。
/**
* 自定义拦截器
*/
public class MyInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
return realChain.proceed(request);
}
}
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new MyInterceptor())
.addInterceptor(new MyInterceptor())
.addInterceptor(new MyInterceptor())
.addNetworkInterceptor(new MyInterceptor())
.build();
如果没有自定义拦截器,那么这个集合就是null。
接下来添加retryAndFollowUpInterceptor(重试重定向拦截器)、 BridgeInterceptor(桥接拦截器)、CacheInterceptor(缓存拦截器)、ConnectInterceptor(连接拦截器)、CallServerInterceptor(服务器通讯拦截器),一共有五大拦截器,这些拦截器是怎么一个一个执行起来,帮助我们完成整个请求过程,这里就涉及到一个设计模式——责任链模式
责任链模式
责任链模式是一种对象行为型模式,为请求创建了一个接收者对象的链,在处理请求的时候执行过滤(各司其职)。
责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。
文字描述理解起来确定有点困难,我们通过代码来看一下什么是责任链。
首先定义一个拦截器接口,五大拦截器分别实现这个接口,定义一个Chain链条实体。
第一个拦截器:RetryAndFollowUpInterceptor:
public class RetryAndFollowUpInterceptor implements Interceptor {
@Override
public String intercept(Chain chain) {
System.out.println("开始执行重试重定向拦截器");
String result = chain.proceed(chain.request + "===>经过重试重定向拦截器");
System.out.println("结束执行重试重定向拦截器");
return result + "===>经过重试重定向拦截器";
}
}
最后一个拦截器:CallServerInterceptor:
public class CallServerInterceptor implements Interceptor {
@Override
public String intercept(Chain chain) {
System.out.println("开始执行服务器通讯拦截器");
System.out.println("===发起请求===");
System.out.println("结束执行服务器通讯拦截器");
return chain.request + "===>经过请求服务器拦截器\nOkHttp响应===>经过请求服务器拦截器";
}
}
Chain:
public class Chain {
private List<Interceptor> interceptors;
private int index;
public String request;
public Chain(List<Interceptor> interceptors, int index, String request) {
this.interceptors = interceptors;
this.index = index;
this.request = request;
}
public Chain(List<Interceptor> interceptors, int index) {
this.interceptors = interceptors;
this.index = index;
}
public String proceed(String request) {
if (index >= interceptors.size()) {
throw new AssertionError();
}
Chain chain = new Chain(interceptors, index + 1, request);
Interceptor interceptor = interceptors.get(index);
return interceptor.intercept(chain);
}
}
public static void main(String[] args) {
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new RetryAndFollowUpInterceptor());
interceptors.add(new BridgeInterceptor());
interceptors.add(new CacheInterceptor());
interceptors.add(new ConnectInterceptor());
interceptors.add(new CallServerInterceptor());
Chain chain = new Chain(interceptors, 0);
System.out.println(chain.proceed("OkHttp请求"));
}
首先将五大拦截器加入List集合中,创建一个Chain对象,将List集合加入链条,并指向第一个拦截器,开始执行proceed,调用Chain的proceed,new一个新的Chain,这时index + 1, 说明新的链条指向第二个拦截器,拿到index对应的拦截器,执行intercept方法,在intercept方法中,通过新链条的index执行第二个拦截器的intercept,以此类推,这样就像工厂流水线一样,一个工序一个工序流下去,而且每个工序在传递给下一个工序之前,还能做自己的事情,互相不影响,这就是所谓的责任链。看下这个过程的打印结果:
OkHttp请求===>经过重试重定向拦截器 ===>经过桥接拦截器 ===>经过缓存拦截器 ===>经过连接拦截器===>经过请求服务器拦截器
OkHttp响应===>经过请求服务器拦截器===>经过连接拦截器===>经过缓存拦截器===>经过桥接拦截器===>经过重试重定向拦截器
拿到响应后,再根据对应的拦截器反向传输回来,类似一个U型流向。
了解了责任链模式后,我们分析下这五大默认拦截器。
重试重定向拦截器
第一个拦截器:RetryAndFollowUpInterceptor,主要就是完成两件事情:重试和重定向。
重试
@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 = new StreamAllocation(client.connectionPool(), createAddress(request.url()),
call, eventListener, callStackTrace);
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(), 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.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, 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) {
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());
}
// 判断是不是可以复用同一份连接
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;
}
}
首先在一个while(true)的死循环里,通过各种try.....catch来分别处理不同类型的异常。
首先定义了一个布尔型releaseConnection
变量,默认为true
,在try语句块里:
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
如果请求没有异常,责任链将事件分发到下一个拦截器,releaseConnection
置为false
。
如果请求阶段发生了 RouteException
或者 IOException
会进行判断是否重新发起请求。
RouteException
catch (RouteException e) {
// 路由异常,连接未成功,请求还没发出去。
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
}
IOException
catch (IOException e) {
// 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
// HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
}
两个方法都是通过recover
方法判断是否能够进行重试,如果返回true
,则表示允许重试。进入recover
方法看下:
private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// 1
if (!client.retryOnConnectionFailure()) return false;
// 2
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// 3
if (!isRecoverable(e, requestSendStarted)) return false;
// 4
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
分别看下这几个if
语句:
-
在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试。这个就是在我们最开始配置OkhttpClient时,如果手动配置了fasle,则不允许重试。
-
这个判断在RouteException下是永远不成立的,所以我们看IOException的情况,在IOException里面:
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
前面我们分析到,HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1,requestSendStarted一定是true,所以主要看第二个条件,
userRequest.body() instanceof UnrepeatableRequestBody
,UnrepeatableRequestBody
是一个接口,这个接口的作用就是打个标签,如果我们的请求体标记成这种类型,那就表示这个请求拦截器不会帮你重试。public class MyRequestBody extends RequestBody implements UnrepeatableRequestBody { @Nullable @Override public MediaType contentType() { return null; } @Override public void writeTo(BufferedSink sink) throws IOException { } }
就像上面这样,自定义MyRequestBody,并且实现了UnrepeatableRequestBody这个接口,如果请求的是我们自定义的请求体,那么OkHttp对这次请求失败后,判断到你的请求体实现了这个接口,那他就不会帮你重试。
-
判断是不是属于重试的异常,这个里面调用了
isRecoverable
方法。如果这个方法返回了false,那么也不会重试,什么情况下返回false呢,我们看一下:private boolean isRecoverable(IOException e, boolean requestSendStarted) { // 如果是协议异常,返回false if (e instanceof ProtocolException) { return false; } 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; } return true; }
-
如果是协议的异常,则返回false,什么情况下是协议的异常,我们看下源码:在CallServerInterceptor中,有一段这样的代码:
if ((code == 204 || code == 205) && response.body().contentLength() > 0) { throw new ProtocolException( "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength()); }
如果code是204或者205(204表示服务器没有返回内容,205表示服务器让你重置内容,也就是刷新网页。),这种情况下,是没有响应体的,但是第二个条件是响应体长度大于0,这就冲突了,这种冲突是服务器的问题,就会抛出ProtocolException。
-
如果异常属于IO异常,同时又属于SocketTimeoutException,那OkHttp就会帮你重试。例如:网络波动造成了Socket连接的超时,可以使用不同路线重试。
-
SSL证书异常/SSL验证失败异常,则不会重试。前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确。
-
如果SSL握手未授权异常,也不能重试。
-
-
经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比如 DNS 对域名解 析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。
重定向
如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要给用户的,还需要进一步来判断是否需要重定向,重定向的判断在followUpRequest
方法中。
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) {
// 407 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权
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 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization”
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
// 308 永久重定向
// 307 临时重定向
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// 300 301 302 303
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// 如果用户不允许重定向,那就返回null
if (!client.followRedirects()) return null;
// 从响应头取出location
String location = userResponse.header("Location");
if (location == null) return null;
// 根据location 配置新的请求 url
HttpUrl url = userResponse.request().url().resolve(location);
// 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
if (url == null) return null;
// 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
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();
/**
* 重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,
* 即只有 PROPFIND 请求才能有请求体
*/
//请求不是get与head
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
// 除了 PROPFIND 请求之外都改成GET请求
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
// 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// 在跨主机重定向时,删除身份验证请求头
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408 客户端请求超时
case HTTP_CLIENT_TIMEOUT:
// 408 算是连接失败了,所以判断用户是不是允许重试
if (!client.retryOnConnectionFailure()) {
// The application layer has directed us not to retry the request.
return null;
}
// UnrepeatableRequestBody实际并没发现有其他地方用到
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求
了
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null;
}
return userResponse.request();
// 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
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;
}
}
重定向的主要过程分析都在代理中注释了,主要注意的一点是:followup在拦截器中定义的最大次数是20次。
总结
本拦截器是整个责任链上的第一环,这意味着它会是首次接触到Request
与最后接收到Response
的角色,在这个拦截器中主要的功能就是判断是否需要重试与重定向。
重试的前提是出现了 RouteException
或者 IOException
,一旦在后续的拦截器执行过程中出现了这两个异常,就会通过recover
方法进行判断是否进行连接重试。
重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用followUpRequest
根据Response
的响应码(当然,如果直接请求失败, Response
都不存在就会抛出异常),来进行不同的重定向操作。注意:followup 最大发生20次。
桥接拦截器
BridgeInterceptor
,连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单。
// 为我们补全请求头,并默认使用gzip压缩,同时将响应体同时设置为gzip读取。
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
// 创建新的requestBuilder
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) {
// 默认建立长连接,如果我们不想与服务器建立长连接,value改为Close。
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<Cookie> 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 | 请求体类型,如: application/x-www-form-urlencoded |
Content-Length / Transfer-Encoding | 请求体解析方式 |
Host | 请求的主机站点 |
Connection: Keep-Alive | 保持长连接 |
Accept-Encoding: gzip | 接受响应支持gzip压缩 |
Cookie | cookie身份辨别 |
User-Agent | 请求的用户信息,如:操作系统、浏览器等 |
在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:
- 保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的
CookieJar
不提供实现 - 如果使用gzip返回的数据,则使用
GzipSource
包装便于解析。
总结
桥接拦截器的执行逻辑主要就是以下几点:
- 对用户构建的
Request
进行添加或者删除相关头部信息,以转化成能够正在进行网络请求的Request
。 - 将符合网络请求规范的
Request
交给下一个拦截器处理,并获取Response
,如果响应体经过了gzip
压缩,那就解压缩,再构建成用户可用的Response
返回。
接下来的缓存拦截器以及后续的分析会在《OkHttp源码解析(下)》中体现。