okhttp
链接:https://juejin.cn/post/6887896333685161992
简述:
通过对外提供的OkHttpClient和Request的builder实现基础信息和必要信息的配置,直到封装构建成了RealCall对象(RealCall implement Call)并新建CallBack实例传入realCall.equeue(callback),才真正完成了请求实体的实例化。
之后realCall.enqueue(call)方法的调用才是实际上开始进行请求:先判断是否call已经执行过了(executed = AtomicBoolean()),若未执行则继续
之后由Dispatcher调用enqueue进行判断当前正在执行的请求数及当前网络请求的主机数是否超过了最大值。要是超过了最大值,就将请求放到等待队列中,要是没超过,就放到正在执行的队列中,然后调用线程池(默认单例初始化了一个缓存线程池(即无核心线程、无限线程池数量、SynchronousQueue
拦截器
RetryAndFollowUpInterceptor
处理错误重试和重定向
BridgeInterceptor
应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor
缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor
连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
networkInterceptors
用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor
请求拦截器,在前置准备工作完成后,真正发起了网络请求。
如
RetryAndFollowUpInterceptor是在一个死循环中进行最大次数为20(MAX_FOLLOW_UPS)的重试(超过抛ProtocolException(“Too many follow-up requests: $followUpCount”)),
每次重试根据不同的responseCode进行补偿【比如30x(除304外,304为协商缓存)进行重定向,503】,对于不必要的情况也会直接终止(HTTP_CLIENT_TIMEOUT 408)。
CacheInterceptor是当请求发起时,先检查是否命中缓存(强制缓存或协商缓存),无命中缓存或缓存失效,则将请求向下传递给下一个拦截器。
获得了响应数据后,检查响应头的Cache-Control,Expires,no-store不存储,immutable等判断是否写入缓存
用了 责任链设计模式 ,它将请求一层一层向下传,直到有一层能够得到Resposne就停止向下传递,并 Response 向上面的拦截器传递,然后各个拦截器会对 respone 进行一些处理,最后会传到 RealCall 类中通过 execute 来得到 Response 。
- 3.1.1 通过 OkHttpClient 实例化 初始化我们需要初始化的 http连接协议 代理地址 缓存控制 Cookie 证书连接 代理选择器等
- 3.1.2 通过 Request Builder模式 拿到我们传入的 url method Headers RequestBody 以及 tags
- 在 OkHttpClient 我们用RealCall接口回调执行Dispacher的 回调实现,入: equeqe(同步) ennque(异步请求) 其中 Dispacher 的同步请求 是 将我们的请求塞到同步队列里面,这个有可能会导致线程阻塞 ennque(异步请求) 是 生产者消费者模型 会将满足 小于并发64 同host请求小于4 的请求塞到我们的 运行队列里面 执行,不满足该条件放到 等待队列里面
然后交给线程池 去执行具体请求事宜 ,拿到响应后
- 3.1.3 我们 会将 响应 通过 getResourceChainPain 通过拦截器网下透传到 重定向拦截器, 桥拦截器 ,缓存拦截器 ,连接池拦截器,网络拦截器,请求拦截器 如果中间有被拦截,就进行一系列的拦截处理,追钟会返回我们客户端,我们客户端再通过 Response 去解析 code message Header ReponseBody 这些参数信息
代码解析:
一:构建realCall
1、2、3步其实都是基础信息和必要信息的配置,基本用构建者模式来完成,直到最后构建成了realCall,才真正完成了请求实体,之后realCall.enqueue方法的调用才是实际上开始为请求调度线程,开始通过责任链一节节的处理请求发起。
二:封装CallBack成AsyncCall,通过dispatcher.equeue调度
在call.enqueue(new Callback(){ … }) 执行之后,首先做的是
1. 调用RealCall.enqueue()方法,判断当前call是否已经被执行过了,被执行过了,就抛出异常,如果没有执行过,就先将callback封装成AsyncCall,然后调用dispatcher.enqueue()方法(dispatcher调度器,在okHttpClient里面创建的)
2. 在dispatcher.enqueue()方法中,判断当前正在执行的请求数及当前网络请求的主机数是否超过了最大值。要是超过了最大值,就将请求放到等待队列中,要是没超过,就放到正在执行的队列中,然后调用线程池执行它。
三 .realcall的异步请求的执行
通过getResponseWithInterceptorChain()责任链处理并最终发起请求接收处理回调
1 executorService.execute
吃过了午饭,继续分析源码,承接上文,我们分析到了,如果符合条件,就用线程池去执行它,也就是这句
1 | executorService().execute(call); |
看一下我们的线程池
1 | public synchronized ExecutorService executorService() { |
无核心线程、MAX_VALUE个最大线程、非核心线程闲置60秒回收。于dispatcher的enqueue中已经对请求数做了限定,故不会由太大的性能负担。
好,继续再看下线程池executorService.execute(call)方法 它会执行里面call方法的run()方法,也就是AsyncCall的run方法,实际上是调用了execute(),该方法由子类实现,也就是调用了AsyncCall.execute()
简单来说,就是executorService.execute(call) -> AsyncCall.run() -> AsyncCall.execute() 看到这个写法,内心中就想吐槽一句话,真鸡儿秀! 来继续看下,AsyncCall.execute()
2 AsyncCall.execute()
看源码 文件位置:realcall.java
1 |
|
- 执行拦截器链,返回Response
- 判断拦截器链中的重定向拦截器是否已经取消了,如果取消了,就执行responseCallback.onFailure() ,这个也就是我们在外边在第3步,传过来的回调方法Callback()中的onFailure()方法。
- 如果没取消,则走onResponse也就是Callback()中的onResponse()方法。返回结果。当然这里都是在子线程里面的。
- 这句其实就是调用了,dispatcher方法中的finished方法。下面我们看下
3 dispatcher.finished()
1 | void finished(RealCall call) { |
- 将该请求从正在执行的任务队列里面删除
- 调用promoteCalls() 调整请求队列
- 重新计算请求数量
4 dispatcher.promoteCalls()
1 | private void promoteCalls() { |
很简单,这里无非就是遍历等待队列中的请求,然后加入到执行请求队列中,直到并发数和当前网络请求的主机数达到上限。
至此,okhttp的异步已经分析完毕了
面试题 1.什么是dispatcher? dispatcher作用是为维护请求的状态,并维护一个线程池。用于执行请求。
小伙子,是不是很简单呀?是不是已经完了?你想多了,来分析最核心的一部分
细说 getResponseWithInterceptorChain()
1 |
|
拦截器 | 作用 |
---|---|
Interceptor应用拦截器 | 拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。 |
RetryAndFollowUpInterceptor | 处理错误重试和重定向 |
BridgeInterceptor | 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。 |
CacheInterceptor | 缓存拦截器,如果命中缓存则不会发起网络请求。 |
ConnectInterceptor | 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。 |
networkInterceptors(网络拦截器) | 用户自定义拦截器,通常用于监控网络层的数据传输。 |
CallServerInterceptor | 请求拦截器,在前置准备工作完成后,真正发起了网络请求。 |
Other
addInterceptor与addNetworkInterceptor的区别
二者通常的叫法为应用拦截器和网络拦截器,从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。
- 首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
- 其次,如上文提到除了CallServerInterceptor,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。
- 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。
网络缓存机制CacheInterceptor
这里的缓存是指基于Http网络协议的数据缓存策略,侧重点在客户端缓存,所以我们要先来复习一下Http协议如何根据请求和响应头来标识缓存的可用性。
提到缓存,就必须要聊聊缓存的有效性、有效期。
HTTP缓存原理
在HTTP 1.0时代,响应使用Expires头标识缓存的有效期,其值是一个绝对时间,比如Expires:Thu,31 Dec 2020 23:59:59 GMT。当客户端再次发出网络请求时可比较当前时间 和上次响应的expires时间进行比较,来决定是使用缓存还是发起新的请求。
使用Expires头最大的问题是它依赖客户端的本地时间,如果用户自己修改了本地时间,就会导致无法准确的判断缓存是否过期。
因此,从HTTP 1.1 开始使用Cache-Control头表示缓存状态,它的优先级高于Expires,常见的取值为下面的一个或多个。
- private,默认值,标识那些私有的业务逻辑数据,比如根据用户行为下发的推荐数据。该模式下网络链路中的代理服务器等节点不应该缓存这部分数据,因为没有实际意义。
- public 与private相反,public用于标识那些通用的业务数据,比如获取新闻列表,所有人看到的都是同一份数据,因此客户端、代理服务器都可以缓存。
- no-cache 可进行缓存,但在客户端使用缓存前必须要去服务端进行缓存资源有效性的验证,即下文的对比缓存部分,我们稍后介绍。
- max-age 表示缓存时长单位为秒,指一个时间段,比如一年,通常用于不经常变化的静态资源。
- no-store 任何节点禁止使用缓存。
强制缓存
在上述缓存头规约基础之上,强制缓存是指网络请求响应header标识了Expires或Cache-Control带了max-age信息,而此时客户端计算缓存并未过期,则可以直接使用本地缓存内容,而不用真正的发起一次网络请求。
协商缓存
强制缓存最大的问题是,一旦服务端资源有更新,直到缓存时间截止前,客户端无法获取到最新的资源(除非请求时手动添加no-store头),另外大部分情况下服务器的资源无法直接确定缓存失效时间,所以使用对比缓存更灵活一些。
使用Last-Modify / If-Modify-Since头实现协商缓存,具体方法是服务端响应头添加Last-Modify头标识资源的最后修改时间,单位为秒,当客户端再次发起请求时添加If-Modify-Since头并赋值为上次请求拿到的Last-Modify头的值。
服务端收到请求后自行判断缓存资源是否仍然有效,如果有效则返回状态码304同时body体为空,否则下发最新的资源数据。客户端如果发现状态码是304,则取出本地的缓存数据作为响应。
使用这套方案有一个问题,那就是资源文件使用最后修改时间有一定的局限性:
- Last-Modify单位为秒,如果某些文件在一秒内被修改则并不能准确的标识修改时间。
- 资源修改时间并不能作为资源是否修改的唯一依据,比如资源文件是Daily Build的,每天都会生成新的,但是其实际内容可能并未改变。
因此,HTTP 还提供了另外一组头信息来处理缓存,ETag/If-None-Match。流程与Last-Modify一样,只是把服务端响应的头变成Last-Modify,客户端发出的头变成If-None-Match。ETag是资源的唯一标识符 ,服务端资源变化一定会导致ETag变化。具体的生成方式有服务端控制,场景的影响因素包括,文件最终修改时间、文件大小、文件编号等等。
OKHttp的缓存实现
上面讲了这么多,实际上OKHttp就是将上述流程用代码实现了一下,即:
- 第一次拿到响应后根据头信息决定是否缓存。
- 下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
- 如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
OKHttp内部使用Okio来实现缓存文件的读写。
缓存文件分为CleanFiles和DirtyFiles,CleanFiles用于读,DirtyFiles用于写,他们都是数组,长度为2,表示两个文件,即缓存的请求头和请求体;同时记录了缓存的操作日志,记录在journalFile中。
开启缓存需要在OkHttpClient创建时设置一个Cache对象,并指定缓存目录和缓存大小,缓存系统内部使用LRU作为缓存的淘汰算法。
1 | ## Cache.kt |
OkHttp早期的版本有个一个InternalCache接口,支持自定义实现缓存,但到了4.x的版本后删减了InternalCache,Cache类又为final的,相当于关闭了扩展功能。
具体源码实现都在CacheInterceptor类中,大家可以自行查阅。
通过OkHttpClient设置缓存是全局状态的,如果我们想对某个特定的request使用或禁用缓存,可以通过CacheControl相关的API实现:
1 | //禁用缓存 |
OKHttp不支持的缓存情况
最后需要注意的一点是,OKHttp默认只支持get请求的缓存。
1 | # okhttp3.Cache.java |
这是当网络请求响应后,准备进行缓存时的逻辑代码,当返回null时表示不缓存。从代码注释中不难看出,我们从技术上可以缓存method为HEAD和部分POST请求,但实现起来的复杂性很高而收益甚微。这本质上是由各个method的使用场景决定的。
我们先来看看常见的method类型及其用途。
- GET 请求资源,参数都在URL中。
- HEAD 与GET基本一致,只不过其不返回消息体,通常用于速度或带宽优先的场景,比如检查资源有效性,可访问性等等。
- POST 提交表单,修改数据,参数在body中。
- PUT 与POST基本一致,最大不同为PUT是幂等的。
- DELETE 删除指定资源。
可以看到对于标准的RESTful请求,GET就是用来获取数据,最适合使用缓存,而对于数据的其他操作缓存意义不大或者根本不需要缓存。
也是基于此在仅支持GET请求的条件下,OKHTTP使用request URL作为缓存的key(当然还会经过一系列摘要算法)。
OkHttp的线程池
OkHttp中的线程池是定义在分发器中的,即定义在Dispatcher
1 | public synchronized ExecutorService executorService() { |
高并发,最大吞吐量。SynchronousQueue队列是无容量队列,
在OkHttp中,配置的线程池的核心线程数为0,最大线程数为Integer.MAX_VALUE,线程的存活时间为60s,采用的队列是SynchronousQueue。
- okhttp
默认同时支持 64 个异步请求(不考虑同步请求),一个 host 同时最多请求 5 个
- okhttp 内部的
线程池都是 CacheThreadPool
:核心线程数为 0,非核心线程数无限,永远添加不到等待队列中 okhttpClient 如果不单例,会出现 oom
:因为大量的 Dispatcher 对象,不同的对象会使用不同的线程去发起网络请求,从而导致线程过多,OOM