Android OkHttp使用原理的简单理解和使用
OkHttp
0、前言
本文主要包括以下内容
- 1、OKHttp请求的整体流程是怎样的?
- 2、OKHttp分发器是怎样工作的?
- 3、OKHttp拦截器是如何工作的?
- 4、OKHttp如何复用TCP连接?
- 5、OKHttp空闲连接如何清除?
- 6、OKHttp有哪些优点?
- 7、OKHttp框架中用到了哪些设计模式?
Okhttp的子系统层级结构图如下所示:
在整个Okhttp的系统中,我们还要理解以下几个关键角色:
1、请求与响应流程
- (1)、当我们通过
OkhttpClient
创立一个Call
,并发起同步或者异步请求时; - (2)、
okhttp
会通过Dispatcher
对我们所有的RealCall
(Call的具体实现类)进行统一管理,并通过execute
()及enqueue
()方法对同步
或者异步
请求进行解决; - (3)、
execute
()及enqueue
()这两个方法会最终调用RealCall
中的getResponseWithInterceptorChain
()方法,从阻拦器链中获取返回结果; - (4)、阻拦器链中,依次通过
RetryAndFollowUpInterceptor(重定向阻拦器
)、BridgeInterceptor(桥接阻拦器)
、CacheInterceptor(缓存阻拦器)
、ConnectInterceptor(连接阻拦器)
、CallServerInterceptor(网络阻拦器)
对请求依次请求重试,缓存处理,与服务的建立连接后,获取返回数据,再经过上述阻拦器依次解决后,最后将结果返回给调用方。
调用流程如下:
1.1 请求的封装
请求是由Okhttp发出,真正的请求都被封装了在了接口Call的实现类RealCall中,如下所示:
Call接口如下所示:
public interface Call extends Cloneable {
//返回当前请求
Request request();
//同步请求方法,此方法会阻塞当前线程知道请求结果放回
Response execute() throws IOException;
//异步请求方法,此方法会将请求添加到队列中,然后等待请求返回
void enqueue(Callback responseCallback);
//取消请求
void cancel();
//请求是否在执行,当execute()或者enqueue(Callback responseCallback)执行后该方法返回true
boolean isExecuted();
//请求是否被取消
boolean isCanceled();
//创建一个新的一模一样的请求
Call clone();
interface Factory {
Call newCall(Request request);
}
}
RealCall的构造方法如下所示:
final class RealCall implements Call {
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
//我们构建的OkHttpClient,用来传递参数
this.client = client;
this.originalRequest = originalRequest;
//是不是WebSocket请求,WebSocket是用来建立长连接的,后面我们会说。
this.forWebSocket = forWebSocket;
//构建RetryAndFollowUpInterceptor拦截器
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
}
RealCall实现了Call接口,它封装了请求的调用,这个构造函数的逻辑也很简单:
- 赋值外部传入的OkHttpClient、Request与forWebSocket,
- 并 创建了重试与重定向拦截器RetryAndFollowUpInterceptor。
1.2 请求的发送
Okhttp的整个请求分为同步
和异步
两种:
- 1、同步请求通过 调用
Call.exectute()
方法直接返回当前请求的Response
- 因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
final class RealCall implements Call {
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
}
- 2、异步请求调用
Call.enqueue()
方法将请求(AsyncCall)
添加到请求队列中- 当正在执行的任务未超过最大限制
64
,同时同一Host
的请求不超过5
个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。
- 当正在执行的任务未超过最大限制
synchronized void enqueue(AsyncCall call) {
//请求数最大不超过64,同一Host请求不能超过5个
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
final class RealCall implements Call {
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
}
AsyncCall
本质上是一个Runable
,Dispatcher
会调度ExecutorService
来执行这些Runable
。
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
String host() {
return originalRequest.url().host();
}
Request request() {
return originalRequest;
}
RealCall get() {
return RealCall.this;
}
@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 {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
}
不管是同步请求还是异步请求最后都会通过getResponseWithInterceptorChain()
获取Response
,只不过异步请求多了个线程调度,异步 执行的过程。
1.3 请求的调度
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
/** Used by {@code Call#execute} to signal it is in-flight. */
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
synchronized void enqueue(AsyncCall call) {
//正在运行的异步请求不得超过64,同一个host下的异步请求不得超过5个
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
}
Dispatcher是一个任务调度器,它内部维护了三个双端队列:
readyAsyncCalls
:准备运行的异步
请求runningAsyncCalls
:正在运行的异步
请求runningSyncCalls
:正在运行的同步
请求
1、同步请求就直接把请求添加到正在运行的同步请求队列runningSyncCalls中,
2、异步请求会做个判断:
- 如果正在运行的异步请求不超过
64
,而且同一个host下的异步请求不得超过5个
,则将请求添加到正在运行的同步请求队列中runningAsyncCalls
并开始执行请求,否则就添加到readyAsyncCalls
继续等待。
1.4 请求的处理
getResponseWithInterceptorChain
()这个方法才是真正发起请求并处理请求的地方
final class RealCall implements Call {
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//这里可以看出,我们自定义的Interceptor会被优先执行
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);
}
}
Interceptor将网络请求、缓存、透明压缩等功能统一了起来,它的实现采用责任链模式,各司其职, 每个功能都是一个Interceptor
,上一级处理完成以后传递给下一级,它们最后连接成了一个Interceptor.Chain
位置决定功能,位置靠前的先执行,最后一个则复制与服务器通讯,请求从RetryAndFollowUpInterceptor
开始层层传递到CallServerInterceptor
,每一层都对请求做相应的处理,处理的结构再从CallServerInterceptor
层层返回给RetryAndFollowUpInterceptor
,最后请求的发起者获得了服务器返回的结果。
拓展:
责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。
2、拦截器
如上所示责任链添加的顺序及作用如下表所示:
每个拦截器的方法都遵循这样的规则:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//1 Request阶段,该拦截器在Request阶段负责做的事情
//2 调用RealInterceptorChain.proceed(),其实是在递归调用下一个拦截器的intercept()方法
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
//3 Response阶段,完成了该拦截器在Response阶段负责做的事情,然后返回到上一层的拦截器。
return response;
}
}
从上面的描述可知:
- Request是按照interpretor的顺序正向处理,而Response是逆向处理的。这参考了OSI七层模型的原理。
CallServerInterceptor
相当于最底层的物理层,请求从上到逐层包装下发,响应从下到上再逐层包装返回。很漂亮的设计。
interceptor的执行顺序:RetryAndFollowUpInterceptor -> BridgeInterceptor ->
CacheInterceptor -> ConnectInterceptor -> CallServerInterceptor。
2.1 RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor负责失败重试以及重定向
2.2 BridgeInterceptor
重试与重定向拦截器只有在请求的过程中遇到异常或需要重定向的时候才有活干,在它收到请求后会把请求直接通过拦截器链交给下一个拦截器,也就是 BridgeInterceptor 处理。
之所以把 BridgeInterceptor 叫首部构建拦截器,是因为我们给 Request 设置的信息缺少了部分首部信息,这时就要 BridgeInterceptor 把缺失的首部放到 Request 中
。
2.3 CacheInterceptor
我们知道为了节省流量和提高响应速度
,减轻服务端的访问压力
,Okhttp是有自己的一套缓存机制的,CacheInterceptor就是用来负责读取缓存以及更新缓存的。
2.3.1、HTTP缓存原理
在HTTP 1.0时代,响应使用Expires
头标识缓存的有效期,其值是一个绝对时间
,比如Expires:Thu,31 Dec 2020 23:59:59 GMT
。当客户端再次发出网络请求时可比较当前时间和上次响应的expires时间进行比较,来决定是使用缓存还是发起新的请求。
- 使用
Expires
头最大的问题是它依赖客户端的本地时间
,如果用户自己修改了本地时间,就会导致无法准确的判断缓存是否过期。
因此,从HTTP 1.1 开始使用Cache-Control
头表示缓存状态,它的优先级高于Expires
,常见的取值为下面的一个或多个。
- 1、
private
,默认值,标识那些私有的业务逻辑数据,比如根据用户行为下发的推荐数据。该模式下网络链路中的代理服务器等节点不应该缓存这部分数据,因为没有实际意义。 - 2、
public
与private相反,public用于标识那些通用的业务数据,比如获取新闻列表,所有人看到的都是同一份数据,因此客户端、代理服务器都可以缓存。 - 3、
no-cache
可进行缓存,但在客户端使用缓存前必须要去服务端进行缓存资源有效性的验证,即下文的对比缓存部分,我们稍后介绍。 - 4、
max-age
表示缓存时长单位为秒,指一个时间段,比如一年,通常用于不经常变化的静态资源。 - 5、
no-store
任何节点禁止使用缓存。
2.3.2、强制缓存
在上述缓存头规约基础之上,强制缓存是指网络请求响应header标识了Expires或Cache-Control带了max-age信息,而此时客户端计算缓存并未过期,则可以直接使用本地缓存内容,而不用真正的发起一次网络请求。
2.3.3、协商缓存(对比缓存)
- 强制缓存最大的问题是,一旦服务端资源有更新,直到缓存时间截止前,客户端无法获取到最新的资源(除非请求时手动添加no-store头)
- 另外大部分情况下服务器的资源无法直接确定缓存失效时间,所以使用对比缓存更灵活一些。
使用Last-Modify / If-Modify-Since
头实现协商缓存,具体方法是服务端响应头添加Last-Modify
头标识资源的最后修改时间,单位为秒,当客户端再次发起请求时添加If-Modify-Since
头并赋值为上次请求拿到的Last-Modify
头的值。
服务端收到请求后自行判断缓存资源是否仍然有效,如果有效则返回状态码304同时body体为空,否则下发最新的资源数据。客户端如果发现状态码是304,则取出本地的缓存数据作为响应。
使用这套方案有一个问题,那就是资源文件使用最后修改时间有一定的局限性:
- 1、Last-Modify单位为秒,如果某些文件在一秒内被修改则并不能准确的标识修改时间。
- 2、资源修改时间并不能作为资源是否修改的唯一依据,比如资源文件是Daily Build的,每天都会生成新的,但是其实际内容可能并未改变。
因此,HTTP 还提供了另外一组头信息来处理缓存,ETag/If-None-Match
。流程与Last-Modify
一样,只是把服务端响应的头变成Last-Modify
,客户端发出的头变成If-None-Match
。ETag
是资源的唯一标识符,服务端资源变化一定会导致ETag变化。具体的生成方式有服务端控制,场景的影响因素包括,文件最终修改时间、文件大小、文件编号等等。
2.3.4、OKHttp的缓存实现
上面讲了这么多,实际上OKHttp就是将上述流程用代码实现了一下,即:
- 1、第一次拿到响应后根据头信息决定是否缓存。
- 2、下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
- 3、如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
2.3.4.1、缓存策略
HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的,最终响应是从缓存中去,还是从服务端重新拉取,HTTP的缓存机制的流程如下所示
上面提到强制缓存
使用的的两个标识:
- Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
- Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。
再来看看对比缓存
的两个标识:
1、Last-Modified/If-Modified-Since
Last-Modified 表示资源上次修改的时间。
当客户端发送第一次请求时,服务端返回资源上次修改的时间:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比
- 如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改,则返回200表示需要重新请求资源,
- 否则返回304表示资源没有被修改,可以继续使用缓存。
上面是一种时间戳
标记资源是否修改的方法,还有一种资源标识码ETag
的方式来标记是否修改,如果标识码发生改变
,则说明资源已经被修改,ETag优先级高于Last-Modified
。
2、Etag/If-None-Match
ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码:
ETag: "5694c7ef-24dc"
码客户端再次发送,会在header里携带上次服务端返回的资源标识码:
If-None-Match:"5694c7ef-24dc"
服务端接收到客户端发来的资源标识码,则会与自己当前的资源吗进行比较,
- 如果不同,则说明资源已经被修改,则返回200,
- 如果相同则说明资源没有被修改,返回304,客户端可以继续使用缓存。
2.3.4.2、Okhttp的缓存策略
Okhttp的缓存策略就是根据上述流程图实现的,具体的实现类是CacheStrategy
,CacheStrategy的构造函数里有两个参数:
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
这两个参数参数的含义如下:
- networkRequest:网络请求。
- cacheResponse:缓存响应,基于DiskLruCache实现的文件缓存,可以是请求中url的md5,value是文件中查询到的缓存,这个我们下面会说。
CacheStrategy就是利用这两个参数生成最终的策略,有点像map操作,将networkRequest
与cacheResponse
这两个值输入,处理之后再将这两个值输出,们的组合结果如下所示:
- 如果networkRequest为null,cacheResponse为null:only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)。
- 如果networkRequest为null,cacheResponse为non-null:不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络。
- 如果networkRequest为non-null,cacheResponse为null:需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
- 如果networkRequest为non-null,cacheResponse为non-null:Header中含有ETag/Last-Modified标签,需要在条件请求下使用,还是需要访问网络。
那么这四种情况是如何判定的,我们来看一下。
CacheStrategy是利用Factory模式
进行构造的,CacheStrategy.Factory
对象构建以后,调用它的get()
方法即可获得具体的CacheStrategy
,CacheStrategy.Factory.get()
方法内部调用的是CacheStrategy.Factory.getCandidate()
方法,它是核心的实现。
public static class Factory {
private CacheStrategy getCandidate() {
//1. 如果缓存没有命中,就直接进行网络请求。
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//2. 如果TLS握手信息丢失,则返回直接进行连接。
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//4. 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//计算当前age的时间戳:now - sent + age
long ageMillis = cacheResponseAge();
//刷新时间,一般服务器设置为max-age
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//一般取max-age
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
//一般取0
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//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());
}
//6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求
//交给服务端判断处理
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.
}
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);
}
}
整个函数的逻辑就是按照上面那个HTTP缓存判定流程图来实现,具体流程如下所示:
2.3.4.3、缓存管理
这篇文章我们来分析Okhttp的缓存机制,缓存机制是基于DiskLruCache
做的
2.4 ConnectInterceptor
在RetryAndFollowUpInterceptor里初始化了一个StreamAllocation对象,我们说在这个StreamAllocation对象里初始化了一个Socket对象用来做连接,但是并没有真正的连接,等到处理完hader和缓存信息之后,才调用ConnectInterceptor来进行真正的连接
public final class ConnectInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//创建输出流
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
//建立连接
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
ConnectInterceptor在Request阶段建立连接,处理方式也很简单,创建了两个对象:
- HttpCodec:用来编码HTTP requests和解码HTTP responses
- RealConnection:连接对象,负责发起与服务器的连接。
总结:
- 连接拦截器是获取一个与服务端真实的连接。
- 如果当前有可以复用的就用复用的,
- 没有的话从连接池中取,连接池中也没有则创建一个新的连接,然后再放入连接池,再将这个连接返回。
- OkHttp中连接池最大可以容纳5个空闲连接,五分钟空闲时间后将回收
2.5 CallServerInterceptor
CallServerInterceptor负责从服务器读取响应的数据,与服务器进行数据交换。
我们通过ConnectInterceptor已经连接到服务器了,接下来我们就是写入请求数据以及读出返回数据了。整个流程:
- 写入请求头
- 写入请求体
- 读取响应头
- 读取响应体
3、连接机制
-
TCP/IP 通信传输流
-
TCP 套接字编程
连接的创建是在StreamAllocation对象统筹下完成的,我们前面也说过它早在RetryAndFollowUpInterceptor就被创建了,StreamAllocation对象,主要用来管理两个关键角色:
- RealConnection:真正建立连接的对象,利用Socket建立连接。
- ConnectionPool:连接池,用来管理和复用连接。
在里初始化了一个StreamAllocation对象,我们说在这个StreamAllocation对象里初始化了一个Socket对象用来做连接,但是并没有
3.1 建立连接
我们在前面的ConnectInterceptor分析中已经说过,onnectInterceptor用来完成连接。而真正的连接在RealConnect中实现,连接由连接池ConnectPool来管理,连接池最多保持5个地址的连接keep-alive,每个keep-alive时长为5分钟,并有异步线程清理无效的连接。
主要由以下两个方法完成:
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
上述方法完成后会创建一个RealConnection对象,然后调用该方法的connect()方法建立连接,最终调用Java里的套接字Socket里的connect()方法。
3.2 连接池
我们知道在负责的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。
复用连接就需要对连接进行管理,这里就引入了连接池的概念。
Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。
3.2.1 OKHttp如何复用TCP连接?
ConnectInterceptor
中查找连接的代码会最终会调用到ExchangeFinder.findConnection
方法,具体如下:
# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
// 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
releasedConnection = transmitter.connection;
result = transmitter.connection;
if (result == null) {
// 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
result = transmitter.connection;
}
}
}
synchronized (connectionPool) {
if (newRouteSelection) {
//3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
// 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
synchronized (connectionPool) {
// 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
//意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果获取到,就关闭我们创建里的连接,返回获取的连接
result = transmitter.connection;
} else {
//最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
connectionPool.put(result);
}
}
return result;
}
上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接
- 1、首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)
- 2、若没有 已分配的可用连接,就尝试从连接池中匹配获取。因为此时没有路由信息,所以匹配条件:address一致——host、port、代理等一致,且匹配的连接可以接受新的请求。
- 3、若从连接池没有获取到,则传入routes再次尝试获取,这主要是针对Http2.0的一个操作,Http2.0可以复用square.com与square.ca的连接
- 4、若第二次也没有获取到,就创建RealConnection实例,进行TCP + TLS握手,与服务端建立连接。
- 5、此时为了确保Http2.0连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。
- 6、第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。
以上就是连接拦截器尝试复用连接的操作,流程图如下:
3.2.2、OKHttp空闲连接如何清除?
上面说到我们会建立一个TCP连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp是如何做到的呢?
# RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}
long cleanup(long now) {
int inUseConnectionCount = 0;//正在使用的连接数
int idleConnectionCount = 0;//空闲连接数
RealConnection longestIdleConnection = null;//空闲时间最长的连接
long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间
//遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//若连接正在使用,continue,正在使用连接数+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空闲连接数+1
idleConnectionCount++;
// 赋值最长的空闲时间和对应连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//连接没有空闲的,就5分钟后再尝试清理.
return keepAliveDurationNs;
} else {
// 没有连接,不清理
cleanupRunning = false;
return -1;
}
}
//关闭移除的连接
closeQuietly(longestIdleConnection.socket());
//关闭移除后 立刻 进行下一次的 尝试清理
return 0;
}
1、在将连接加入连接池时就会启动定时任务
2、有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。
3、没有空闲连接就等5分钟后再尝试清理。
4、没有连接不清理。
4、OKHttp有哪些优点?
1、使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
2、扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求
3、功能强大,支持Spdy、Http1.X、Http2、以及WebSocket等多种协议
4、通过连接池复用底层TCP(Socket),减少请求延时
5、无缝的支持GZIP减少数据流量
6、支持数据缓存,减少重复的网络请求
7、支持请求失败自动重试主机的其他ip,自动重定向
5、OKHttp框架中用到了哪些设计模式?
1、构建者模式:OkHttpClient与Request的构建都用到了构建者模式,
- 主要用处是将对象的创建与表示相分离,用Builder组装各项配置。
2、外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
3、责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置
4、享元模式: 享元模式的核心即池中复用,OKHttp复用TCP连接时用到了连接池,同时在异步请求中也用到了线程池。
5、工厂模式工厂模式和建造者模式类似,区别就在于工厂模式侧重点在于对象的生成过程,而建造者模式主要是侧重对象的各个参数配置。
- 例子有CacheInterceptor拦截器中又个CacheStrategy对象
6、观察者模式,Okhttp中websocket的使用,由于webSocket属于长连接,所以需要进行监听,这里是用到了观察者模式
5、总结
OKhttp是一个网络请求开源项目,Android网络请求轻量级框架,支持文件上传与下载,支持https等功能。使用过程如下:
(1)、当我们通过OkhttpClient创立一个Call,并发起同步或者异步请求时;
(2)、okhttp会通过Dispatcher对我们所有的RealCall(Call的具体实现类)进行统一管理,并通过execute()及enqueue()方法对同步或者异步请求进行解决,异步需要加入双队列,利用线程池调度;
(3)、execute()及enqueue()这两个方法会最终调用RealCall中的getResponseWithInterceptorChain()方法,从阻拦器链中获取返回结果;
(4)、阻拦器链中,依次通过RetryAndFollowUpInterceptor(重定向阻拦器)、BridgeInterceptor(桥接阻拦器)、CacheInterceptor(缓存阻拦器)、ConnectInterceptor(连接阻拦器)、CallServerInterceptor(网络阻拦器)对请求依次请求重试,缓存处理,与服务的建立连接后,获取返回数据,再经过上述阻拦器依次解决后,最后将结果返回给调用方。
6、常见问题
6.1、OkHttpClient有几种发起请求的方式,有何不同?
有两种,同步请求和异步请求。
- 1、同步请求将请求(任务)加入到调度器(Dispatcher)的runningSyncCalls(正在执行)队列中,然后直接调用了getResponseWithInterceptorChain
- 2、异步请求将请求加入到调度器(Dispatcher)中,经历两个阶段:readyAsyncCalls、runningAsyncCalls,之后调用
getResponseWithInterceptorChain。- 因为正在执行的任务未超过最大限制
64
,同时同一Host
的请求不超过5
个
- 因为正在执行的任务未超过最大限制
6.2、OkHttp中的Dispatcher调度器是什么,是怎么实现调度的
调度器就是对任务执行的调度,Dispatcher
中维护了三个队列,分别是:
- 一个准备异步执行队列readyAsyncCalls,
- 两个正在执行的队列(异步正在执行队列:runningAsyncCalls,同步正在执行队列:runningSyncCalls)
在每个任务执行完成最后都会执行dispatch.finish()方法,之后会重新执行到promoteAndExecute()方法
,意为:促进和执行。只要队列中有任务,就会一直重复执行
。
同时,调度器中定义了两个并发执行数的变量,分别对并发做限制,最大同时执行数为64,同主机执行数为5。
调度器构造方法中定义了一个 ExecutorService
线程池变量,利用线程池对任务进行执行操作。
6.3、OkHttp中的线程池是怎么实现的?
OkHttp的调度器中初始化了一个线程池,
- 线程池
没有核心线程
,最大支持线程为Int.MAX_VALUE(无界线程池
),同时定义了60秒空闲时间,超过60秒的空线程将会回收。 - 并且通过ThreadFactory生成的线程定义了线程名称。
- 其工作队列为SynchronousQueue同步队列。
- 当一个任务到来时,会加入到同步队列中,如果有空闲线程则直接从同步队列中取出,在空闲线程中进行处理
- 当没有空闲线程时,则会先创建一个新的线程再接受任务进行执行。
- OkHttp线程池设计为核心线程为0是因为客户端可能在一段时间内不会有网络请求,为了避免浪费不必要的线程内存,所以不保留最低线程
- 同时最大线程设置为Int.MAX_VALUE为了防止同一时间有大量的请求进入,造成部分请求被抛弃的问题,设置60秒为线程空闲最大时间,在一段时间不使用的情况进行线程回收。
6.4、OkHttp有哪些拦截器,作用分别是什么?
6.5、缓存拦截器
缓存拦截器主要做了以下几个事情:
- 1、通过缓存策略(CacheStrategy)获取缓存
- 2、判断是否可以使用网络,如果不允许使用网络时:缓存为空,则直接返回504,缓存不为空直接返回缓存
- 3、可以使用网络时,调用后续的拦截器继续网络请求,获取网络请求结果
- 4、请求结果下,如果有缓存比,比对code是否为304,如果是则说明缓存还可以使用,直接返回缓存,且更新缓存
- 5、如果缓存不可用了,则新增/更新缓存,返回最新网络请求结果。
这里会有几个问题:
- (1)缓存是怎么存储和获取的?
- OkHttp的缓存通过Request的url作为key进行存储,使用
DiskLruCache
算法对缓存进行存储。
- OkHttp的缓存通过Request的url作为key进行存储,使用
- (2)每次请求都会去存储和获取缓存吗?
- (3)缓存策略(CacheStrategy)到底是怎么处理网络和缓存的?networkRequest什么时候为空?
(2)每次请求都会去存储和获取缓存吗?
这里需要看一下cache是否为空?cache变量为空的话不会有缓存,所以这个变量什么时候初始化的?
上图说明,cache应该是在创建OkHttpClient时创建的:
val client = OkHttpClient.Builder().cache(Cache(cacheDir, 10 * 1024 * 1024))
第二个问题应该是:当开发者设置了缓存以后才会去存储和获取缓存,并且缓存大小有限制。
(3)缓存策略(CacheStrategy)到底是怎么处理网络和缓存的?networkRequest什么时候为空?
class CacheStrategy internal constructor(
val networkRequest: Request?,
val cacheResponse: Response?
)
fun compute(): CacheStrategy {
val candidate = computeCandidate()
return candidate
}
private fun computeCandidate(): CacheStrategy {
//没有缓存情况下,返回空缓存
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
//...
//缓存控制不是 no-cache,且未过期
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
val builder = cacheResponse.newBuilder()
return CacheStrategy(null, builder.build())
}
return CacheStrategy(conditionalRequest, cacheResponse)
}
在这个缓存策略生存的过程中,只有一种情况下会返回缓存,也就是缓存控制不是no-cache,并且缓存没过期情况下,就返回缓存,然后设置networkRequest为空。
6.6、为什么需要连接池?怎么实现的?
我们知道在负责的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive
连接对于降低延迟和提升速度有非常重要的作用。
keepalive机制是什么呢?也就是可以在一次TCP连接中可以持续发送多份数据而不会断开连接。所以连接的多次使用,也就是复用就变得格外重要了,而复用连接就需要对连接进行管理,于是就有了连接池的概念。
- OkHttp中使用ConectionPool实现连接池,默认支持5个并发KeepAlive,默认链路生命为5分钟。
怎么实现的?
1)首先,ConectionPool中维护了一个双端队列Deque
,也就是两端都可以进出的队列,用来存储连接。
2)然后在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
也就是遍历了双端队列,如果连接有效,就会调用acquire方法计数并返回这个连接。
3)如果没找到可用连接,就会创建新连接,并会把这个建立的连接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了ConectionPool的put方法。
public final class ConnectionPool {
void put(RealConnection connection) {
if (!cleanupRunning) {
//没有连接的时候调用
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
4)其实这个线程池中只有一个线程,是用来清理连接的,也就是上述的cleanupRunnable
- 这个runnable会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,然后进入wait等待。
- 当如果空闲连接
maxIdleConnections
超过5个
或者keepalive
时间大于5分钟
,则将该连接清理掉。
5)这里有个问题,怎样属于空闲连接?
其实就是有关刚才说到的一个方法acquire计数方法:
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
在RealConnection
中,有一个StreamAllocation虚引用列表allocations
。每创建一个连接,就会把连接对应的StreamAllocationReference
添加进该列表中,如果连接关闭以后就将该对象移除。
总结:
连接池的工作就这么多,并不复杂,主要就是管理双端队列Deque,可以用的连接就直接用,然后定期清理连接,同时通过对StreamAllocation的引用计数实现自动回收。
参考
1、Android开源框架源码鉴赏:Okhttp
2、【知识点】OkHttp 原理 8 连问
3、Android 进阶之探索 OkHttp 原理
4、OkHttp原理解析之面试题分析
5、谈谈OKHttp的几道面试题
6、Android面试Android进阶(十七)-OkHttp相关问题