背景:
前面我们已经梳理过HTTP相关知识点了,但是作为Android开发,大多数时候并不会直接面向HTTP协议,而是面向OkHttp这个网络请求框架来做网络请求(当然了,现在可能更多的是面向Retrofit做网络开发,但Retrofit说到底只是个工具,内部还是OKHttp完成的网络请求)。这也是为啥OkHttp变成了面试必问的原因,这里我们就简单理一下OkHttp这个框架的相关知识。
一、OkHttp的组成
OkHttp框架一经面试就收到的热捧,这是因为它极大的简化了开发者完成网络开发的步骤。框架内部使用的大量的设计模式,但今天我们不说设计模式,我们先来说说它的主要组成。作为Android开发,OkHttpClient我想都不会陌生,因为我们的网络请求就得通过它来发出去。这也是整个OkHttp框架的入口。
通过源码很清晰的就能看到OkHttpClient的组成部分。其中被提及的最多的就是:分发器和拦截器两个成员。这也是我们将要说的重点。
- 分发器:负责管理和分发网络请求(Call)的核心
- 拦截器:负责网络请求的拦截、建立连接、拼包、发送/接收数据等任务
二、分发器都负责干啥
OkHttp中的分发器可以说是除了拦截器以外最关键的一个环节了。之所以有分发器的存在是因为我们一个APP会有无数的网络请求,且可能同一时间进行大量的网络请求。这就要求OkHttp能够支持大并发的网络请求需要,同时还要支持异步请求和同步请求。
public final class Dispatcher {
//异步请求同时存在的最大请求
private int maxRequests = 64;
//异步请求同一域名同时存在的最大请求
private int maxRequestsPerHost = 5;
//闲置任务(没有请求时可执行一些任务,由使用者设置)
private @Nullable Runnable idleCallback;
//异步请求使用的线程池
private @Nullable ExecutorService executorService;
//异步请求等待执行队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//异步请求正在执行队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步请求正在执行队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
.....
}
如上源码,分发器主要有一个线程池+三个等待队列组成。至于他的工作流程我就不去贴代码一步步扣了,有兴趣的自己去跟下代码把,这里就总结下它的工作流程:
同步请求:因为同步请求是不存在任何条件限制立马执行,不需要线程池,所以分发器只将其放入同步执行队列做一下记录,等任务执行完再移除。
异步请求:异步请求相对于同步来说要复杂一丢丢。
- 正在执行的异步任务小于最大同时请求数(64),且同一Host请求小于最大同一Host请求数(5)则将Call加入异步正在执行队列,并交给线程池立即执行。否则将Call加入异步等待队列。
- 不管是异步请求还是同步请求,任务执行完后都会执行finish操作,唯一的区别是异步请求执行finish操作时会轮询异步等待队列。
前面也说了OkHttp能够达到高并发的要求,那么问题来了,他是怎么做到的呢?关键点就在分发器的线程池上:
线程池中常用的有三种队列,我们需要简单了解下:
(1)SynchronousQueue:是一个没有容量,无缓冲,不存储元素的阻塞队列,是个典型的生产者消费者模型,会直接将任务交给消费者,必须等队列中添加的元素被消费后才能继续添加新的元素。这样结合线程池的工作原理就不难明白为啥选择它来支持高并发了,由此提交到这个线程池的任务会被立即执行。
(2) LinkedBlockingQueue:是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待,当队列满时,才会开启新的线程,立即执行新添加的任务,当线程数达到 maximumPoolSize 数量时,执行线程拒绝策略。每个线程完全独立于其他线程。
(3)ArrayBlockingQueue:是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会执行拒绝策略。
其中LinkedBlockingQueue和ArrayBlockingQueue类似,区别就是一个无界(Integer.MAX_VALUE大小),一个有界(需要指定大小)。
三、拦截器的工作
这里我们只讲一下OkHttp内部自带的五种拦截器,因为这五种拦截器组合完成了真正的Http网络请求。自带的五大拦截器将Request按责任链模式依序向下传递,Response结果再按责任链逆序向上传递返回。也就意味着第一个执行的拦截器最先接触到Request,最后接收到Response:
- 重试与重定向拦截器:RetryAndFollowUpInterceptor
(1)重试
在使用者不禁用重试的前提下,当出现某些异常(如:Socket超时异常、路由异常)时,且存在更多的路由线路,则会尝试换条线路进行请求的重试。但是这里要注意有几种异常不会进行重试:
- SSL证书异常(证书验证失败)
- SSL验证失败异常(通常是没有证书)
- 协议异常(未按照Http协议定义数据)
(2)重定向
如果请求结束,也没有发生异常并不代表当前获得的响应就是最终结果,而是要根据Response响应码再次判断是否需要重定向。状态码很多,这里就不啰里吧嗦介绍了,有兴趣自己去扣一下源码
我想强调的是,重定向是有次数限制的,最大次数为20次。
- 桥接拦截器:BridgeInterceptor
上面在说HTTP协议的时候已经讲过它的报文了,其中的Header的关键部分都是在这个拦截器中进行补全的。如:
请求头 | 说明 |
---|---|
Content-Type | 请求体类型,如:application/x-www-form-urlencoded |
Content-Length /Transfer-Encoding | 请求体解析方式 |
Host | 请求的主机站点 |
Connection: Keep-Alive | 保持长连接 |
Accept-Encoding: gzip | 接受响应支持gzip压缩 |
Cookie | cookie身份辨别 |
User-Agent | 请求的用户信息,如:操作系统、浏览器等 |
在补全请求头后交给下一个拦截器处理,在得到响应结果后还会做两件事:
- 保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的
CookieJar
不提供实现 - 如果使用gzip返回的数据,则使用
GzipSource
包装便于解析。
- 缓存拦截器:CacheInterceptor
Http的缓存机制前面在介绍Http协议的时候我们也详细讲解过它的工作流程了,在这里我也不再啰嗦它的策略机制了,有兴趣可以翻到前面去看看。缓存拦截器的作用就是在发出请求前,判断缓存是否命中。如果命中则可以不请求,直接使用缓存的响应。但是要注意OkHttp只会缓存GET请求的响应(至于为啥,我也不知道,哈哈)。
- 连接拦截器:ConnectInterceptor
既然要通信,那必然要先建立连接,连接拦截器的工作就是获取一个与目标服务器的连接。这里要稍微讲一下在Http2.0中它的多路复用逻辑:
- 首先在连接池中查找DNS、代理、SSL证书、服务器域名、端口等连接参数完全相同的连接
- 判断连接是否被占用或被断开
- 满足上述条件则复用连接,否则建立新连接
这里还要提一嘴的是,在连接拦截器将请求提交给请求服务器之间,还会判断用户是否设置了网络拦截器,如果设置了先执行用户的网络拦截器。
- 请求服务器连接器:CallServerInterceptor
连接建立后剩下的就是通信发送请求获取响应了。但是前面我们也说过HTTP的POST(带请求体)和GET请求区别是一个POST请求会发两个包,第一个包先用来确认服务器是否接收请求体,如果服务器响应Expect:100-continue就表示接收请求,继续发送剩余请求数据。最后获取响应数据并解析响应。
总结:在OkHttp的整个工作流程就像个工厂流水线,分发器负责下达分发任务,拦截器负责执行,只不过拦截器需要最少经过五道工序才能得到最终成果:
1、重试重定向拦截器在交出请求(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据异常情况以及响应码判断是否需要重试或重定向,如果满足条件那么就会重启执行所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie的接口并解析GZIP数据。
3、缓存拦截器顾名思义在交出之前读取并判断是否使用缓存;且获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流。
5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。