Okhttp拦截器详解
Okhttp拦截器介绍
概念:拦截器是Okhttp中提供的一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能。我们先来了解下Okhttp中的系统拦截器:
- RetryAndFollowUpInterceptor:负责请求失败的时候实现重试重定向功能。
- BridgeInterceptor:将用户构造的请求转换为向服务器发送的请求,将服务器返回的响应转换为对用户友好的响应。
- CacheInterceptor:读取缓存、更新缓存。
- ConnectInterceptor:与服务器建立连接。
- CallServerInterceptor:从服务器读取响应。
1、拦截器的工作原理
在上一篇文章中我们提到了获取网络请求响应的核心是getResponseWithInterceptorChain()方法,从方法名字也可以看出是通过拦截器链来获取响应,在Okhttp中采用了责任链的设计模式来实现了拦截器链。它可以设置任意数量的Intercepter来对网络请求及其响应做任何中间处理,比如设置缓存,Https证书认证,统一对请求加密/防篡改社会,打印log,过滤请求等等。
责任链模式:在责任链模式中,每一个对象和其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
接下来就通过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);
//通过拦截器执行具体的请求
return chain.proceed(originalRequest);
}
从代码中可以看到首先创建了一个List集合,泛型是Interceptor,也就是拦截器,接着创建了一系列的系统拦截器(一开始介绍的五大拦截器)以及我们自定义的拦截器(client.interceptors()和client.networkInterceptors()),并添加到集合中,然后构建了拦截器链RealInterceptorChain,最后通过执行拦截器链的proceed()方法开始了获取服务器响应的整个流程。这个方法也是整个拦截器链的核心,接下来就看一下RealInterceptorChain中的proceed()方法。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
......
// Call the next interceptor in the chain.
// 调用链中的下一个拦截器,index+1代表着下一个拦截器的索引
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
// 取出要调用的拦截器
Interceptor interceptor = interceptors.get(index);
// 调用每个拦截器的intercept方法
Response response = interceptor.intercept(next);
......
return response;
}
proceed()方法的核心就是创建下一个拦截器。首先创建了一个拦截器,并且将index = index+1,然后我们根据index从存放拦截器的集合interceptors中取出当前对应的拦截器,并且调用拦截器中的intercept()方法。这样,当下一个拦截器希望自己的下一级继续处理这个请求的时候,可以调用传入的责任链的proceed()方法。
2、RetryAndFollowUpInterceptor 重试重定向拦截器
RetryAndFollowUpInterceptor:重试重定向拦截器,负责在请求失败的时候重试以及重定向的自动后续请求。但并不是所有的请求失败都可以进行重连。
查看RetryAndFollowUpInterceptor中的intercept方法如下:
@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 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 {
// 【1】、将请求发给下一个拦截器,在执行的过程中可能会出现异常
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.
// 【2】、路由连接失败,请求不会再次发送
// 在recover方法中会判断是否进行重试,如果不重试抛出异常
// 在一开始我们提到了并不是所有的失败都可以进行重连,具体哪些请求可以进行重连就在这个recover方法中。
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
releaseConnection = false;
// 满足重试条件,继续重连
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
// 【3】、尝试与服务器通信失败,请求不会再次发送
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();
}
}
// 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 {
// 【4】、在followUpRequest中会判断是否需要重定向,如果需要重定向会返回一个Request用于重定向
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release();
throw e;
}
// followUp == null 表示不进行重定向,返回response
if (followUp == null) {
streamAllocation.release();
return response;
}
closeQuietly(response.body());
// 【5】、重定向次数最大为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());
}
// 重新创建StreamAllocation实例
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;
}
}
类的介绍
StreamAllocation:维护了服务器连接、并发流和请求(Connection、Streams、Calls)之间的关系,可以为一次请求寻找连接并建立流,从而完成远程通信。在当前的方法中没有用到,会把它们传到之后的拦截器来从服务器中获取请求的响应。
HttpCodec:定义了操作请求和解析响应的方法,实现类为Http1Codec和Http2Codec,分别对应Http1.x和Http2协议。
可以看到其实在RetryAndFollowUpInterceptor中并没有对Request请求做什么特殊的处理,就将请求发送给了下一个拦截器,在拿到后续的拦截器返回的Response之后,RetryAndFollowUpInterceptor主要是根据Response的内容,以此来判断是否进行重试或者重定向的处理。
2.1 重试请求
根据【2】和【3】可以得出在请求期间如果发生了RouteException或者IOException会进行判断是否重新发起请求。而这两个异常都是根据recover()来进行判断的,如果recover()返回true,就表示可以进行重试,那么我们就来看一下recover()方法中做了哪些操作。
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
// 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
//The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
// 2、如果是RouteException,requestSendStarted这个值为false,无需关心
//We can't send the request body again.
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//todo 3、判断是不是属于重试的异常
//This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;
//todo 4、不存在更多的路由
//No more routes to attempt.
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
1、我们可以在OkhttpClient中配置是否允许进行重试,如果配置了不允许重试,那么请求发生异常后就不会进行重试的操作。
2、如果是RouteException,requestSendStarted这个值为false,无需关心。
如果是IOException,那么requestSendStarted为false的情况只有在http2的io异常的时候出现。那么我们来看第二个条件可以发现UnrepeatableRequestBody是一个接口,这个条件表示如果我们自定义的请求body实现了这个UnrepeatableRequestBody这个接口的时候,就不进行重试请求。
3、判断是不是属于可以重试的异常,主要在isRecoverable()中实现。
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// 协议异常,ProtocolException异常的时候服务器不会返回内容,不能重试
// 请求和服务器的响应存在异常,没有按照http的协议来定义,重试也没用
if (e instanceof ProtocolException) {
return false;
}
// 如果是超时异常,可以进行重试
// 可能是发生了网络波动导致的Socket连接超时
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;
}
4、检查当前有没有可用的路由路线来进行连接,比如DNS对域名解析后会返回多个IP,如果一个IP连接失败后可以使用下一个IP的连接。
2.2 重定向请求
如果请求结束后并没有发生【2】【3】中存在的异常,那么接下来我们会继续进行重定向操作的判断。重定向的逻辑位于【4】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;
}
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;
Request.Builder requestBuilder = userResponse.request().newBuilder();
/**
* 重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,
* 即只有 PROPFIND 请求才能有请求体
*/
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);
}
// 不是 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()) {
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;
}
// 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
if (retryAfter(userResponse, 0) > 0) {
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;
}
}
从followUpRequest()方法中可以得知,如果最终返回的是null,那么表示我们不需要再进行重定向了,如果返回的不为null,那么就需要对返回的Request重新进行请求。这里需要注意的是重定向并不是会一直进行,根据【5】可以得知重定向的最大次数限制是20次。
2.3 总结
3、BridgeInterceptor 桥接拦截器
BridgeInterceptor:桥接拦截器,用户与网络之间的桥梁,我们发出的请求将会经过它的处理才能发送给服务器,可以根据Request信息构建Header以及设置响应信息,例如Content-Length的计算和添加、gzip的⽀持(Accept-Encoding: gzip)、
gzip压缩数据的解包。
1、负责将用户构建的一个Request请求转化为可以进行网络访问的请求,比如对Request相关头部信息进行添加或者删除。
2、将这个符合网络请求的Request进行网络请求,交给下一拦截器处理。
3、将网络请求回来的响应Response转化为用户可用的Response,如果经过了GZIP压缩,那就需要解压。
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
// 根据用户的Request请求构建可以进行网络访问的请求
Request.Builder requestBuilder = userRequest.newBuilder();
// ...执行构建请求的代码,设置请求长度、Cookie等、
// 执行网络请求
Response networkResponse = chain.proceed(requestBuilder.build());
// 判断响应头是否包含Cookie,通过CookieJar来实现存储Cookie
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)));
}
// 返回response
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 | 请求的用户信息,如:操作系统、浏览器等 |
4、CacheInterceptor 缓存拦截器
CacheInterceptor:缓存拦截器,会根据请求的信息和缓存响应信息来判断是否存在缓存可以使用。我们来看CacheInterceptor的intercept()方法。
public Response intercept(Chain chain) throws IOException {
//【1】、从缓存中获取对应请求的响应缓存
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//【2】、创建缓存策略:根据各种条件(请求头)组成
CacheStrategy strategy =
new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
//
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
//【3】、禁止使用网络和缓存
//If we're forbidden from using the network and the cache is insufficient, fail.
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();
}
//【4】、禁止网络请求
//If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//【5】、发起请求,交给下一个拦截器
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//【6】、缓存响应不为空,并且服务器返回304,那就使用缓存的响应修改了时间等数据后作为本次请求的响应
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());
}
}
//【7】、到这里说明缓存不可用 那就使用网络的响应
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
//【8】http头部是否有响应体,并且缓存策略是可以被缓存的,进行缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response,
networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//【9】、如果是不进行缓存的方法则移除缓存
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
【1】、首先来判断cache中的缓存是否存在,如果有就返回response并且会判断当前response是否可用,如果没有,则返回null。
【2】、构建缓存策略CacheStrategy,在CacheStrategy中维护了 networkRequest和cacheResponse,networkRequest表示网络请求,cacheResponse表示缓存响应,根据二者来判断是否允许使用缓存。
【3】、如果用户在创建请求时,配置了onlyIfCached这意味着用户这次希望这个请求只从缓存获得,不需要发起请求。那如果生成的 CacheStrategy存在networkRequest这意味着肯定会发起请求,此时出现冲突!那会直接给到拦截器一个既没有networkRequest又没有 cacheResponse的对象。拦截器直接返回用户504。
【4】、如果禁止网络请求并且缓存响应不为空,那么就直接返回缓存。
【5】、发起网络请求并调用proceed方法传递给下一个拦截器,获取 networkResponse。
【6】、如果第5步中获取到的networkResponse返回304表示缓存资源没有发生改变,可以直接使用,更新一下缓存信息即可。
【7】&【8】&【9】、使用网络请求返回的响应,并判断是否对返回的响应进行缓存。
CacheInterceptor的主要内容是在CacheStrategy内部具体实现的,因为根据CacheStrategy中networkRequest与cacheResponse的不同组合就能够判断是否允许使用缓存。来看CacheStrategy中的getCandidate()方法。
private CacheStrategy getCandidate() {
// No cached response.
//1、没有缓存,进行网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//okhttp会保存ssl握手信息 Handshake ,如果这次发起了https请求,但是缓存的响应中没有握手信息,发起网络请求
//2、https请求,但是没有握手信息,进行网络请求
//Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//3、主要是通过响应码以及头部缓存控制字段判断响应能不能缓存,不能缓存那就进行网络请求
//If this response shouldn't have been stored, it should never be used
//as a response source. This check should be redundant as long as the
//persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//4、如果 请求包含:CacheControl:no-cache 需要与服务器验证缓存有效性
// 或者请求头包含 If-Modified-Since:时间 值为lastModified或者data 如果服务器没有在该头部指定的时间之后修改了请求的数据,服务器返回304(无修改)
// 或者请求头包含 If-None-Match:值就是Etag(资源标记)服务器将其与存在服务端的Etag值进行比较;如果匹配,返回304
// 请求头中只要存在三者中任意一个,进行网络请求
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//5、如果缓存响应中存在 Cache-Control:immutable 响应内容将一直不会改变,可以使用缓存
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//6、根据 缓存响应的 控制缓存的响应头 判断是否允许使用缓存
// 6.1、获得缓存的响应从创建到现在的时间
long ageMillis = cacheResponseAge();
// 6.2、获取这个响应有效缓存的时长
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
freshMillis = Math.min(freshMillis,
SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 6.3 请求包含 Cache-Control:min-fresh=[秒] 能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 6.4
// 6.4.1、Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
// 6.4.2、Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长 如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
// 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
// 6.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以使用的时间
// 允许使用缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
//如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection "Response is stale"");
}
//如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection "Heuristic expiration"");
}
return new CacheStrategy(null, builder.build());
}
//7、缓存过期了
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
//如果设置了 If-None-Match/If-Modified-Since 服务器是可能返回304(无修改)的,使用缓存的响应体
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
-
如果从缓存获取的
Response是null,那就需要使用网络请求获取响应。 -
如果是
Https请求,但是又丢失了握手信息,那也不能使用缓存,需要进行网络请求。 -
如果判断响应码不能缓存且响应头有
no-store(不进行缓存)标识,那就需要进行网络请求。 -
如果请求头有
no-cache(不使用缓存)标识或者有If-Modified-Since/If-None-Match,那么不允许使用缓存,需要进行网络请求。 -
如果响应头没有
no-cache标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行网络请求。 -
如果缓存的响应中包含
Cache-Control: immutable,意味着对应请求的响应内容将一直不会改变,此时就可以直接使用缓存。否则继续判断缓存是否可用。 -
如果缓存过期了,判断响应头是否设置
Etag/Last-Modified/Date,没有那就直接使用网络请求否则需要考虑服务器返回304。
4.1 总结
5、ConnectInterceptor 连接拦截器
ConnectInterceptor:连接拦截器,负责和服务器建立连接。
/** Opens a connection to the target server and proceeds to the next interceptor. */
// 打开与目标服务器的连接,并且进入下一个拦截器
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
// 在重试重定向拦截器中创建的StreamAllocation,在这里使用
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 httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
// 获取RealConnection
RealConnection connection = streamAllocation.connection();
// 进入下一个拦截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
ConnectInterceptor中的代码很少,这里使用的newStream()方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的HttpCodec中包含了输入输出流,并且封装了对Http请求报文的编码与解码,直接使用它就能够与请求主机完成http通信。
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
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
newStream()方法中主要干了两件事,获取RealConnection和建立HttpCodec,而RealConnection具体的获取过程在findHealthyConnection()方法中。进入findHealthyConnection()方法进行查看:
/**
* Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
* until a healthy connection is found.
* 查找连接,如果连接状况良好,则将其返回。如果不健康的话就重复此过程,直到找到健康的连接为止。
*/
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);
// If this is a brand new connection, we can skip the extensive health checks.
// 如果是一个全新的连接,则没有必要做健康检查
synchronized (connectionPool) {
//新创建的连接还未使用
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
// 检查连接池中的连接是否良好,如果不健康,从池中取出并继续查找
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
//禁止这条连接
noNewStreams();
continue;
}
return candidate;
}
}
findHealthyConnection()中主要做了以下几件事:
1、开启一个循环通过findConnection()方法获取连接。
2、判断获取到的连接是否是新创建的,如果是新创建的,则跳过健康检查,直接返回使用。
3、进行健康检查,如果不健康,则禁止这条连接并继续循环。
4、返回最终获取到的连接。
接下来我们进入findConnection()方法进行查看。
/**
* Returns a connection to host a new stream. This prefers the existing connection if it exists,
* then the pool, finally building a new connection.
* 返回一个连接,首先选择的是现有连接,如果现有连接不存在,就从连接池中获取,如果连接池中也没有,就创建一个新的连接
*/
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;//
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
// 尝试使用已分配的连接。我们在这里需要小心已经分配的连接可能受到限制,无法创建新的流
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
// 获取到已分配的连接
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
// 如果上面没有获取到连接,则从连接池中获取连接,最终调用ConnectionPool.get()
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
// 从连接池中获取到连接
foundPooledConnection = true;
//将连接赋值给result
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
// 获取到现有的连接或者从连接池中获取到连接
return result;
}
// If we need a route selection, make one. This is a blocking operation.
// 切换路由
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. This could match due to connection coalescing.
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
// 遍历路由并且再次在连接池中进行查找连接
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
route = selectedRoute;
refusedStreamCount = 0;
// 创建一个新的连接并赋值给result
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
// If we found a pooled connection on the 2nd time around, we're done.
// 如果第二次找到了可以复用的,则返回
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// Do TCP + TLS handshakes. This is a blocking operation.
// 进行TCP+TLS握手
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
// Pool the connection.
// 将连接添加到连接池中
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
findConnection()中的代码主要可以归纳为以下几个步骤:
1.查看当前已分配的连接是否可以复用。
2.如果当前分配的连接不可用,尝试从连接池中找可以复用的连接。
3.如果连接池中没有找到可以复用的连接,则切换路由,继续在连接中尝试找可以复用的连接
4.如果以上的方法中都获取不到可复用的连接,创建一个新的连接,并存到连接池中。
到此,RealConnection就已经获取完成了,这里我们来了解一下连接池的相关部分。
5.1 连接池
OkHttp3将客户端与服务器之间的连接抽象为Connection/RealConnection,为了管理这些连接的复用而设计了ConnectionPool。共享相同IP/PORT的请求可以复用连接。TCP建立连接需要进行三次握手,如果断开连接需要进行四次挥手,如果每次网络请求都执行一次三次握手和四次挥手,那么如此频繁的请求操作将会导致性能问题的产生,所以为了能够进行复用连接,Http中存在一种KeepAlive机制,当数据传输完毕后仍然保持连接,等待下一次请求时可以直接复用该连接,这样就大幅度的提高了请求的效率。
连接池存储
可以看到在put方法中会把连接都加入到队列当中,但是在加入之前会判断清理任务是否有执行,如果没有执行就启动。
继续查看
clearup()方法,都在注释里,这里就不做过多的描述了。
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//检查连接是否正在被使用(StreanAllocation的引用数量)
//If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
// 如果正在使用,记录正在使用的连接数
inUseConnectionCount++;
continue;
}
// 记录闲置连接数
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
//闲置时间 = 当前时间 - 这个连接的最近一次使用时间
//遍历完成后,获得闲置了最久的连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
// 1、闲置时间 > 保活时间 从连接池中移除,移除后返回0,继续检查
// 2、闲置个数 > 连接池最大闲置个数 从连接池中移除,移除后返回0,继续检查
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it
// below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
// 池内存在闲置连接,等待时间 = 还能闲置多久 = 保活时间 - 闲置最久的连接
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we
// run again.
// 存在使用中的连接,等5分钟继续检查
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
//都不满足,可能池内没任何连接,直接停止清理,put后会再次启动
cleanupRunning = false;
return -1;
}
}
连接池删除
public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
// 如果有正在使用的连接,就不能复用
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// If the non-host fields of the address don't overlap, we're done.
// 如果地址不同,不能复用。包括了配置的dns、代理、证书以及端口等等
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
// 域名也都相同,那就可以复用了
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
。。。
}
5.2 总结
6、CallServerInterceptor 服务请求拦截器
CallServerInterceptor:服务请求拦截器,向服务器发起真正的网络请求,然后接收到服务器返回响应。这是整个拦截器链中的最后一个拦截器,在这个拦截器中不会再有proceed方法调用下一个拦截器,而是会把拿到的响应处理之后返回给上一层的拦截器。
/** This is the last interceptor in the chain. It makes a network call to the server. */
public final class CallServerInterceptor implements Interceptor {
private final boolean forWebSocket;
public CallServerInterceptor(boolean forWebSocket) {
this.forWebSocket = forWebSocket;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
// 向socket中写入请求头信息
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
// 如果请求上有一个“ Expect:100-continue”标头,则在发送请求正文之前,请等待“ HTTP1.1 100 Continue”响应。
// 如果没有得到,请返回我们得到的结果(例如4xx响应),而无需传输请求主体。
// Expect:100-continue 这个请求头代表在发送请求体之前需要和服务器确定是否愿意接收客户端发送的请求体
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
// 向socket中写入请求体
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
// 写入完成
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
streamAllocation.noNewStreams();
}
}
// 完成网络请求的写入工作
httpCodec.finishRequest();
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
// 读取网络响应中请求的头部信息
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
// 如果响应是100,这代表了是请求Expect: 100-continue成功的响应,需要马上再次读取一份响应头,这才是真正的请求对应结果响应头。
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
// 读取网络响应的body信息
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
//关闭流
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
7、总结
整个Okhttp功能的实现就在这5个默认的系统拦截器当中,当用户发起一个请求后,会由任务分发器Dispatcher将请求交给重试拦截器处理。
- 重试重定向拦截器给下一个拦截器发起请求后,会获取后续的拦截器返回的结果,并且根据响应判断是否需要重试重定向操作。
- 桥接拦截器在给下一个拦截器发送请求之前,负责将
http协议必备的请求头加入其中并且会添加一些默认行为。在获取到结果后,会调用保存cookie接口并解析GZIP数据。 - 缓存拦截器会在给下一个拦截器发送请求之前,判断是否存在可直接使用的缓存,在或得结果后会判断是否将结果缓存。
- 连接拦截器会在给下一个拦截器发送请求之前,复用或者建立一个新的连接,并获得对应的
socket流。 - 请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。在经过了这一系列的流程后,就完成了一次
HTTP请求!
最后使用一张图来对Okhttp拦截器的整个流程做个总结:
Okhttp拦截器的介绍到这里就结束了,如有需要改正的地方,还请各位BaBa及时指正并多多包涵。