深入理解OKhttp

671 阅读5分钟

整体流程

使用OKhttp进行网络请求的的起始点是构造OkHttpClient与Request的实例,接着构建一个call并调用同步或者异步执行的方法。在OKhttp的内部会接着执行应用拦截器、重定向拦截器、桥接拦截器、缓存拦截器、连接拦截器、网络拦截器、执行拦截器,最后就是等待服务端的请求返回。

image.png

调度器

在okhttp3/Dispatcher.kt这个类中,调度器作用是维护任务队列与线程池并完成请求调配,内部有如下三个队列,内部的核心逻辑也是主要围绕着这三个队列。

同步任务队列:runningSyncCalls

异步任务等待队列:readyAsyncCalls

异步任务队列:runningAsyncCalls

同步任务会被add到runningSyncCalls中,接着便开始调getResponseWithInterceptorChain方法同步获取请求结果,最后在finish执行完成的任务,clean掉runningSyncCalls。对于同步任务来说,任务在当前线程同步执行,调度器没有实质作用,只是进行记录而已。

异步任务会先被添加到readyAsyncCalls中,接着遍历readyAsyncCalls,满足条件后则将call加入到runningAsyncCalls中执行。最终调用executorService.execute,回调AsyncCall的RUN方法,在run方法里执行getResponseWithInterceptorChain,将请求结果Callback回调回去,回调完成后清理异步任务队列。

promoteAndExecute是Dispatcher中一个比较关键的方法,主要有三个作用:

  1. readyAsyncCalls任务移交runningAsyncCalls;
  2. 线程池执行任务;
  3. 返回是否是执行任务状态。 promoteAndExecute关键代码如下:
...
synchronized(this) {
  val i = readyAsyncCalls.iterator()
  while (i.hasNext()) {
    val asyncCall = i.next()

    if (runningAsyncCalls.size >= this.maxRequests) break // 队列中超过阈值(64)终止
    if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // 超过并发执行阈值(5)终止
    
    i.remove()
    asyncCall.callsPerHost.incrementAndGet() // 并发执行数自增
    executableCalls.add(asyncCall)
    runningAsyncCalls.add(asyncCall)   // 切换队列
  }
  isRunning = runningCallsCount() > 0
}

for (i in 0 until executableCalls.size) {
  val asyncCall = executableCalls[i]
  // 线程池执行任务
  asyncCall.executeOn(executorService)
}
...

两个重点

任务队列内部是用链表实现的双端队列的原因是与readyAsyncCalls向runningAsyncCalls转换有关,当执行完一个请求或调用enqueue方法入队新的请求时,会对readyAsyncCalls进行一次遍历,将那些符合条件的等待请求转移到runningAsyncCalls队列中并交给线程池执行。尽管二者都能完成这项任务,但是由于链表的数据结构致使元素离散的分布在内存的各个位置,CPU缓存无法带来太多的便利,另外在垃圾回收时,使用数组结构的效率要优于链表。

OKHttp线程池阻塞队列用的SynchronousQueue,它的特点是不存储数据,当添加一个元素时,必须等待一个消费线程取出它,否则一直阻塞,如果当前有空闲线程则直接在这个空闲线程执行,如果没有则新启动一个线程执行任务。通常用于需要快速响应任务的场景,在网络请求要求低延迟的大背景下比较合适。 

 

拦截器

OkHttp将整个请求的复杂逻辑切成了一个一个的独立模块并命名为拦截器(Interceptor),通过责任链的设计模式串联到了一起,最终完成请求获取响应结果。

名称作用
Interceptors应用拦截器拿到原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor重试与重定向拦截器,处理错误重试和重定向。
BridgeInterceptor桥接拦截器主要工作是为请求添加cookie、设置其他报头,如User-Agent,Host,Keep-alive等。设置gzip压缩,并在接收到内容后进行解压。
CacheInterceptor缓存拦截器主要是使用缓存与设置缓存
connectInterceptor连接拦截器内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
NetworkInterceptors网络拦截器属于用户自定义拦截器,通常用于监控网络层的数据传输。
CalllServerInterceptor请求拦截器,在前置准备工作完成后,真正发起了网络请求。

RetryAndFollowUpInterceptor

server收到一个client请求后,发现请求的这个资源实际放在另一个位置,于是server在返回的响应头的Location字段中写入那个请求资源的正确的URL,并设置reponse的状态码为30x,端上收到重定向的响应后,会在重定向拦截器里用新的URL重新发起请求。假如一个请求在 RetryAndFollowUpInterceptor 这个拦截器内部重试或者重定向了 N 次,那么其内部嵌套的所有拦截器也会被调用N次。 重拾与重定向拦截器的逻辑如下: image.png

CacheInterceptor

HTTP 304 未改变说明无需再次传输请求的内容,也就是说可以使用缓存的内容。 OKhttp默认仅支持GET请求缓存,实际上也只有GET请求的缓存比较有实际意义。 缓存拦截器的执行流程如下: image.png

ConnectInterceptor

从源码上看ConnectInterceptor这个类十分简洁,核心的实现全部都在下边这行代码,方法内部实现了DNS解析、Sokect建连等逻辑

realChain.call.initExchange(chain)

主要的时序如下: image.png

连接复用

image.png

两个关键判断

  1. 当前连接是否可用?

(1) 当前RealCall的已经建立的的连接的host、port是否与正在发起链接的一致。
(2) 已存在的连接可以复用,如果这个连接出现一些异常、在链接池中被移除等就不能直接复用了。

  1. 当前链接池中的连接是否可用?

(1) 当前的连接的最大并发数不能达到上限,否则不能复用
(2) 两个连接的address的Host、port不相同,不能复用
(3) 1、2通过后,url的host相同则可以复用
(4) 如果3中url的host不相同,可以通过合并连接实现复用
(5) 但首先这个连接需要是HTTP/2
(6) 不能是代理
(7) IP的address要相同
(8) 这个连接的服务器证书必须覆盖新的主机
(9) 证书将必须匹配主机

总结

本文从OKhttp的请求出发,讲叙了OKhttp中调度器和拦截器这两个重点,通过流程图分析了RetryAndFollowUpInterceptor、CacheInterceptor,通过connectInterceptor执行的时序图、connectInterceptor链接复用流程图剖析了整体结构与连接复用的原理。

参考

github.com/square/okht…

juejin.cn/post/684490…

juejin.cn/post/684490…