一、请求过程
首先来看一个最简单的Http
请求是如何发送的。
val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url("https://www.google.com/")
.build()
okHttpClient.newCall(request).enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
}
})
这段代码看起来比较简单,OkHttp
请求过程中最少只需要接触OkHttpClient
、Request
、Call
、 Response
,但是框架内部会进行大量的逻辑处理。——外观模式。
所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。 关于分发器与拦截器,这里先简单介绍下,后续会有更加详细的讲解。
- 分发器:内部维护队列与线程池,完成请求调配;
- 拦截器:五大默认拦截器完成整个请求过程。
对应的UML图大概是这样:
整个网络请求过程大致如上所示
- 通过建造者模式构建
OKHttpClient
与Request
。 OKHttpClient
通过newCall
发起一个新的请求。- 通过分发器维护请求队列与线程池,完成请求调配。
- 通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作。
- 得到网络请求结果。
二、分发器工作流程
发器的主要作用是:维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表, 等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求。
而这里同步请求和异步请求又略有不同。
同步请求
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);
}
}
当正在执行的任务未超过最大限制64,同时同一Host
的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。
每个任务完成后,都会调用分发器的finished
方法,这里面会取出等待队列中的任务继续执行。
三、拦截器工作流程
经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了
# RealCall
override fun execute(): Response {
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}
我们再来看下RealCall
的execute
方法,可以看出,最后返回了getResponseWithInterceptorChain
,责任链的构建与处理其实就是在这个方法里面。
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
val chain = RealInterceptorChain(
call = this,interceptors = interceptors,index = 0
)
val response = chain.proceed(originalRequest)
}
如上所示,构建了一个OkHttp
拦截器的责任链。
用户自定义应用拦截器和5大默认拦截器的责任链添加的顺序及作用如下表所示:
拦截器 | 作用 |
---|---|
应用拦截器 | 拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。 |
RetryAndFollowUpInterceptor | 处理错误重试和重定向 |
BridgeInterceptor | 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。 |
CacheInterceptor | 缓存拦截器,如果命中缓存则不会发起网络请求。 |
ConnectInterceptor | 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。 |
CallServerInterceptor | 请求拦截器,在前置准备工作完成后,真正发起了网络请求。 |
我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptor
的intercept
方法,此方法会将网络响应的结果封装成一个Response
对象并return
。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain
方法的返回,如下图所示:
应用拦截器和网络拦截器有什么区别?
细心的同学可能发现了在
getResponseWithInterceptorChain()
中还有个networkInterceptors
,那么它是什么呢?okhttp除了让用户扩展自定义应用拦截器外,还提供了网络拦截器,它也是自定义拦截器,通常用于监控网络层的数据传输。
从整个责任链路来看,
应用拦截器是最先执行的拦截器,由于应用拦截器在
RetryAndFollowUpInterceptor
和CacheInterceptor
之前,所以一旦发生错误重试或者网络重定向,应用拦截器永远只会触发一次。通常用于统计客户端的网络请求发起情况。例如日志打印、添加请求头、token校验等。
网络拦截器位于
ConnectInterceptor
和CallServerInterceptor
之间,此时网络链路已经准备好,只等待发送请求数据。一般每次网络请求都会调用一次。但是如果在CacheInterceptor
中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。通常可用于统计网络链路上传输的数据,监控数据传输等。
四、缓存原理
OKHttp内部使用Okio来实现缓存文件的读写。
大致的流程如下:
- 第一次响应根据头信息决定是否需要缓存。
- 再次请求判断是否存在本地缓存,是否需要使用对比本地缓存。
- 如果缓存失效或者对比本地缓存,则发出网络请求,否则使用本地缓存。
通过OkHttpClient设置缓存是全局状态的,如果我们想对某个特定的request使用或禁用缓存,可以通过CacheControl相关的API实现:
//禁用缓存
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder().noCache().build())
.url("http://publicobject.com/helloworld.txt")
.build();
最后需要注意的一点是,OKHttp默认只支持get请求的缓存。
# okhttp3.Cache.java
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
...
//缓存仅支持GET请求
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
//对于vary头的值为*的情况,统一不缓存
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
...
}
五、复用连接池
private final ConnectionPool connectionPool;
里面有个复用连接池,其实就是类似于线程池。
构造方法中的参数:
最大连接数默认为5个、保活时间为5分钟
大致流程:
- 判断当前的连接是否可以使用:流是否已经被关闭,并且已经被限制创建新的流;
- 如果当前的连接无法使用,就从连接池中获取一个连接;
- 连接池中也没有发现可用的连接,创建一个新的连接,并进行握手,然后将其放到连接池中。
六、Okhttp对于网络请求做了哪些优化,如何实现的?
-
通过缓存策略减少重复的网络请求。
-
通过复用连接池来减少请求延时(有5分钟保活的长连接)。
七、OKHttp中用到了哪些设计模式?
- 建造者模式:
OkHttpClient
与Request
的构建都用到了构建者模式。 - 外观模式:
OkHttp
使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient
统一暴露出来。 - 责任链模式:
OKHttp
的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置。