这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
上文我们已经分析了Okhttp的拦截器,那么下面我将和大家一起分析Okhttp系统内部的那些拦截器的具体作用,在责任链模式下,又会碰撞出什么火花呢?
五种拦截器源码分析
RetryAndFollowInterceptor重定向拦截器
从名字上可以看出就应该知道该拦截器的作用是进行失败重连的;打个比方,如果我们想要失败重连,就可以在OKhttpClient进行配置,但是有一点我们需要注意,不是所有的网络请求失败后都可以重连,有一定的限制范围的.因此,Okhttp内部就会进行检查网络请求异常和行为码判断,如果符合相应条件,才会去执行重连的,这也就是重定向拦截器存在的意义
下面我们看下它的源码,首先看下intercept方法
- 先看初始化部分
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;
- 这里创建了一个streamAllocation对象,这个其实用来建立执行http请求所需要那些网络的组件 ,分配stream流
- 需要注意的是虽然streamAllocation对象已经创建好,但是并没有使用到,真正使用到的是ConnectInterceptor(后面会细说),主要是用于获取服务端Connection的连接和用于服务端进行数据传输的输入输出IO流,会依次通过拦截器链传递给ConnectInterceptor拦截器使用的
对此,还有个问题值得思考,我们知道在整个OkHttp拦截器链的过程中,可以理解为一个递归,内部会通过**RealInterceptorChain()**这个类负责所有拦截器并串联起来,所以说只有当所有拦截器执行完之后才会被返回Repsonse;但是在平时开发过程中,网络并不是一直稳定的,肯定会有不同程度的问题,有可能网络中断或者失败,那么response返回的code就不是正常的200了,已经出现异常了,那么就要用到RetryAndFollowInterceptor拦截器,它是怎么进行拦截的呢?
- 可以看下intercept方法中的while循环
private static final int MAX_FOLLOW_UPS = 20;
while(true) {
....
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
}
- 这里代码过多就只展示部分了,所有的逻辑都在这个while死循环当中,同学可以去详细看下,我就不多说了
- 我们看下,这里面会对重试次数判断,也就是这里不可能无限制的去重试网络请求,这里MAX_FOLLOW_UPS的值设置为20,换而言之,如果okhttp内部超过了20次的重试请求之后释放它的straeamAllocation对象,当然也不会再去请求了,这也是这个拦截器的核心流程,它有个跳出的逻辑
RetryAndFollowInterceptor总结
- 创建StreamAllocation对象,用来建立Okhttp请求所需要所有的网络组件,用来分配Stream
- 调用RealInterceptorChain.proceed()方法进行实际的网络请求
- 根据异常结果或者响应结果判断是否要重新请求
- 调用下一个拦截器,对response进行处理,返回给上一个拦截器
BridgeInterceptor桥接拦截器
对于BridgeInterceptor桥接拦截器,它负责的主要内容就是设置内容长度,编码方式,压缩等等,主要就是添加头部
看下intercept方法
- 首先,初始化工作
@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<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());
}
....
}
- 这里这么多行代码,其实主要作用就是给一个普通的request添加很多头部信息,成为一个可以发送网络请求的request
- 具体来看,从代码当中,可以很清晰的看出,会从原来空的contentType添加请求头,比如说Content-Type,Content-Length,Transfer-Encoding,Host主机,Connection Keep-Alive(在一定时间内保持连接状态),这些代码所做到的就是初始化工作,添加header头部信息
- 转化可用的Repsonse
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
- 这里调用了RealInteceptor的proceed方法,向服务器发送一个请求,服务器获取请求操作后会返回客户端response响应
- 接着调用HttpHeaders的静态方法receiveHeaders,将我们的网络请求和服务器返回给我们的Repsonse转化为用户可以使用的Respsonse
- 可以看下receiveHeaders的具体实现
public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {
if (cookieJar == CookieJar.NO_COOKIES) return;
List<Cookie> cookies = Cookie.parseAll(url, headers);
if (cookies.isEmpty()) return;
cookieJar.saveFromResponse(url, cookies);
}
其实这里内部主要就是使用CookieJar保存下响应报文的信息,其中HTTP的Cookie就是用来保存用户的一些信息的
- Content-Encoding支持gzip
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();
- 这里就去重新构建响应Repsonse
- 接着利用transparentGzip可以让响应头Content-Encoding对Gzip格式进行支持,并且解压成用户可用的响应Repsonse,其中对于GzipSource就是以解压的方式去读取流数据
- 最后将响应response返回,进行到下一个拦截器
BridgeInterceptor总结
- 是负责将用户构建的一个Request请求转化为能够进行网络访问的请求
- 将这个符合网络请求的Request进行网络请求
- 将网络请求回来的响应Response转化为用户可用的Response(Gzip压缩,Gzip解压)
CacheInterceptor缓存拦截器
该拦截器的主要负责工作在于从缓存中去拿取响应结果,如果没有的话,才会通过网络请求去获取响应
下面来看下Cache类的源码实现
在解析CacheInterceptor之前,我们首先来使用下Okhttp如何使用缓存的,其实使用起来非常简单,调用cache方法中的Cache类,传入文件目录和大小两个参数,这样就设置好了所需要的缓存路径,代码如下
private fun cacheRequest() {
val client = OkHttpClient.Builder()
.cache(Cache(File("cache"), 24 * 1024 * 1024)).build()
val request = Request.Builder().url("https://www.baidu.com")
.get().build()
val call = client.newCall(request)
kotlin.runCatching {
val response = call.execute()
response.close()
}.onFailure {
println(it)
}
}
- 首先我们可用看到有个internalCache这个接口
final InternalCache internalCache = new InternalCache() {
@Override public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
....
};
可以看到它里面所有的方法,无论是get,put也好,都是通过Cache类实现的
- 在看下put方法的具体实现
@Nullable CacheRequest put(Response response) {
.....
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
- 这里会创建一个Entry实例,写入缓存的部分(里面包含了很多url,header,协议方法等等),已经封装了
- 还有个非常值得注意的DiskLruCache,看到这里才明白,整个okhttpd的缓存做了那么多封装呀,最终还是交给了这个DiskLruCache缓存算法来实现的,这里暂不详细看了,但是需要了解的是Okhttp内部是维护了一个清理的线程池的,由它来实现对缓存的自动清理和管理
- 定义好edit对象后,通过cache.edit方法,传入一个key值,这个值就是将我们网络的url转化为对应的key,里面做了md5的加密处理,然后得到md5的16进制表示形式
- 在做好所有工作后,调用entry.writeTo()方法,就是将我们的缓存写入到磁盘上
在这里或许大家都有个疑问,这个writeTo方法传入的editor对象,而这个editor中的key只保存了url,那我们既然把请求的头部和响应的头部都保存好了,那么最关键响应的body我们在哪里保存了呢?
- 看下最后的return的CacheRequestImpl类
private final class CacheRequestImpl implements CacheRequest {
private final DiskLruCache.Editor editor;
private Sink cacheOut;
private Sink body;
boolean done;
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
....
};
}
...
- 可以看到内部有个body,这就是我们的响应主体,同时这里还有一个editor,这里通过DiskLruCache.Editor来书写我们的body
- 这里CaheRequestImpl实现了个CacheRequest接口,这就是暴露给CacheInterceptor缓存拦截器的,可以直接根据这个CacheRqeustImpl这个实现类来更新和写入缓存数据
CacheInterceptor的创建传入的client.internalCache()方法的返回值,可以看下internalCache方法
InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
这里主要就是如果在OkhttpClient.Builder创建的时候传入了Cache参数或者InternalCache参数,那么设置internalCache和Cache会置为null,也就是相互抵消了,如果都没有传入相关的设置,它们默认都为null,具体可看如下代码
void setInternalCache(@Nullable InternalCache internalCache) {
this.internalCache = internalCache;
this.cache = null;
}
/** Sets the response cache to be used to read and write cached responses. */
public Builder cache(@Nullable Cache cache) {
this.cache = cache;
this.internalCache = null;
return this;
}
看下CacheInterceptor的Intercept方法
- 获取缓存策略
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
- 这里一开始就去获取候选的缓存,如果没有的返回null,根据这个结果去设置CacheStrategy缓存策略,这里有networkRequest和cacheResponse两个对象,分别就是网络请求和返回的缓存响应,接下来就是对相关条件进行筛选判断,正式步入缓存拦截器的负责工作
- 一系列的条件判断逻辑
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
- 如果networkRequest为null同时cacheResponse为null,即代表网络请求和缓存都没有的时候,直接会返回504错误的Response响应码
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
- 如果网络请求为null,不允许网络请求的时候,但是有缓存,这里直接返回缓存的响应
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
- 如果networkRequest不为空的时候,表示需要使用网络,则会时候就会调用拦截器链剩下的拦截器继续处理得到networkResponse响应
- 如果这时候缓存响应存在的话,并且有效的话,就需要更新缓存,反之,没有缓存响应的话,直接写入缓存就可以了
ConnectInterceptor连接拦截器
该拦截器主要的工作在于获取一个目标请求的连接,正式开启网络请求
直接看它的intercept方法
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
- 在前面已经提到过了,这里获取的streamAllocation对象就是前面在重定向拦截器传入的,然后再创建了HTTPCodec和RealConnection对象
- 最后我们会调用拦截器链的proceed方法,之前我们已经非常熟悉了,这个时候就已经设置好了整个网络连接拦截器
这里有个疑问这个HTTPCodec到底是什么东西呢?我们来具体看下它的代码
HttpCodec
可以看到它是对HTTP协议的抽象实现, Http1Codec和 Http2Codec实现了这个接口,对应的是HTTP1.1/HTTP2两个版本的实现
public interface HttpCodec {
int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
...
}
- 在HttP1Codec代码中实现了对java.io和java.nio的封装,对此,它对Socket进行了封装
- 主要是用来编码我们的request,以及解码我们的repsonse
看下newStream方法
下面代码先后创建了RealConnection和HttpCodec对象,然后用了一个同步代码块返回Httpcodec对象
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
这里总的来说就做了两件事情
- 调用了findHelathyConnection方法生成RealConnection对象,进行实际的网络连接
- 接着通过realConnection来生成HttpCodec对象,然后在同步代码块中返回
- 看下findHealthyConnection方法
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
- 可以看到这里又调用一个findConnection方法来获取RealConnection
- 同步代码块中可以看到如果这个RealConnection的successCount为0的时候,意味着整个网络连接就结束了
- 如果是不健康的连接(例如Socket连接没有关闭,输入输出流没有关闭)就会调用noNewStreams()方法,并且循环执行findConnection()方法
- 看下findConnection()方法
由于findConnection()方法过于冗长,这里只展示部分核心代码
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
result = this.connection;
releasedConnection = null;
}
- 尝试复用这个connection给releaseconnection的变量
- 去判断这个可复用的连接是否为空,如果不为空就将这个connection赋值给我们的result
如果不能复用的时候,这个时候result是空的
if (result == null) {
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
- 这个时候会从连接池获取一个实际的realconnection,将它赋值给我们的result
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
Internal.instance.put(connectionPool, result);
- 获取realConnection后,调用connect来进行实际的网络连接,然后需要将它反正连接池中
简单来说,findConnection()会去尝试去获取connection,能复用就去复用,不能的话就从连接池当中获取新的connection来进行连接,最后需要把这个新的connection放入连接池当中
ConnectInterceptor总结
- 弄一个RealConnection
- 选择不同的连接方式
- ConnectInterceptor获取Inteceptor传过来的StreamAllocation,streamAllocation.newStream()
- 将刚才创建的用于网络IO的RealConnection对象,以及对于与服务器交互中最为关键的HttpCodec等对象传递给后面的拦截器
- 最后调用CallServerInterceptor来完成整个okhttp网络请求操作
CallServerInterceptor调用服务拦截器
这个拦截器主要是负责向我们的服务器发起一个真正的网络请求,然后接收服务器返回的读取响应,最后再返回
intercept()方法
- 定义了五个对象
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
- RealInterceptorChain: 拦截器链,所有拦截器都要通过这个拦截器链链接在一起,然后完成响应的功能
- HttpCodec:包括Htpp1.1协议和Http1.2协议(本文主要讲的是Http1.1),编码request和解码response
- StreamAllocation:建立http请求所需要的所有网络组件,分配Stream
- RealConnection:把客户端与服务器之间的连接已经抽象成了connection,这个就是抽象连接的具体实现
- Request:网络请求
- 核心部分
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
- 这里httpCodec调用了writeRequestHeaders()方法,并且传入我们的网络请求,目的就是向socket写入请求的头部信息
- 处理特殊情况
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
- 这里的判断就是询问服务器是否发送带有请求体的信息,如果该请求头部可以添加Expect,以及100-continue,那么就会返回给我们一个100的响应码,客户端继续发送请求,跳过写入body,直接获取响应信息,正常情况不一定会走
- 写入body信息 正常情况下,如果没有上述情况下,会走以下流程
request.body().writeTo(bufferedRequestBody);
...
httpCodec.finishRequest();
- 调用body中的writeTo()方法向socket当中写入body信息
- httpCodec的finishRequest()方法调用表明了完成了http请求的写入工作
- 请求读取相应工作
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(false);
}
...
if (forWebSocket && code == 101) {
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
//建造者模式创建了一个response
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
streamAllocation.noNewStreams();
- 调用readResponseHeaders方法读取http请求的头部信息
- 调用noNewStreams()就是禁止新的流创建,关闭IO流,关闭connection
最后检查一下,code响应码是否是204或者205,如果是的话会抛出一个异常
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
至此,我们完成了整个repsonse的获取工作
尾言
至此okhttp的源码解析就告一段落了,简单梳理下整个okhttp中一次网络请求的大致过程
- Call对象对请求的封装
- dispatcher对请求的分发
- getResponseWithInterceptors()方法,拦截器链的使用,五个拦截器的链式调用,层层递进
- RetryAndFollowInterceptor
- BridgeInterceptor
- CacheInterceptor
- ConnectInterceptor
- CallServerInterceptor
okhttp还有很多内容值得去深究,这里就不过多解析了,接下来我将开始分析Retrofit,同是网络框架,相对于Okhttp它又会有怎样的不同呢?持续学习ing....