OkHttp 网络请求

714 阅读9分钟

总结

  1. 连接可以复用,可以理解为重用 Socket
  2. 重试或重定向时如果 scheme,host,port 相同是可重用连接
  3. okhttp 有两种拦截器,第一种在所有拦截器前调用,第二种在建立连接后(即 tcp+https 握手完成后)调用。第二种又被称为网络拦截器
    • 因此,网络拦截器可以拿到请求的 http 协议等相关信息,因为它是在连接建立以后调用
  4. 可以在 OkHttpClient 中设置连接使用的 socketFactory 和 sslSocketFactory 以及 dns
  5. http 的缓存只支持 GET 请求
  6. 链接的清除:
    • 通过类似引用计数的方式判断 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() 完成。这里看一下它主要的属性:

image.png

类的初始化

真正网络请求在 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 构造函数中,构造函数如下:

iShot2021-09-06 16.42.58.png


void put(RealConnection connection) {
  // 开启清理工作。后面说
  if (!cleanupRunning) {
    cleanupRunning = true;
    executor.execute(cleanupRunnable);
  }
  // 简单的将传入的参数记录焉
  connections.add(connection);
}

清理

存时会使用另一线程池执行清理工作

iShot2021-09-06 16.53.30.png

上面调用的 cleanup 逻辑就非常简单,它的返回值表示空闲最久的连接已空闲的时间,截最重要的如下:

iShot2021-09-06 17.04.58.png

cleanup 每一次只清除空闲最久的连接,结合 cleanupRunnable() 中的 while(true)就可以清除掉连接池中所有的超出设定的连接

上面调用了 pruneAndGetAllocationCount() 方法,从这个方法可以看出 RealConnection 通过内部属性 allocations 记录下了它所有服务过的 StreamAllocation

iShot2021-09-06 17.15.32.png

取之前会判断连接能不能用于当前的请求

iShot2021-09-06 18.06.30.png

对于是否可复用,就需要根据 http 协议版本进行区分,我们知道只有 http2 才存在多路复用,才有可能复用

iShot2021-09-06 19.25 拷贝.jpg

get() 方法后又调用了 StreamAllocation.acquire(),这个就很简单

iShot2021-09-06 19.47.32.png

到这里,RealConnection 的存取都说完了,但要记住ConnectionPool#get 有可能返回 null

StreamAllocation

newStream() 为它关联连接,noNewStreams() 关闭它的连接

newStream

上面的分析只是分析完 ConnectionPool,算是走完 StreamAllocation 的创建。随着 ConnectInterceptor 的继续执行,下面到其 newStream 方法

iShot2021-09-06 20.10.58.png

findHealthyConnection 会死循环调用 findConnection。findConnection() 很复杂,分段看

iShot2021-09-06 20.56.29.png

上面列举了两种情况,我们知道 StreamAllocation 在 RetryAndFollowUpInterceptor.intercept 中创建的,因此每一次请求情况一基本上不可能成立,除非重定向或者重试时

在 RetryAndFollowUpInterceptor 的拦截方法中有一个死循环,里面有这样一段代码,这段代码表示:重试或重定向时如果 scheme,host,port 相同是可重用连接的,也即重用 Socket

image.png

再回到 findConnection() 中接着看,如果情况一、二已经找到连接了,就直接返回,整个方法结束。如果没有,走情况三、四

image.png

情况三表示的是缓存中的,情况四表示的是新建。出现情况三时,方法也就结束了;情况四时,就需要新建连接了

未标题-1 拷贝.png

到此为止,newStream 分析完了,它的核心作用就是 为当前 StreamAllocation 创建连接,即 RealConnecton 对象

ConnectInterceptor.intercept() 下面还有一步,它就直接返回 StreamAllocation 中的 codec,非常简单。

总结一下 newStream 的作用:

  1. 复用或创建连接。复用指从 ConnectionPool 中复用,或使用 StreamAllocation 已分配的连接;新建指在无法复用的情况下,新创建一个 RealConnection 对象
  2. 如果是新建连接,就会连接到服务器。这里主要是 Socket 连接,包括 tcp+https 握手过程
  3. 连接已建立,下面就是 CallServerInterceptor 拦截器开始往服务器发数据以及读服务器数据了

noNewStreams

这一步会关闭对应的的 Socket

image.png

RealConnection

connect 方法

到目前为止,所有的分析都是为 StreamAllocation 刚开一个 RealConnection 对象,这也是 ConnectInterceptor 的所有作用

在上面也说过,如果是新建的连接,会调用 RealConnection#connect() 方法,该方法会完成 tcp+https 握手过程:如下:

image.png

这里还处理了 http1.1 中添加的隧道,不太懂,不看。

然后看 connectSocket():

image.png

再看 establishProtocol()

image.png

上面会调用 startHttp2(),这个方法最重要的就是为 RealConnection#http2Connection 赋值,即创建一个 Http2Connection 对象。

其中的 coonectTls() 利用了 SSLSocket 完成 https 的握手过程:

image.png

上面代码中有个 if 判断,主要是验证是否可使用得到的证书。这里面涉及到 x509 证书知识(主要是证书上中的 Subject Alternative Name 字段),感兴趣可以看看 OkHostnameVerifier 中的相关方法。if 判断最终也是到这个类中相关的方法。

socketFactory

在 connect() 创建 Socket() 时使用了工厂模式,工厂的来源是 Address 中相应的属性,而 Address 的创建在 RetryAndFollowUpInterceptor 中

image.png

上面三个值都来自于 client,也就是说可以通过 OkHttpClient.Builder 指定:

image.png

CallServerInterceptor

上面的所有分析都是建立连接,现在连接已经建立,下面剩余的就是使用连接发送请求接收数据了。这一部分在 CallServerInterceptor 中。代码较长,分段看

image.png

注意上面的 Codec,它分为 Http1Codec 以及 Http2Codec,它包含有 Socket 的输入输出流,后面就是使用该类往服务器读写数据:

image.png

经上面两步,请求数据全部发送完成,下面就是读返回结果,以及处理一些情况:

image.png

在 http 中 204,205 都应该没有响应体的,所以最后一个判断会处理一些异常情况。

HttpCodec

到目前为止,只有真正读写数据的部分没看。这部分就是 HttpCodec 的实现。它根据 http 的版本分为 http1Codec 与 http2Codec 两个实现。

前者就比较简单,直接使用输入输出流读写即可。后者会先将数据变成二进制帧,然后读写,这一块分析不动。

链接复用

复用

链接的复用主要涉及到 StreamAllocation 与 ConnectionPool 两个类,主要涉及的方法是 StreamAllocation#findConnection() 与 ConnectionPool#put() 与 get() 两个方法。

  1. StreamAllocation 本身使用 connection 记录是否已有链接,如果有直接使用,结束
  2. 尝试将 route 设置为 null,从 pool 中获取 connection;获取到,结束
  3. 尝试进行一次路由选择,然后再从 pool 中获取;获取到,结束
  4. 新建一个 RealConnection,并赋值给 StreamAllocation#connection 属性,同时缓存到 pool 中
  5. 如果是 http2 即存在多路复用时,会再次尝试从 pool 中获取,如果获取到就会放弃新建的 connection

回收

回收的整个调用流程如下:

image.png

最后的 connectionBecameIdle 如下,总结一下就是:如果需要缓存连接,则 socket 先不关闭,否则直接关闭

image.png

有缓存就必然需要对链接进行清理,主要是 RealConnection#cleanup() 方法,该方法上面分析过。它会调用 pruneAndGetAllocationCount() 判断当前 connection 是否还在被使用。

connection 主要是被 StreamAllocation 引用,因此只要判断有没有 StreamAllocation 引用 connection 就可以知道 connection 是否还有用。connection 内部使用通过 allocations 属性存储所有引用它的 StreamAllocation 的弱引用。这种方式有点类似于引用计数

image.png

总结一下:

  1. 通过类似引用计数的方式判断 connection 是否还在被使用
  2. 通过类似 Lru 方式决定回收哪个 connection。如果需要被回收(有太多空闲 connection 或者空闲太久),就会将空闲最久的 connection 回收掉