嗨,我是哈利迪~《看完不忘系列》将以从树干到细枝
的思路分析一些技术框架,本文将对开源项目okhttp
网络库进行介绍。
本文约3800字,阅读大约10分钟。如个别大图模糊,可前往个人站点阅读。
概览
源码基于3.14.9,即java版本的最新版
首先上职责图,各个类的名字基本可以见名知意了,就不翻译了,直接起飞~
树干
我们先看一趟飞行的大体流程,
好了,进入代码环节,引入依赖,
1 | implementation 'com.squareup.okhttp3:okhttp:3.14.9' |
简单使用(只分析异步请求,同步请求类似),
1 | class OkhttpActivity extends AppCompatActivity { |
OkHttpClient
和Request
使用构建者模式创建即可,当然,如果OkHttpClient
不需要进行配置,直接new就行。知道了起点和终点,就可以创建航班Call
了,
1 | //OkHttpClient.java |
可见Call
的实例是RealCall
,航班创建好后,进入就绪跑道,
1 | //RealCall.java |
AsyncCall
就是一个Runnable,run方法里调了execute方法,
1 | //AsyncCall.java |
AsyncCall
里有一个原子计数器,
1 | //目前每个主机(域名)有多少个会话call |
Dispatcher
里有两个默认max值,
1 | int maxRequests = 64; //最多同时请求数为64 |
什么意思呢?可以这么理解,机场的调度中心,限制了同时最多起飞的航班为64班;飞往同一个城市的航班,同时最多只能有5班,为什么做城市限制?跟连接池的复用有关,后面会讲。下面我们以上海为例,
看下enqueue方法做了啥,
1 | //Dispatcher.java |
跟进promoteAndExecute,
1 | //Dispatcher.java |
其中executorService()返回了一个线程池,
1 | //Dispatcher.java |
核心线程数为0,空闲了60秒后,所有线程会被清空;最大线程数无限制,其实还好,已经有调度中心Dispatcher
会限制请求数了。
继续跟进executeOn方法,
1 | //AsyncCall.java |
可见,回调都在子线程里完成,所以Activity里要切回主线程才能操作UI。至此,核心流程就结束了。
细枝
拦截器链
前边得到Response
的地方,调了getResponseWithInterceptorChain,进去看看,
1 | //RealCall.java |
拦截器链基于责任链模式
,即不同的拦截器有不同的职责,链上的拦截器会按顺序挨个处理,在Request
发出之前,Response
返回之前,插入一些定制逻辑,这样可以方便的扩展需求。当然责任链模式
也有不足,就是只要一个环节阻塞住了,就会拖慢整体运行(效率);同时链条越长,产生的中间对象就越多(内存)。
我们先跟proceed方法,
1 | //RealInterceptorChain.java |
下面简要分析下各个拦截器的功能。
一、RetryAndFollowUpInterceptor
:
负责重试和重定向。
1 | //最大重试次数 |
其中followUpRequest方法会根据Response
不同的响应码做相应的处理,就不跟了。
二、BridgeInterceptor
:
桥接,负责把应用请求转换成网络请求,把网络响应转换成应用响应,说白了就是处理一些网络需要的header,简化应用层逻辑。
1 | Response intercept(Chain chain) throws IOException { |
这里需要注意的一点是,在服务器支持gzip压缩的前提下,客户端不设置Accept-Encoding=gzip的话,okhttp
会自动帮我们开启gzip和解压数据,如果客户端自己开启了gzip,就需要自己解压服务器返回的数据了。
三、CacheInterceptor
:
负责管理缓存,使用okio读写缓存。
1 | InternalCache cache; |
关于缓存策略CacheStrategy
会在缓存
章节展开。
四、ConnectInterceptor
:
负责创建连接Connection
。
1 | Response intercept(Chain chain) throws IOException { |
newExchange方法会在连接池
章节展开。
五、CallServerInterceptor
:
负责写请求和读响应。
1 | Response intercept(Chain chain) throws IOException { |
缓存
缓存的实现是基于请求和响应的header来做的。CacheStrategy
即缓存策略,CacheInterceptor
拦截器会根据他拿到网络请求networkRequest、缓存响应cacheResponse,从而决定是使用网络还是缓存。
1 | //CacheStrategy.java |
getCandidate里面就是根据header字段得到各种策略,然后交给拦截器处理,感兴趣的读者自行阅读啦。
那么缓存是如何写入磁盘的呢?跟进InternalCache
接口,他的实现在Cache
类里,
1 | //Cache.java |
okhttp的DiskLruCache
,就是根据最近最少使用算法,来管理磁盘缓存,他和Glide里的DiskLruCache
有几份相似,比如日志处理都一样,内部都有一个线程池来清理磁盘,不过okhttp有用到okio。感兴趣的读者可以留意下okhttp3.internal.cache.DiskLruCache
和com.bumptech.glide.disklrucache.DiskLruCache
。
注:缓存默认是关闭的,需要自行开启:
1 | new OkHttpClient.Builder() |
连接池
还记得Transmitter
吗,前面我们叫他机长,他是应用和网络之间的桥梁,管理着连接、请求、响应和流。在拦截器
章节知道:
RetryAndFollowUpInterceptor
里调了transmitter.prepareToConnect;准备一个连接
ConnectInterceptor
里调了transmitter.newExchange;创建一个交换器
这里补充几个概念:
Connection,实现为RealConnection:连接,抽象概念,内部维护了Socket
ConnectionPool,持有RealConnectionPool:连接池,管理连接的复用
Exchange:交换器(管理请求和响应、持有ExchangeCodec)
ExchangeCodec:编解码器,用于编码请求,解码响应,实现有Http1ExchangeCodec和Http2ExchangeCodec
HTTP 1.1:引入keep-alive机制,支持连接保活,可以多个请求复用一个连接,但请求是串行的
HTTP 2.0:支持多路复用,一个连接的多个请求可以并行
先看RealConnectionPool
,
1 | //RealConnectionPool.java |
RealConnection
代码有点多,知道他内部维护了Socket就行了。
前面提到过,同一主机的同时请求数被限制成maxRequestsPerHost = 5 ,为什么这么做?同主机的请求可以共用一个连接,所以大概是为了限流?比如同时飞往上海的航班如果不限数量,会把上海机场挤爆?有知道答案的小伙伴留下评论呀~
小结
okhhttp
具有以下优势:
- 使用简单,拦截器链的设计方便扩展
- 请求失败能自动重连和尝试主机的其他ip、能重定向
- 可以自动处理gzip
- 本地缓存可以避免重复请求
- 同主机的请求可以共享一个Socket,socket由Connection维护,ConnectionPool管理Connection的复用,避免频繁地创建和销毁连接
尾声
还是那句话,该系列旨在摸清技术的整体实现思路,okhhttp
里还有很多精彩细节,如cookie、route、dns、tls等处理,本文没有提到,大家还是要对着源码学习呀。哈迪在看源码过程还发现了很多不懂的地方,比如各种协议和标准,这也是个补充网络知识的好机会,一起飞~
系列文章: