总结
连接可以复用,可以理解为重用 Socket重试或重定向时如果 scheme,host,port 相同是可重用连接okhttp 有两种拦截器,第一种在所有拦截器前调用,第二种在建立连接后(即 tcp+https 握手完成后)调用。第二种又被称为网络拦截器- 因此,网络拦截器可以拿到请求的 http 协议等相关信息,因为它是在连接建立以后调用
- 可以在 OkHttpClient 中设置连接使用的 socketFactory 和 sslSocketFactory 以及 dns
http 的缓存只支持 GET 请求- 链接的清除:
通过类似引用计数的方式判断 connection 是否还在被使用。connection 存储有所有引用它的 StreamAllocation 的弱引用,一旦所有 StreamAllocation 被回收后,就认为 connection 没有在使用通过类似 Lru 方式决定回收哪个 connection。如果需要被回收(有太多空闲 connection 或者空闲太久),就会将空闲最久的 connection 回收掉,即将它的 socket 关闭
拦截器
okhttp 发起网络请求时会涉及到多个类,这些类的初始化散落在各种拦截器中。OkHttp 各拦截器使用责任链模式串联,依次调用,然后依次处理返回结果。
OkHttpClient.Builder.addInterceptor
这是所有拦截器里面最先被调用的,可以在请求之后做一些公共操作,比如添加某些 header,解密服务端返回的数据等
RetryAndFollowUpInterceptor
处理重试以及重定向,总次数不能超过 20 次
BridgeInterceptor
为请求添加一些请求头以及 gzip 解压。比如添加有 Connection、Host、User-Agent 等头信息
CacheInterceptor
处理缓存。这个主要就是根据 http 协议对 get 请求进行存储、解析
ConnectInterceptor
创建连接,处理 tcp+https 握手过程。这一步执行完后,客户端就有了一个可以与服务端进行通信的链路,就可以直接将数据发往服务端
只有这一步以后,拦截器拦截方法中的 Chain.connection() 的返回结果才不为空
此拦截器执行完后表示和服务器已连接,下面就是 CallServerInterceptor 拦截器开始往服务器发数据以及读服务器数据
OkHttpClient.Builder.addNetworkInterceptor
网络请求前给使用者添加的拦截器。因为到这一步已经建立了连接,所以可以拿到请求版本号等各种信息。
CallServerInterceptor
真正发起网络请求。这一步主要用到了 HttpCodec 类
几个类
请求服务器时,至少有请求与连接,以及将请求写入连接/从服务器读取数据中的流。
在 OkHttp 请求用 Call 表示(子类为 RealCall,它包含我们设置的 Request),后者用 Connection 表示(具体子类为 RealConnection,它包括 Socket,握手信息等),流使用 HttpCodec 表示。为啥将 Codec 叫做流?因为 StreamAllocation#newStream() 返回的是 HttpCodec 对象,看名字就是个流。
一个连接可以承载多个流,也就是说一个 RealConnection 可以为多个 StreamAllocation 服务。它服务的 StreamAllocation 会记录在 RealConnection#allocations 中。这样做的目的是为了节省建立连接是握手时间(tcp+https 握手)
RealConnection: 连接。包含 Socket 以及握手信息等,它是对 Socket 的封装。拥有它,就相当于拥有了一个可与服务端通信的链路。连接在新建时会执行 tcp+https 的握手
ConnectionPool:okhttp 会对连接进行复用,该类就是负责管理连接
HttpCodec:流。用于往服务器写数据和读数据。它包含有 Socket 的输入、输出流,因此可以往服务器读写数据
StreamAllocation:协调 HttpCodec, Connection, Call 的桥梁。它的主要目标有两个:一个寻找到合适的连接,一个建立对应的流。前者通过 newStream() 完成,后者通过关联的 RealConenction#newCodec() 完成。这里看一下它主要的属性:
类的初始化
真正网络请求在 CallServerInterceptor 中
// CallServerInterceptor.java
// 这些是涉及到的类
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
上面涉及到 HttpCodec 与 RealConnection,这两个类在 ConnectInterceptor 中生成
// ConnectInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
// 获取已设置的 StreamAllocation
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
// 通过 StreamAllocation 生成 HttpCodec
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
// 获取 StreamAllocation 中设置过的 RealConnection
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
而 StreamAllocation 在 RetryAndFollowUpInterceptor 中添加
// RetryAndFollowUpInterceptor.java
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
各个类的初始化都找到了,现在从后往前看各个类的作用。
ConnectionPool
连接池。用于管理链接,即 RealConnection 对象,当符合条件时会进行复用,当超过最大时间时会清除,有点类似于线程池。
它的初始化在 OkHttpClient.Builder 构造函数中,构造函数如下:
存
void put(RealConnection connection) {
// 开启清理工作。后面说
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
// 简单的将传入的参数记录焉
connections.add(connection);
}
清理
存时会使用另一线程池执行清理工作
上面调用的 cleanup 逻辑就非常简单,它的返回值表示空闲最久的连接已空闲的时间,截最重要的如下:
cleanup 每一次只清除空闲最久的连接,结合 cleanupRunnable() 中的 while(true)就可以清除掉连接池中所有的超出设定的连接
上面调用了 pruneAndGetAllocationCount() 方法,从这个方法可以看出 RealConnection 通过内部属性 allocations 记录下了它所有服务过的 StreamAllocation
取
取之前会判断连接能不能用于当前的请求
对于是否可复用,就需要根据 http 协议版本进行区分,我们知道只有 http2 才存在多路复用,才有可能复用
get() 方法后又调用了 StreamAllocation.acquire(),这个就很简单
到这里,RealConnection 的存取都说完了,但要记住ConnectionPool#get 有可能返回 null
StreamAllocation
newStream() 为它关联连接,noNewStreams() 关闭它的连接
newStream
上面的分析只是分析完 ConnectionPool,算是走完 StreamAllocation 的创建。随着 ConnectInterceptor 的继续执行,下面到其 newStream 方法
findHealthyConnection 会死循环调用 findConnection。findConnection() 很复杂,分段看
上面列举了两种情况,我们知道 StreamAllocation 在 RetryAndFollowUpInterceptor.intercept 中创建的,因此每一次请求情况一基本上不可能成立,除非重定向或者重试时。
在 RetryAndFollowUpInterceptor 的拦截方法中有一个死循环,里面有这样一段代码,这段代码表示:重试或重定向时如果 scheme,host,port 相同是可重用连接的,也即重用 Socket
再回到 findConnection() 中接着看,如果情况一、二已经找到连接了,就直接返回,整个方法结束。如果没有,走情况三、四
情况三表示的是缓存中的,情况四表示的是新建。出现情况三时,方法也就结束了;情况四时,就需要新建连接了
到此为止,newStream 分析完了,它的核心作用就是 为当前 StreamAllocation 创建连接,即 RealConnecton 对象。
ConnectInterceptor.intercept() 下面还有一步,它就直接返回 StreamAllocation 中的 codec,非常简单。
总结一下 newStream 的作用:
- 复用或创建连接。复用指从 ConnectionPool 中复用,或使用 StreamAllocation 已分配的连接;新建指在无法复用的情况下,新创建一个 RealConnection 对象
- 如果是新建连接,就会连接到服务器。这里主要是 Socket 连接,包括 tcp+https 握手过程
- 在
- 连接已建立,下面就是 CallServerInterceptor 拦截器开始往服务器发数据以及读服务器数据了
noNewStreams
这一步会关闭对应的的 Socket
RealConnection
connect 方法
到目前为止,所有的分析都是为 StreamAllocation 刚开一个 RealConnection 对象,这也是 ConnectInterceptor 的所有作用
在上面也说过,如果是新建的连接,会调用 RealConnection#connect() 方法,该方法会完成 tcp+https 握手过程:如下:
这里还处理了 http1.1 中添加的隧道,不太懂,不看。
然后看 connectSocket():
再看 establishProtocol()
上面会调用 startHttp2(),这个方法最重要的就是为 RealConnection#http2Connection 赋值,即创建一个 Http2Connection 对象。
其中的 coonectTls() 利用了 SSLSocket 完成 https 的握手过程:
上面代码中有个 if 判断,主要是验证是否可使用得到的证书。这里面涉及到 x509 证书知识(主要是证书上中的 Subject Alternative Name 字段),感兴趣可以看看 OkHostnameVerifier 中的相关方法。if 判断最终也是到这个类中相关的方法。
socketFactory
在 connect() 创建 Socket() 时使用了工厂模式,工厂的来源是 Address 中相应的属性,而 Address 的创建在 RetryAndFollowUpInterceptor 中
上面三个值都来自于 client,也就是说可以通过 OkHttpClient.Builder 指定:
CallServerInterceptor
上面的所有分析都是建立连接,现在连接已经建立,下面剩余的就是使用连接发送请求接收数据了。这一部分在 CallServerInterceptor 中。代码较长,分段看
注意上面的 Codec,它分为 Http1Codec 以及 Http2Codec,它包含有 Socket 的输入输出流,后面就是使用该类往服务器读写数据:
经上面两步,请求数据全部发送完成,下面就是读返回结果,以及处理一些情况:
在 http 中 204,205 都应该没有响应体的,所以最后一个判断会处理一些异常情况。
HttpCodec
到目前为止,只有真正读写数据的部分没看。这部分就是 HttpCodec 的实现。它根据 http 的版本分为 http1Codec 与 http2Codec 两个实现。
前者就比较简单,直接使用输入输出流读写即可。后者会先将数据变成二进制帧,然后读写,这一块分析不动。
链接复用
复用
链接的复用主要涉及到 StreamAllocation 与 ConnectionPool 两个类,主要涉及的方法是 StreamAllocation#findConnection() 与 ConnectionPool#put() 与 get() 两个方法。
- StreamAllocation 本身使用 connection 记录是否已有链接,如果有直接使用,结束
- 尝试将 route 设置为 null,从 pool 中获取 connection;获取到,结束
- 尝试进行一次路由选择,然后再从 pool 中获取;获取到,结束
- 新建一个 RealConnection,并赋值给 StreamAllocation#connection 属性,同时缓存到 pool 中
- 如果是 http2 即存在多路复用时,会再次尝试从 pool 中获取,如果获取到就会放弃新建的 connection
回收
回收的整个调用流程如下:
最后的 connectionBecameIdle 如下,总结一下就是:如果需要缓存连接,则 socket 先不关闭,否则直接关闭
有缓存就必然需要对链接进行清理,主要是 RealConnection#cleanup() 方法,该方法上面分析过。它会调用 pruneAndGetAllocationCount() 判断当前 connection 是否还在被使用。
connection 主要是被 StreamAllocation 引用,因此只要判断有没有 StreamAllocation 引用 connection 就可以知道 connection 是否还有用。connection 内部使用通过 allocations 属性存储所有引用它的 StreamAllocation 的弱引用。这种方式有点类似于引用计数
总结一下:
通过类似引用计数的方式判断 connection 是否还在被使用通过类似 Lru 方式决定回收哪个 connection。如果需要被回收(有太多空闲 connection 或者空闲太久),就会将空闲最久的 connection 回收掉