前言
在上一篇博客中,我们从源码分析了,一次异步网络请求的整个大概表面的流程,但是涉及到某些具体的内容呢,就直接带过了。本篇文章我们就先来了解一下在发起一次网络请求时,OkHttp是怎么发起请求获取响应的。这里边就涉及到OkHttp的一个很棒的设计——拦截器Interceptor。
分析
源码基于最新的版本:3.10.0。
还记得上一篇博客中一次异步任务中,到最后一步执行的代码吗?
那就是RealCall.AsyncCall#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);
}
}
我们注意到其实真正执行网络请求,或者说入口是在
Response response = getResponseWithInterceptorChain();
这一句代码中。
另外,在发起同步请求的时候RealCall#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);
}
}
核心入口是一样,只不过我们要知道,同步请求的时候是在主线程中执行的,而异步请求我们在上一篇博客中也分析过了,是在线程池中执行的。
下面我们就来看一下RealCall#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) {
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
是一个接口,它有个内部接口Chain
,它的具体的实现类RealInterceptorChain
,顾名思义,就是拦截器链,就是将一个个拦截器串起来,然后执行RealInterceptorChain#proceed(Request request)
方法来返回得到Response
。
那么上边具体的实现是在另一个重载方法里边,下边看一下这个重载的方法proceed(...)
都干了什么?
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
// 唤起链中的下一个拦截器
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
return response;
}
上边代码去除一些错误抛出判断的内容后,主要的逻辑就是在唤起链中的下一个拦截器这三行代码。
①第一行:
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
创建一个RealInterceptorChain对象。注意这里传入的一个参数index+1
。
②第二行:
Interceptor interceptor = interceptors.get(index);
获得集合中当前索引的拦截器,index
初始为0。
③第三行:
Response response = interceptor.intercept(next);
调用当前索引拦截器的intercept(Chain chain)
方法。
我们就拿集合中第一个拦截器RetryAndFollowUpInterceptor
为例,
看一下RetryAndFollowUpInterceptor#intercept(Chain chain)
方法:
@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 {
//执行(index+1)拦截器链的proceed()方法
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.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, 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();
}
}
...
}
}
上边最关键的代码,其实也就是这一句:
response = realChain.proceed(request, streamAllocation, null, null);
我们会发现,这一句执行的是下一个拦截器链的proceed
方法,接着回到上边,会调用下一个拦截器的intercept()
方法,这样反复相互递归调用,直到链的尽头,即拦截器链的集合已经遍历完成。
我们查看其它拦截器的源码可以发现,他们的样式非常类似:
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 1、在发起请求前对request进行处理
// 2、递归调用下一个拦截器递归,获取response调用
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
//3、对response进行处理,返回给上一个拦截器
return response;
}
}
那么分析到这里,我们思考一个问题,也可能面试中会问到:
这里为什么每次都重新创建RealInterceptorChain
对象next
,为什么不直接复用上一层的RealInterceptorChain
对象?
因为这里是递归调用,在调用下一层拦截器的interupter()方法的时候,本层的 response阶段还没有执行完成,如果复用RealInterceptorChain对象,必然导致下一层修改该RealInterceptorChain对象,所以需要重新创建RealInterceptorChain对象。
我们通过一张经典图来描述一下上面说的整个逻辑,我们可以看的更清晰一些:
这里的拦截器分层的思想就是借鉴的网络里的分层模型的思想。请求从最上面一层到最下一层,响应从最下一层到最上一层,每一层只负责自己的任务,对请求或响应做自己负责的那块的修改。
源码中核心拦截器
源码中核心拦截器完成了网络访问的核心逻辑,由上边往list中添加拦截器的顺序,我们可以知道它依次调用顺序(也可以在上图中看出),下边大致了解一下各个拦截器所负责的内容:
RetryAndFollowUpInterceptor
①在网络请求失败后进行重试;
②当服务器返回当前请求需要进行重定向时直接发起新的请求,并在条件允许情况下复用当前连接BridgeInterceptor
这个拦截器是在v3.4.0之后由HttpEngine
拆分出来的。
①设置内容长度,内容编码
②设置gzip压缩,并在接收到内容后进行解压。省去了应用层处理数据解压的麻烦
说到这里有一个小坑需要注意:
源码中注释也有说明,
If we add an "Accept-Encoding: gzip" header field,
we're responsible for also decompressing the transfer stream.
就是说OkHttp是自动支持gzip压缩的,也会自动添加header,这时候如果你自己添加了"Accept-Encoding: gzip"
,它就不管你了,就需要你自己解压缩。
③添加cookie
④设置其他报头,如User-Agent,Host,Keep-alive等。其中Keep-Alive是实现多路复用的必要步骤CacheInterceptor
这个拦截器是在v3.4.0之后由HttpEngine
拆分出来的。
①当网络请求有符合要求的Cache时直接返回Cache
②当服务器返回内容有改变时更新当前cache
③如果当前cache失效,删除ConnectIntercetot
这个拦截器是在v3.4.0之后由HttpEngine
拆分出来的。
为当前请求找到合适的连接,可能复用已有连接也可能是重新创建的连接,返回的连接由连接池负责决定CallServerInterceptor
负责向服务器发起真正的访问请求,并在接收到服务器返回后读取响应返回。
自定义拦截器
除了官方已经写好的比较核心的拦截器之外,我们还可以自定义拦截器。比如官方给的一个LoggingInterceptor
来打印请求或者响应的时间日志;再比如我们在向服务器传输比较大的数据的时候,想对post的数据进行Gzip压缩,这个可以参考github上的这个issue:
Add GZip Request Compression #350
或者我之前写的一篇博客:
Okhttp3请求网络开启Gzip压缩
首先我们看一张官方wiki的图:
拦截器可以被注册为应用拦截器(application interceptors)或者网络拦截器(network interceptors)。
其实在上边源码分析中,RealCall#getResponseWithInterceptorChain()
这个方法去构建一个拦截器集合的时候(也决定了其递归调用顺序):
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());//Application Interceptors
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());//Network Interceptors
}
interceptors.add(new CallServerInterceptor(forWebSocket));
...
return chain.proceed(originalRequest);
}
我们可以知道应用拦截器和网络拦截器在递归调用的顺序。
那么,这两者到底有什么区别呢?这里给出官方wiki的翻译:
Application Interceptors
通过OkHttpClient.Builder
的addInterceptor()
注册一个 application interceptor:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
URLhttp://www.publicobject.com/helloworld.txt
会重定向到https://publicobject.com/helloworld.txt
,
OkHttp
会自动跟踪这次重定向。application interceptor会被调用一次,并且通过调用chain.proceed()
返回携带有重定向后的response。
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
我们可以看到,会重定向是因为response.request().url()
和request.url()
是不同的,日志也打印了两个不同的URL。
Network Interceptors
注册一个Network Interceptors的方式是非常类似的,只需要将addInterceptor()
替换为addNetworkInterceptor()
:
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
当我们执行上面这段代码,这个interceptor会执行两次。一次是调用在初始的request http://www.publicobject.com/helloworld.txt
,另外一次是调用在重定向后的request https://publicobject.com/helloworld.txt
。
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
那么我们如何选择这两种拦截器的使用?它们各有各的优缺点,我们可以根据自己需要来合理选择。
Application Interceptors
- 不需要关心由重定向、重试请求等造成的中间response产物。
- 总会被调用一次,即使HTTP response是从缓存(cache)中获取到的。
- 关注原始的request,而不关心注入的headers,比如If-None-Match。
- interceptor可以被取消调用,不调用Chain.proceed()。
- interceptor可以重试和多次调用Chain.proceed()。
Network Interceptors
- 可以操作由重定向、重试请求等造成的中间response产物。
- 如果是从缓存中获取cached responses ,导致中断了network,是不会调用这个interceptor的。
- 数据在整个network过程中都可以通过Network Interceptors监听。
- 可以获取携带了request的Connection。
结语
本篇文章算是接着上篇文章将整个网络请求中重要的交互步骤作了源码分析,详细了解了拦截器的相关内容。
在之后,我们再对OkHttp的任务调度队列,缓存,连接等方面的内容作深入了解。