前言
上文中我们介绍了OkHttp的基本使用和一些简单原理,若有不熟悉的参考OkHttp的运用于原理这篇文章,本篇我们来详细讲解OkHttp的拦截器链.
在探究拦截器链之前,我们先来认识一下责任链模式:
责任链(Chain of Responsibility)模式:为了可以将请求发送者与多个请求处理者进行解耦,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
如上图所示,客户端发送网络请求,通过每个拦截器的加工处理,发往服务器,在响应数据以后,依次通过拦截器加工成客户端所识别的数据返回,其过程实现了发送者与多个请求处理者的解耦,每个处理者各司其职,完成处理后交给下一处理者从而形成一条处理链
拦截器调用
上文中已经讲到OkHttp的请求处理核心都在getResponseWithInterceptorChain()
中,我们来看
Response getResponseWithInterceptorChain() throws IOException {
//以下通过将所有的连接器加入到集合中
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);
}
这里创建了一个拦截器集合,添加的顺序便是之后拦截器执行的顺序,首先是自定义拦截器,其次内置拦截器,到网络拦截器,最后是网络读写拦截器.RealInterceptorChain
在整个拦截器链中用于记录下一个拦截器的索引,监听,及上一个拦截器做的处理结果,在之后的链中,会随着拦截器调用而被传递到下一次记录中.
到RealInterceptorChain
的proceed()
中
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout); //第一个参数为之前创建的拦截器集合,index在每次调用proceed()中都会+1,以便在下次集合中获取下一个拦截器元素对象
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next); //同时将带有下一个拦截器索引信息的链对象传入到下次的调用当中
对于自定义拦截器,会最先被回调,我们这里不去探究.在创建RealCall对象时创建了第一个拦截器RetryAndFollowUpInterceptor
,从集合中获取到该对象后,调用此拦截器的intercept()
方法
重试重定向拦截器
从这里便正式开始了拦截器链的执行之路,首先进入的是
重试
RetryAndFollowUpInterceptor
拦截器的intercept()
方法:
public Response intercept(Chain chain) throws IOException {
//.....省略....
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
//.....省略....
int followUpCount = 0; //用于记录重试的次数,
Response priorResponse = null;
while (true) { //注意这里是循环,也就是说若请求不成功,会重新执行请求
try{
//进入下一拦截器 该方法会一层层调用,直到获取服务器数据返回响应
response = realChain.proceed(request, streamAllocation, null, null);
//对执行之后拦截器过程中可能出现的异常进行捕获,重试的机制主要在异常的捕获中来判断
}catch(RouteException e){ //在发送请求访问服务器过程中,若出现路由连接失败
//通过recover()方法来判断是否有必要进行重试,若是一些不可更改的错误,则直接抛出异常,终止此次请求,否则执行下一次循环
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
continue; //后续代码将不在执行,进入下一循环
}catch (IOException e) {//远程连接后,出现双方通信管道(io)数据交互异常
boolean requestSendStarted = !(e instanceof ConnectionShutdownException); //ConnectionShutdownException只有http2才会被抛出,若为http1则一定为true
if (!recover(e, streamAllocation, requestSendStarted, request))
throw e; //recover返回false,抛出异常并结束
releaseConnection = false;
continue;
}finally { //可以理解为若异常非以上两种异常情况,将直接回收此连接,不会在重试
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
//以上为代码前半段重试操作处理,后半段我们稍后探究
}
}
上述中,首先会进行一次网络请求(调用拦截器)获取响应,对过程中发生的异常进行捕获,而这里并非对所有的异常都进行处理,对于一些不可修复的异常,再多次重试也是没有意义的,例:网址不正确,协议证书有错等等.两次异常捕获中都调用recover()
如果返回true,则可以重试,那么来看一下,可重试需要具备哪些条件呢?
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
//客户端初始化OkhttpClient时配置不允许重试,
if (!client.retryOnConnectionFailure()) return false;
//若为RouteException,条件1参数为false
//若为IOException,之前说过http2才会抛ConnectionShutdownException异常,那么满足条件2--body只允许发送一次
//UnrepeatableRequestBody为一个接口,相当于一个标记,body实现此接口后,连接将不会被重试
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
//方法判断是否为不可修复异常
if (!isRecoverable(e, requestSendStarted)) return false;
//查找是否有更多的路由路线进行连接
if (!streamAllocation.hasMoreRoutes()) return false;
return true; //以上条件都不满足,具备重试条件,可重试
}
若不允许被重试,recover返回false,抛出异常并终止,否则执行continue进入下一次循环,也就代表这重新调用之后连接器,若存在更多的路由路线可连接,更换路线进行重试,对是否为不可修复的异常在isRecoverable()
方法中进行判断:
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
//协议异常,不可修复
if (e instanceof ProtocolException) {
return false;
}
//如果是连接超时则可以重试
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
//SSL握手协议,证书异常,不能重试
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//SSL握手验证异常,不可重试
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
}
- 协议异常: 双方沟通出现障碍,无法进行交流,没必要重试
- SSL证书异常: http2中加入了SSL加密,通过双方的证书签名来验证对方身份,若证书出现问题,无法建立连接,没必要重试
- 连接超时: 可能是网络原因,并非不可修复,若存在其他路由路线,则可以选择换条路线进行重新请求,对于不同的路由路线–DNS解析域名后或许会返回多个IP地址,此次IP地址访问失败,可以选择换其他IP地址进行访问
通过recover()
可以认识到OkHttp的重试所需要具备的条件.
重定向
回到intercept()
方法来看代码的后半段
public Response intercept(Chain chain) throws IOException {
//...省略
//若触发重试代码块,因为continue,将直接跳过后续操作
//followUpRequest() 通过获得响应来判断是否需要进行重定向及具体操作
Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) { //若不需要重定向,此次数据请求结束,回收资源并返回响应
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
//重试重定向并非无限次数执行,当重试次数达到最大数时,宣布失败,MAX_FOLLOW_UPS默认最大为20
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
//.....省略
}}
当服务端数据迁移网址以后,客户端使用原地址请求数据时,服务端会将迁移后的新地址通过header返回给客户端,同时携带3开头的响应码告诉客户端目标地址以改变,需要重定向地址,客户端接收到后根据响应码和新地址来重新发出请求,获取目标数据
followUpRequest()
中代码虽然很多,但其主要作用是根据不同的响应码来分别处理:
private Request followUpRequest(Response userResponse, Route route) throws IOException {
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
case HTTP_PROXY_AUTH: //407 客户端使用HTTP代理服务器,要求代理服务器授权后重新请求
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
return client.proxyAuthenticator().authenticate(route, userResponse);
// 401 验证身份,服务器需要验证使用者的身份,客户端需要在header中加入Authorization
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT: //308 永久重定向
case HTTP_TEMP_REDIRECT: //307 临时重定向
//请求方法非get或者非head,不会重定向
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// 重定向
case HTTP_MULT_CHOICE: //300
case HTTP_MOVED_PERM: //301
case HTTP_MOVED_TEMP: //302
case HTTP_SEE_OTHER: //303
//客户端不允许重定向,返回空
if (!client.followRedirects()) return null;
// 服务端响应的新地址在响应头的Location字段中
String location = userResponse.header("Location");
if (location == null) return null;
// 将旧地址替换,返回新url对象
HttpUrl url = userResponse.request().url().resolve(location);
// 若返回新地址协议不受支持,可能是系统或框架不支持,取不到url对象
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"请求
if (HttpMethod.permitsRequestBody(method)) {//请求非get和header
final boolean maintainBody = HttpMethod.redirectsWithBody(method); //请求为PROPFIND
if (HttpMethod.redirectsToGet(method)) { //如果非PROPFIND
requestBuilder.method("GET", null); //将请求方式改为get,body设为空
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null; //PROPFIND则添加body
requestBuilder.method(method, requestBody);
}
// 若不是PROPFIND请求,body为null,所以要将请求头中有关body头信息删除
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// 当跨主机重定向时,删除请求头Authorization身份验证信息
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408 客户端请求超时,4开头已经不是重定向了,连接已失败
case HTTP_CLIENT_TIMEOUT:
// 判断用户是否允许重试
if (!client.retryOnConnectionFailure()) {
return null;
}
// 此处的判断似曾相识,在重试recover()条件判断中也有此条件,有该接口标记的body,将不会被重试
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
//priorResponse为上一次请求保存的响应,若上一次的响应也是408重试,放弃
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
return null;
}
// 响应中有Retry-After属性,代表多久后重试,则直接返回null
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
// 503 服务器不可用,也代表连接失败
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request();
}
return null;
default:
return null;
}
}
重定向的实际操作其实只发生在响应码300,301,302,303中,根据服务器返回的地址构建新的request并返回,至于其他响应,408,503等连接已经出现失败,若重试成功返回request,否则返回null
由于篇幅,我们只探究重试重定向的核心逻辑,在重试重定向拦截器中,首先一个while大循环,接着调用之后的拦截器来获取响应结果,它是请求的开始也是响应的最后一个结束,请求或连接过程中抛出异常,根据异常调用recover()
判断条件,决定是否具备重试的条件,条件满足则直接进入下一次循环重新请求,否则会返回response响应,根据返回的响应调用followUpRequest()
,并通过响应码是否为3开头来决定是否修改新地址进行重定向,整个循环也就是该拦截器的最大重试限度为20,超过将直接抛出异常,终止循环
桥拦截器
从重试重定向拦截器中执行proceed()
会进入到此拦截器中,并执行到intercept()
方法中
RequestBody body = userRequest.body();
if (body != null) { //若请求体不为空,将请求体的类型设置到请求头中
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
// 请求体长度不确定时,为了减少等待时间,使用分块传输chunked,结合长连接,当请求体数据庞大时,选择分块发送减少资源消耗提高效率,反之给定固定长度
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");
}
}
// 向header中添加url中的域名
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
// 默认设置长连接,所谓长连接就是tcp在发送完消息后不会直接关闭连接,而是持
//续连接,因为TCP的一次建立和销毁需要三次握手四次挥手,为了节省资源减小消耗,会
//在后续的数据传输中重用
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
boolean transparentGzip = false;
// 设置响应体接收格式为gzip压缩
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
//上篇文章中提到过cookie使用,重写loadForRequest()会在此处调用,获取客户端保存的cookie信息
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) { //不为空则添加到请求头中
requestBuilder.header("Cookie", cookieHeader(cookies));
}
//请求的用户信息,即是谁实际发送了请求,如:操作系统,浏览器,手机app
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
//调用下一烂机器
Response networkResponse = chain.proceed(requestBuilder.build());
// 根据返回的响应确定是否保存到cookie中,如果为 CookieJar.NO_COOKIES(默认)
//或没有要保存的信息,不保存,否则回调客户端重写的saveFromResponse(),交给客
//户端保存
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
// 如果有响应体并且请求体类型为gzip压缩,将响应体解压为Gizp对象
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();
总得来讲,桥拦截器比较简单,其主要工作是完善客户端的request,转化为能够访问网络的完整请求体,同时将服务端返回的响应体转换为客户端对象,如解压缩
缓存拦截器
OkHttp提供了自己的缓存机制,为了减少服务器端的压力,提高效率,客户端在GET请求网址时,会将请求地址与对应服务器返回的响应以键值对形式进行缓存,当之后使用相同的网址访问数据时,只要缓存数据有效,将不会发起网络请求而直接使用本地的缓存数据.要注意的是客户端只缓存GET请求,并且,OkHttp默认是没有缓存的,需要客户端自己实现缓存类指定存储大小和存储位置,关于缓存的使用和存储效果,在OkHttp的运用与原理(cookie、缓存、源码解析)这篇文章中做了清晰描述.
那么对于OkHttp是怎样处理缓存?其条件是什么呢?
public Response intercept(Chain chain) throws IOException {
// 如果客户端实现缓存对象,则根据url获取本地缓存,否则返回null
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
//...省略...
// 缓存策略类,通过此类来判断如何缓存
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//....稍后介绍....
}
来看 CacheStrategy.Factory以及内部的get()
public Factory(long nowMillis, Request request, Response cacheResponse){
//...省略...
if (cacheResponse != null) {
// 发送请求的本地时间
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
//接受到响应的本地时间 记录两个时间是为了计算并判断当前缓存是否失效
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
// 将服务器端响应的header头属性信息存储到成员变量
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
//Date:请求发送的时间
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
//Expires: 目标资源过期时间
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
//Last-Modified: 目标资源最后一次修改的时间
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
/*ETag: 目标资源唯一标识--作用:若服务器的目标资源有过修改后,
ETag会重新计算得出新标识,客户端则需要使用ETag标识与服务器返
回的ETag进行对比,若不一样则表明目标资源已被修改,本地缓存数据
无效,重新发送网络请求获取目标资源来更新本地缓存数据
*/
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
//Age: 该数据缓存从产生到现在的时间,即缓存的年龄(服务器返回)
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
要注意,这些头信息是由服务器返回的response头信息.
内部类Factory主要目的是创建一个CacheStrategy策略类,通过get()
去判断应创建一个怎样的策略类对象,即获取目标数据是访问网络还是使用本地缓存.根据CacheStrategy的构造参数networkRequest 与cacheResponse: 是否为null来决定,前者不为空访问网络,后者不为空使用缓存
public CacheStrategy get() {
//执行策略
CacheStrategy candidate = getCandidate();
//onlyIfCached:请求头Cache-control的属性,由客户端设置,在上篇文章中
//有写到使用方式.设置该属性表示客户端只接收缓存的数据,也就是说此属性
//将不会访问网络
//那么,策略判断认为需要访问网络请求数据(例如缓存过期或者目标数据有过修改),而客户端又要求只能用缓存,两个意见冲突则直接返回一个什么都没有的对象
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
getCandidate()
是CacheStrategy内部类Factory对象的方法,用于根据响应与请求判断是否访问网络或使用缓存而返回一个CacheStrategy对象
private CacheStrategy getCandidate() {
// 缓存为空,请求网络
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//https请求,没有所需的握手(建立连接过程中发生)
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// isCacheable()根据响应码和响应头判断是否需要请求网络
/*
凡是响应码为200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308,若请求头或响应头中不包含noStore(不允许存储缓存),返回true
凡是响应码为303,307,响应头中包含Expires(值为缓存过期时间),或Cache-Control中带有max-age,public,private其中一个,同样不包含noStore,返回true
否则,一律返回false
*/
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
/* Cache-Control:noCache,请求头信息,noCache并不是表面看起来的不缓存
数据,数据也会进行缓存,只是每次在使用本地缓存前需要先进行一次网络
请求验证缓存
If-Modified-Since:请求头中携带的存储客户端缓存最后修改的时间,服务
器将实际文件修改时间与请求头中时间进行对比,若相同返回304码表示目标
资源未更改,客户端可以使用本地缓存,若不同返回200,同时将目标资源返回
给客户端
If-None-Match:与If-Modified-Since类似作用,value为缓存资源的ETag
值(资源唯一标识),到服务端时同样会进行比较,相同返回304,不同返回目
标资源
*/
CacheControl requestCaching = request.cacheControl();
//请求包含noCache请求头或If-Modified-Since或者有If-None-Match,访问网络
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// Cache-Control: immutable 响应头中包含,表示目标资源将一直不会修改,可以直接使用缓存
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
/*
ageMillis:缓存产生到现在的时间
minFreshMillis: Cache-Control:min-fresh的值,客户端认为的缓存剩余有效可用时间
freshMillis:响应缓存最小可用有效时长
maxStaleMillis:Cache-Control:max-stale值.缓存过期后还可使用的时长,若未指定值,表示过期多久都可使用,反之表示过期后还可使用多久
*/
//若无需访问网络验证缓存并且(缓存产生年龄+客户端认为的缓存有效时间) < (缓存实际有效时长 + 缓存后仍可使用时长) 则可以使用缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
// 如果缓存年龄+客户端认为的有效时间超过实际缓存时间,提出警告信息
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
//缓存年龄超过1天并且未设置过期时间,也要添加警告
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// 走到这里说明缓存已经过期了,那么就一定需要请求网络根据一些头信息与服务器对比来验证目标资源是否有过更改,这些成员变量是在CacheStrategy.Factory构造方法中从上一次返回的响应头中获取进行赋值的
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);
}
//如果有则添加到请求头中发送网络请求验证本地缓存是否还可用
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);
}
一些缓存相关的标签在注释中已经描述很明确,相关方法通过注解已经描述的非常清楚,不在翻看源码,有兴趣读者可以跟进看看内部源码
Cache-Control是header中的属性信息,其包含很多value,用于缓存的控制,不熟悉可以了解相关资料,内容比较简单
条件是顺序执行的,但凡中间有一个条件满足,将不会进行之后的判断,所以后面的判断是建立在前面条件不满足的前提下的
好了继续回到CacheInterceptor.intercept()
public Response intercept(Chain chain) throws IOException {
//....省略....
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//不需要访问网络,也不能使用缓存,返回504
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();
}
// 策略中网络请求为空 返回响应,否则往下执行
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//...省略...
//执行网络请求
networkResponse = chain.proceed(networkRequest);
//省略
// 本地缓存不为空 网络请求后响应体响应码HTTP_NOT_MODIFIED(目标资源未更改),继续使用本地资源
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();
//.....
//若本地缓存被修改 或为null,则使用请求网络返回的响应体
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//.......
//缓存不为空,响应包含响应体并且满足缓存条件,存入本地
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//请求方式为POST,PUT,PATCH,PROPPATCH,REPORT时 删除缓存
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
总结一下缓存拦截器的任务:首先判断本地是否存在缓存,通过客户端请求与本地的缓存获取一个缓存策略类,整个拦截器核心在于返回怎样的策略类对象,在注释中我们已经描述很清楚,方法中多次用到cache-Control,其可以包含多个值,读者在阅读本文同时,也要简单了解.
连接拦截器
ConnectInterceptor主要负责与服务端的连接建立和维护工作,建立则创建Socket对象开启连接,维护则对已有的连接进行复用,当然前提是可以被复用,由于OkHttp支持多种不同的网络协议版本http1.0,http2.0等,根据不同版本的不同特性来维护连接.
下图是我们接下来要探索的源码主要路线
public Response intercept(Chain chain) throws IOException{
//StreamAllocation路由,连接,流三者之间的桥梁,负责调度协调,其实例化在第一个拦截器中进行,但在这个拦截器中才真正被调用
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//创建或复用并打开一个连接
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
public HttpCodec newStream(){
// ....省略
// 寻找一个健康可靠的连接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
// 根据此链接返回http编码解码器对象
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
// .....省略
}
关于StreamAllocation官方描述会比较明白解释其用途
通过 streamAllocation.newStream()
内创建连接和获取连接编码解码器展开描述
首先要探讨如何创建和复用Connection连接,
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,boolean doExtensiveHealthChecks) throws IOException {
//通过循环获取connection对象(连接)
while (true) {
//寻找连接
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
//寻找的连接没有过连接记录,说明是一个新创建的连接,无需对其检查,直接返回
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
//检查寻找的连接是否健康,比如是否已经close,shutdown,或超时等,有则将此连接丢弃,重新查找
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
在讲述怎么查找连接之前,需要先简单了解一下ConnectionPool连接池,用于管理和复用socket连接,在创建一个新连接后会将连接存储到连接池中保存,以便复用.简单来看
public final class ConnectionPool{
//用于存储RealConnection连接的队列
private final Deque<RealConnection> connections = new ArrayDeque<>();
public ConnectionPool() {
//连接池最多容纳5个闲置连接,每个连接5分钟内可以无动作,超出时间将被去除
this(5, 5, TimeUnit.MINUTES);
}
//从连接池中获取连接
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
//遍历连接池连接
/*
篇幅原因就不在跟进isEligible()的源码,读者可以跟进探究,其主要功能是检查此连接是否可用:
1.如果连接不能添加新流(http1.0一个连接只能有一个流,http2.0一个连接可有多个流)或达到最大限制不可复用
2.请求域名不相同,不可复用.也就是要保证与上次请求的是同一个地址
3.必须是http2连接,这个我们已经讲到过了,http1一个连接只能有一个流
4.DNS,服务代理,CA证书等要完全相同,则可复用
*/
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
//添加连接
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
//调用put()前,如果池中无连接会进入,打开一个类似gc机制,不断检查空闲连接,
//其余情况不会
if (!cleanupRunning) {
cleanupRunning = true;
/*
执行回收算法,其内部工作是找到最不活跃连接,当空闲的连接超过五个后删
除这个不活跃的连接,内部是一个死循环,当所有连接为活跃状态,则等待一
段时间后会继续执行回收机制,直到池中无连接,退出清理,cleanupRunning
赋值false
*/
executor.execute(cleanupRunnable);
}
//添加新连接
connections.add(connection);
}
}
get()
获得连接池连接复用条件通过isEligible()
决定,put()
添加新连接时,会有检查机制不断执行,直到池中无连接时cleanupRunning
为false,并退出检查,下次调用put()
则会重新开启检查机制,具体实现在其cleanup()
方法中,这里不在探究
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
//.....省略
//尝试使用已分配连接获取新流
releasedConnection = this.connection;
//如果此连接不能在分配新流,释放此连接并从池中移除
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// 若可用则进行复用
result = this.connection;
releasedConnection = null;
}
//若等于空表示不可用
if (result == null) {
// 那么试着从池中获取一个连接
Internal.instance.get(connectionPool, address, this, null);
// 不为空表示成功从池中获取
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
//刚开始的判断,如果已分配的连接不能创建新流,在这里进行回收
closeQuietly(toClose);
//如果已分配的连接 或者 成功从池中获取到连接,那么复用此连接
if (result != null) {
return result;
}
//....省略...
//如果根据请求的地址仍没有找到可复用的连接的话,通过切换路由地址再到池中寻找可复用的连接
if (newRouteSelection) {
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) {
//..省略...
// 创建新的连接
result = new RealConnection(connectionPool, selectedRoute);
// 在此连接上添加流
acquire(result, false);
}
//如果修改路由地址后找到可复用连接,返回
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
//新连接,创建socket对象
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,connectionRetryEnabled, call, eventListener);
//...省略...
//添加到连接池中
Internal.instance.put(connectionPool, result);
}
连接拦截器的核心:一是怎样复用一个连接,即根据地址,证书,DNS,最大流限制,http2,空闲超时等条件确定能否满足复用连接的条件,再一个便是如何创建一个连接,connect()
方法便是创建连接的核心,我们来探究其内部的原理
//简要代码
public void connect(){
//只有http才会为null
if (route.address().sslSocketFactory() == null) {
//如果客户端为开启使用明文传输,抛出异常,这个在manifest文件application标签下添加usesCleartextTraffic="true"默认false,开启后可以使用http请求
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
//如果平台不支持,抛异常
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
while(true){
//如果需要建立连接隧道
if (route.requiresTunnel()) {
//建立代理隧道,通过代理隧道建立连接,这样网络会经由代理服务器发送到目标服务器
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
//隧道连接失败,但正确关闭了资源,退出循环
break;
}
} else {
//否则使用socket构建完整的http或https上的连接
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
/*
如果当前连接为http,设置为 Protocol.HTTP_1_1协议
否则创建Tls通道,即在普通的套接字上添加Tls加密协议,这里不了解的可
以到网上查询Tls相关内容,如果协议为Protocol.HTTP_2的话(http2),创
建Http2Connection,并开启连接.根据注释可以翻看establishProtocol()源码
*/
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
//.......省略......
if (http2Connection != null) {//如果是http2,设置该连接的最大并发
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}
}
简单看一下connectSocket()
对Socket的创建过程
private void connectSocket(int connectTimeout, int readTimeout, Call call,EventListener eventListener) throws IOException {
//如果没有代理服务器,通过socketFactory()创建一个普通的套接字
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy); //否则创建代理连接
//...省略....
//执行socket.connect()开启连接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
/*
public void connectSocket(Socket socket, InetSocketAddress address,int connectTimeout) throws IOException {
socket.connect(address, connectTimeout);
}
*/
//保存连接流对象,这里的流将会在下一个拦截器中使用
//Source对应输入流 Sink对应输出流
try {
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
//..省略..
}
connect()
方法只有当新建一个连接时才会被调用,若连接是可以复用的话,并不会执行到这里,在connectSocket()
中我们发现进行了socket的连接,并且将连接流对象进行保存.
花了这么大的篇幅分析了newStream()
的findHealthyConnection()
,跟随源码最后终于找到一个可用的健康连接,回到newStrea()
中
public HttpCodec newStream(){
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this); //通过获取的连接要创建一个HttpCodec,此对象在下一个拦截器中使用,其存储的是刚才保存的连接流
}
public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
StreamAllocation streamAllocation) throws SocketException {
//http2Connection对象在establishProtocol
if (http2Connection != null) { // 如果是http2,创建对应的编码解码器
return new Http2Codec(client, chain, streamAllocation, http2Connection);
} else {
//否则创建http对应的对象
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1Codec(client, streamAllocation, source, sink);
}
}
最后一步,调用 proceed()进入下一拦截器
return realChain.proceed(request, streamAllocation, httpCodec, connection)
总结一下,连接拦截器主要工作是与网络服务器建立连接,并且对连接提供了复用机制(必须为HTTP2),再讲述findConnection()
之前,对连接池ConnectionPool的添加和获取做了简单的介绍.至于其他一些方法,这里不在做过多介绍.当然了解ConnectionPool连接池能够更好地帮助读者理解连接的复用与创建
网络流拦截器
在这个拦截器中就要开始真正的网络数据交互了,它所有拦截器中离服务器最近的一个环节.
在上面establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
方法中,我们说过,如果是http,会设置http1协议,而如果是http2,会构建Http2Connection对象,并且,在上面connectSocket()
方法的最后,得到了sink
和source
对象,分别表示socket的网络输出流和网络输入流,可以将其看做io中的output与input.在这个拦截器中将会被使用到.在介绍这个拦截器之前首先来看一个类HttpCodec
public interface HttpCodec {
//连接超时时间,被用于连接重用
int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
// 创建请求body,如果有
Sink createRequestBody(Request request, long contentLength);
// 通过连接拦截器中获取的sink将请求发往服务器
void flushRequest() throws IOException;
// 对于http1codec实例,会调用flushRequest(),而http12codec首先发送请求并关闭当前流通道
void finishRequest() throws IOException;
// 通过连接拦截器中获取的source对象读取响应头
Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;
// 与createRequestBody相对,读取服务器返回的响应体,并转换为相应的RealResponseBody
ResponseBody openResponseBody(Response response) throws IOException;
// 关闭连接或流
void cancel();
}
其拦截器的核心主要围绕此接口的几个方法,而直接继承此接口的两个类分别是Http1Codec与Http2Codec,具体使用哪一个,取决于使用哪一个协议,即http1.0还是http2.0.
这里做一下普及,http1:向服务器发送基于文本类型的请求,其局限性在于该协议是一个流水线式的网络传输,若一个请求不能成功接受到响应,则会阻塞后续的请求,http2完全采用二进制的方式进行传输,且与http1不同的不仅在其二进制传输,两端仅需要建立一个连接,单个连接可以包含多个流,多次请求响应的交互便是流上进行的,而每个消息又会被分为更小单位的帧,最终发往服务器.与http1最明显的区别,使用二进制形式传输数据,并且能够解决同一连接的并发问题,同一连接的不同流可看做物理管道,每个流对应了唯一的id标识,以便到目的地时便于区分通道,且数据的发送无需照顺序发送,在每一个帧的数据中,会包含有关于数据的标识,即使乱序发送,到达目的地时,根据帧单元序号可完美还原数据本身
熟悉了http1与http2的根本区别,对于研究网络流拦截器有着关键作用,由于框架庞大,文章无法将所有知识点进行详细讲解,所以本文以http2请求为基础来探究,倘若能理解基于http2的CallServerIntercept,那么http是要比其更加简单,废话不多说,进入正文:
在连接拦截器中newStrem()
中findHealthyConnection()
得到一个连接,若为http2,则会返回一个Http2Connection,接着使用该连接的newCodec()
创建对应的Http2Codec对象,简单来看
public final class Http2Codec implements HttpCodec {
//通过此方法获取输出流对象,其后会通过输出流将封装的body对象发向网络
public Sink createRequestBody(Request request, long contentLength) {
return stream.getSink();
}
//将请求头写入到流对象中
public void writeRequestHeaders(Request request) throws IOException {
if (stream != null) return;
boolean hasRequestBody = request.body() != null;
// 获取请求的header集合,在类的成员变量中会有一些预定的头信息,如果匹配将会全部添加到集合中
List<Header> requestHeaders = http2HeadersList(request);
// 向Http2Connection中添加流,
stream = connection.newStream(requestHeaders, hasRequestBody);
//...省略
}
public void flushRequest() throws IOException {
//调用sink.flush()将请求发往服务器
connection.flush();
}
public void finishRequest() throws IOException {
// 关闭当前连接的当前流管道,steam流在writeRequestHeaders()被调用时初始化
stream.getSink().close();
}
// 读取响应头
public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
// 获取此流的响应体,若响应体为空,会阻塞线程,而真正的去网络获取响应的底层io操作
// 其实并不在这个方法里,我们之前讲过若连接池中没有可用连接,会创建一个连接
// ,当条件满足创建http2Connection,在connect()->establishProtocol()中最
// 后一行http2Connection.start();,读者可以顺着这条线往下走便可以找
// 到框架是如何读取响应的
List<Header> headers = stream.takeResponseHeaders();
// 解析获取到的headers集合,转为response.builder
Response.Builder responseBuilder = readHttp2HeadersList(headers);
return responseBuilder;
}
// 将request的请求头转化为集合
public static List<Header> http2HeadersList(Request request) {
Headers headers = request.headers();
// Http2Codec定义了一些常见请求头,若request有匹配将直接存入list中
List<Header> result = new ArrayList<>(headers.size() + 4);
result.add(new Header(TARGET_METHOD, request.method()));
result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
String host = request.header("Host");
if (host != null) {
result.add(new Header(TARGET_AUTHORITY, host)); // Optional.
}
result.add(new Header(TARGET_SCHEME, request.url().scheme()));
for (int i = 0, size = headers.size(); i < size; i++) {
ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name)) {
result.add(new Header(name, headers.value(i)));
}
}
return result;
}
public ResponseBody openResponseBody(Response response) throws IOException {
String contentType = response.header("Content-Type");
// 获取响应中body的长度
long contentLength = HttpHeaders.contentLength(response);
// 将一个流输入通道封装到RealResponseBody对象并返回
Source source = new StreamFinishingSource(stream.getSource());
return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
}
}
此拦截器的主要两个相关类,Http2Connection与Http2Codec,前者是一个可以承载多个连接流的连接,每个连接流被封装为一个Http2Stream,在Http2Stream中封装了两个内部类,分别是FramingSink和FramingSource,很明显它又对Sink(输出)与Source(输入)的网络流的封装,而且以Framing开头有帧的意思.最终仍是用到了连接拦截器中获取的sink与source对象去接收与发送服务端的消息,读者可以跟进去简单做一了解
当然在http1.0的话并没有这些封装类的,其主要涉及到的为Http1Codec.
介绍完Http2Codec的大致功能后我们正式来看网络流拦截器的intercept()
public Response intercept(Chain chain) throws IOException {
//请求header的监听回调
realChain.eventListener().requestHeadersStart(realChain.call());
//调用Http2Codec的writeRequestHeaders()将请求写入到网络流中
httpCodec.writeRequestHeaders(request);
/*
内部源码connection.newStream(requestHeaders, hasRequestBody);
private Http2Stream newStream(....){
...
创建一个连接流
stream = new Http2Stream(streamId, this, outFinished, inFinished, requestHeaders);
...
streams.put(streamId, stream); 将连接流与标识id绑定并保存,随后
发送请求时标识也将会随着数据一同写入到网络io流中用于区分管道
...
将数据写入到buffer中包括流id,请求头等
writer.synStream(outFinished, streamId, associatedStreamId, requestHeaders);
...
}
*/
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
//检查请求方式为!GET||!HEAD 并且有body
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
//Expect:100-continue : 有请求正文的话,会首先请示服务器是否接收带有body的请求,
//若服务器表示只接受请求头而不要请求体时,会返回给客户端4开头响应码,反之响应为null
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
// 含有Expect:100-continue信息,向服务器发送请求
httpCodec.flushRequest();
// 同时读取服务器给出的响应
responseBuilder = httpCodec.readResponseHeaders(true);
}
// 响应为null表示服务器同意接收,继续书写请求体
if (responseBuilder == null) {
long contentLength = request.body().contentLength();
// 将httpCodec的sink输出流封装到CountingSink中,此处返回为FramingSink
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
// 将sink封装到bufferdSink中
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
// 通过bufferdSink将body写入io中
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
// 如果服务器不接受,且连接为http1.0,阻止该连接被重用,复用连接的前提必须为http2
streamAllocation.noNewStreams();
}
}
// 无论有无请求体,最终都会flush()一次请求到服务器,将请求发送,Http2Codec中首先会将缓存中的请求发送,之后将该流关闭
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();
// 即使请求头中没有100-continue,服务器也返回了100响应码,再次读取网络响应并拼接响应
if (code == 100)
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
//响应码101:切换协议 如果服务器要求切换协议,返回时将回到重试重定向拦截器中重新发送请求,所以body没必要有内容
if (forWebSocket && code == 101) {
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
// 否则正常获取响应体,
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
/*
public ResponseBody openResponseBody(Response response) throws IOException {
String contentType = response.header("Content-Type");
long contentLength = HttpHeaders.contentLength(response);
Source source = new StreamFinishingSource(stream.getSource());
获取响应头中关于响应体的信息,类型,长度,响应体流source, 最后封装到RealResponseBody中
return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
// 而在客户端callback.onResponse()回调中----response.body().string()内部,通过source流以字符串形式读取响应体,
}
*/
}
// 服务器要求关闭连接 则关闭此连接
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
//...
return response;
}
小总结:这里针对http2的情况作了讲解,因为网上有关http2的网络流拦截器讲解比较少,所以基于http2连接来分析的网络流拦截器,对于http1的拦截器,当读者真正理解http2以后,发现http1中所做的操作并没有那么多,反而更加简单,因为其不会涉及到一个连接的多流传输,相对来说要简单许多
总结
首先需要知道一点,拦截器intercept()
方法中,在调用chain.proceed()
前与后所代表的的含义是不同的,未开始调用之前为客户端请求的处理,而在调用该方法以后,为服务器返回的响应体.其次五个拦截器:
- 重试重定向拦截器: 若发生路由异常或输入输出管道异常,可能会重试,而这些异常基本来自于之后的拦截器操作过程中发生的,一些不可修改的异常,将不会进行重试,重定向当服务器返回有新地址时,需要重新获取目标资源,则会修改当前的request中url并重新发送请求.该拦截器有最大的限制循环次数,超出次数将不会进行重试重定向
- 桥拦截器:比较简单.完善用户的request请求,拼凑一个完整的网络请求
- 缓存拦截器: 在客户端允许缓存的情况下,根据客户端提供的数据缓存地址即大小,根据缓存策略来决定使用缓存还是发送网络请求
- 连接拦截器:与之相关的两个类ConnectionPool连接池与StreamAllocation,其功能用于维持connection,stream与route三者之间的关系.进入该拦截器后,首先会从连接池中获取连接,若池中没有连接则会创建一个新的连接,复用连接的前提条件是必须为http2请求,创建新连接同时,会将socket的输入输出流进行存储,在网络流拦截器中进行使用
- 网络流拦截器: 根据连接拦截器获取的输入输出流Sink与Source与服务器端进行交互,该拦截器是离服务器最近的一个,本文我们分析
终于写完了,由于没有做充分的考虑,所以将五个内置拦截器放到一篇文章中
讲解,导致文章异常的长,由于篇幅的原因,一些源码以注释的形式进行了讲解.