前言
半年前阅读了 Volley 源码,但是现在主流网络请求都是使用 OkHttp + Retrofit + RxJava 甚至 Android 中 HttpUrlConnection 的具体实现都被替换成了 OkHttp,因此打算好好研究下 OkHttp 的源码,本文做为阅读笔记。
注:最近发现 OkHttp 使用 Kotlin 语言进行重写了,于是又重新阅读了一遍源码加深下理解,同时学习下 OkHttp 中 Kotlin 的使用方式,本文基于 v4.9.0 版本 官方仓库地址
简单使用
这里只例举基本的同步及异步 Get 请求,详细的请看 官方文档。
val client = OkHttpClient() |
由上可知,发送一个基本的 get 请求需要如下几步:
- 创建
OkHttpClient
实例。 - 创建
Request
实例。 - 创建
Call
实例。 - 执行
Call.execute()
或者Call.enqueue()
。
下面按照这四步探索下源码实现。
OkHttpClient 实例的创建
OkHttpClient
实例的创建主要有以下两种方式:
val client1 = OkHttpClient() |
一种通过调用无参构造器,另一种通过 OkHttpClient.Builder
类构造。当不需要自定义 OkHttpClient
配置的时候可以采用第一种,如果需要自定义配置那么必须采用第二种。接着分别看看上述两种方式的源码实现。
open class OkHttpClient internal constructor( |
可以看到 OkHttpClient
提供了一个主构造器以及一个次构造器,虽然主构造器访问修饰为 internal
没法直接调用,但是上述两种创建 OkHttpClient
实例的方式其实都是调用了主构造器。主构造器对成员变量进行赋值以及执行 init
代码块,代码过多就不展开了,无非就是从 OkHttpClient.Builder
中拷贝对应的参数赋值给其成员变量。下面关注下 OkHttpClient.Builder
的构造器,看看默认的参数。
class Builder constructor() { |
注:我感觉如果使用 Kotlin 写 OkHttp 就可以去除建造者模式,改为使用默认参数的方式,不过为了考虑兼容 Java 还是得用建造者模式。
可以看到 OkHttpClient.Builder
同样提供了一个主构造器和一个次构造器,次构造器用于拷贝 OkHttpClient
,主构造器执行成员变量的赋值,相关变量的作用及默认值已经做了简单说明。接着来看看第二步 Request
实例的创建。
Request 实例的创建
Request
实例通过 Request.Builder
进行创建的,因此首先看看 Request.Builder
的构造器。
open class Builder { |
可以看到 Request.Builder
同样提供了一个主构造器和一个次构造器,次构造器用于拷贝 Request
,主构造器内部设置了默认请求方法为 GET,并且创建了一个 Headers.Builder
实例用于统一管理请求头。接着看看其 url
方法和 build
方法。
open fun url(url: String): Builder { |
注:Kotlin 中如果方法需要返回当前类对象直接使用 apply
方法包裹方法体。
其中 url
方法主要是将请求地址封装成 HttpUrl
实例并赋值给成员 url
,build
方法创建了 Request
实例。至此第二步结束了接着看看第三步 Call
实例的创建。
Call 实例的创建
通过调用 OkHttpClient 实例的 newCall 方法创建 Call 实例。
override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false) |
newCall
方法创建并返回了一个 RealCall
实例,forWebSocket
用于区分是否是 WebSocket 握手请求,这里为 false。至此第三步也结束了看看最后一步 call.execute
以及 call.enqueue
方法是如何进行网络请求的。
Call.execute 和 Call.enqueue
通过上文可知 Call
的具体实现为 RealCall,首先看看相对而言比较简单的同步请求方法 execute。
// RealCall.kt |
方法内部执行顺序为:
- 校验该
RealCall
是否已经执行过,如果已经执行过就会抛出异常。 - 回调
EventListener.onStart
方法。 - 执行
Dispatcher.executed
方法,仅仅只把当前RealCall
实例放入runningSyncCalls
队列中。 - 执行
getResponseWithInterceptorChain
方法。
继续跟踪 getResponseWithInterceptorChain
方法
|
方法内部执行顺序为:
- 拼接所有的
Interceptor
,包括 OkHttp 内置的RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
、ConnectInterceptor
、CallServerInterceptor
以及用户设置的interceptors
、networkInterceptors
,其中networkInterceptors
主要用于网络调试,因为其可以获取到服务端返回的原始数据,可用代码库有HttpLoggingInterceptor
、StethoInterceptor
。 - 创建
RealInterceptorChain
实例,并调用其proceed
方法,获取到Response
实例返回给外界。
继续跟踪 proceed
方法,看看其是如何获取到响应的。
override fun proceed(request: Request): Response { |
proceed
方法实现了 OkHttp 拦截器的链式调用,执行顺序为:
- 校验索引是否越界,如果越界抛出异常。
- 校验
networkInterceptors
是否修改了主机及端口号。注:exchange
会在ConnectInterceptor
中赋值。 - 校验
networkInterceptors
是否没调用proceed
方法或者调用了多次。 - 拷贝
RealChainInterceptor
实例,并将其 index 属性加 1。注:必须拷贝的原因是可以进行重试 - 获取指定
index
处的Interceptor
实例,执行其intercept
方法,将拷贝后的RealChainInterceptor
实例传入。 - 校验每个拦截器返回的响应体是否为空。
- 将调用
intercept
方法获取到的响应进行返回。
根据上述逻辑,默认 index
为 0,因此会首先执行 RetryAndFollowInterceptor.intercept
方法,并将 index
为 1 的 RealChainInterceptor
实例传入。
RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor
主要负责失败重试,以及重定向。
class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor { |
大致流程是在一个死循环中,调用传入的 RealChainInterceptor
实例的 proceed
方法获取到响应,如果在获取响应图中发生了异常判断是否需要进行重试,需要则进行下次循环重试,如果成功获取到响应,判断响应码是否为 301、302 等重定向响应码,如是则取出 Location
响应头,将其值当做下次请求的 url,如果响应码为 401,那么执行 Authenticator.authenticate
方法获取新的 Request 实例。
这里也不是真正请求网络的地方,其也是通过 RealChainInterceptor
实例的 proceed
方法获取的响应,不过该 RealChainInterceptor
的 index 字段已经是 1 了,也就是执行索引为 1 的 Interceptor
,也就是 BridgeInterceptor
。
BridgeInterceptor
BridgeInterceptor 做为应用程序代码和网络代码之间的桥梁,其添加了若干个请求头,并会对网络响应进行 gzip 解压。
class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor { |
大体上就是添加了一些常用的请求头,Host、Connection、User-Agent、Accept-Encoding、Cookie、Content-Type、Content-Length ,然后处理了下 gzip 压缩。
这里也不是真正请求网络的地方,其也是通过 RealChainInterceptor
实例的 proceed
方法获取的响应,不过该 RealChainInterceptor
的 index 字段已经是 2 了,也就是执行索引为 2 的 Interceptor
,也就是 CacheInterceptor
。
CacheInterceptor
CacheInterceptor
用于从缓存中获取响应和写响应到缓存。
class CacheInterceptor(internal val cache: Cache?) : Interceptor { |
大体上就是从 Cache
中取出保存的响应,然后根据请求和缓存的响应判断缓存是否命中,命中就会直接构建一个新的响应返回,如果没命中(由于响应过期),则会根据缓存响应的 ETag(对应请求头 if-none-match)、LastModify(对应请求头 if-modified-since) 等响应头去构造当前的请求头,这样当服务器判断资源没变化时可以直接返回 304,框架也只需要更新下缓存的响应头就可以直接返回了。
默认配置的 OkHttpClient
不带任何缓存,但是其提供了一个 Cache
类,如果需要缓存可以进行如下配置
val client = OkHttpClient.Builder().cache(Cache(cacheFile, 50 * 1000)).build() |
这里也不是真正请求网络的地方,其也是通过 RealChainInterceptor
实例的 proceed
方法获取的响应,不过该 RealChainInterceptor
的 index 字段已经是 3 了,也就是执行索引为 3 的 Interceptor
,也就是 ConnectIntercept
。
ConnectIntercept
ConnectIntercept
用于与目标主机创建 TCP 连接,并且进行必要的握手,这也是 OkHttp 最难的地方。
object ConnectInterceptor : Interceptor { |
方法内部主要是调用了 RealCall.initExchange
方法,获取到一个 Exchange
实例,来跟踪下 initExchange
。
internal fun initExchange(chain: RealInterceptorChain): Exchange { |
方法内部会通过调用 ExchangeFinder.find
获取到一个 ExchangeCodec
实例,然后在用它构建一个 Exchange
实例,这里涉及到以下几个类
ExchangeFinder
用于寻找连接,在RetryAndFollowUpInterceptor
中创建实例。ExchangeCodec
用于生成和解析报文。拥有两个子类Http1ExchangeCodec
、Http2ExchangeCodec
。Exchange
用于传输单个 HTTP 请求和响应,内部借助ExchangeCodec
生成和解析报文。
fun find( |
方法内部通过调用 findHealthyConnection
寻找可用连接,然后调用 newCodec
返回一个 ExchangeCodec
实例。首先看看 findHealthyConnection
private fun findHealthyConnection( |
方法内部是个死循环,又通过调用 findConnection
获取到连接,然后检查是否可用,可用就返回,如果不可用,判断是否有路由还没有尝试,如果全尝试过了就抛出异常,如果还有待尝试的则继续循环。继续跟踪 findConnection
private fun findConnection( |
如果请求不是重试或者重定向那么 Call.connection
为 null
,那么尝试从连接池中获取一个连接,先看看 callAcquirePooledConnection
是如何获取的。
// RealConnectionPool.kt |
遍历所有连接,由于首次 requireMultiplexed
(是否需要多路复用) 为 false,因此执行 RealConnection.isEligible
判断连接是否可用
- 如果可用那么执行
RealCall.acquireConnectionNoEvents
将该连接赋值给RealCall.connection
字段并返回 true - 如果所有连接都不可用那么返回 false
接下来看看到底是如何判断连接是否可用的
// RealConnection.kt |
根据代码一个有用的连接需要满足以下几个条件:
- 请求数没有达到上限,Http1 为 1,Http2 为 4。
- 连接还可以被使用,还没有关闭。
- 本次请求的端口号、支持协议、代理、代理选择器、连接规格(密钥算法套件 + TLS 版本)、主机名校验器等需要与连接的一致。注:使用同一个 OkHttpClient 进行请求也只有主机名和端口号会不同。
- 本次请求的主机名与连接的目标主机名一致。
对于 Http1 连接必须同时满足上述 4 个条件才能进行复用,对于 Http2 主机名可以不同,但是需要额外满足以下几个条件:
- 本次请求的所有路由中包括该连接的路由(有相同的 IP 地址和端口号)。
- 该连接上次请求是 Https 请求,并且当前请求的主机名必须在上次请求的服务端证书中。
- 通过 CertificatePinner 校验。
下面继续分析 RealConnectionPool.findConnection 的剩余代码
private fun findConnection(...): RealConnection { |
首次进入时 nextRouteToTry
、routeSelection
都为 false,因此首先会去创建 RouteSelector
实例,然后从中获取到 RouteSelection
实例,再从中获取到路由列表。然后再次执行 callAcquirePooledConnection
传入路由列表获取连接,如果还是获取不到自己创建一个 RealConnection
并进行连接,连接成功后再次执行 callAcquirePooledConnection
获取连接,目的是尽量少的维护连接,假设 A、B 两个 Http2 请求同时发送,且这两个请求目标主机名端口号都一样,A 先创建了连接,将连接放入了连接池,B 也创建了连接,这时候从连接池获取到了 A 创建的连接,那么就会抛弃 B 自己创建的连接,并将当前成功连接的路由赋值给 nextRouteTry
,方便下次重试使用。如果获取不到,将本次创建的连接放入连接池然后返回。
三次调用 callAcquirePooledConnection
的目的是:
- 获取 Http1 连接
- 获取 Http1、Http2 连接
- 获取 Http2 连接
看到这里,应该还存在以下两个问题:
- RouteSelector、RouteSelector.Selection、Route 这三者之间是什么关系,都是做什么用的?
- RealConnection 是如何与目标服务器建立 TCP 连接,并进行握手?
首先分析第一个问题:
RouteSelector 构造器会调用 resetNextProxy
用来初始化代理列表
// RouteSelector.kt |
如果没有设置 Proxy
,那么所有的代理由 ProxySelector
决定。其默认实现为 DefaultProxySelector
,如果没有为系统设置代理,那么返回一个类型为直连的 Proxy
实例,否则返回配置的系统代理。
这里又提出两个疑问
- 系统代理是在哪设置的?
- 如何才能绕过代理?
对于第一个疑问,应用程序进程启动后,就会通过反射调用 ActivityThread.main
方法,而在该方法中又会调用 attach
方法进而跨进程调用 ActivityManagerService.attachApplication
而在其内部又会跨进度调用 bindApplication
以及 scheduleLaunchActivity
,这两个方法都只是发送了一个消息就返回了,等到 ActivityManagerService
执行完毕后,客户端进程执行到 Looper.loop
后,就开始执行第一个消息调用 handleBindApplication
方法,内部有如下代码:
// ActivityThread.java |
在 getProxyForNetwork
中系统会读取网络配置中的代理设置,这个就不展开了。直接看一看 setHttpProxySystemProperty
方法时如何设置代理的。
// Proxy.java |
方法内部逻辑非常简单,解析代理信息,设置或清除以下六个系统属性:
- http.proxyHost http请求代理主机
- https.proxyHost https请求代理主机
- http.proxyPort http请求代理端口
- https.proxyPort https请求代理端口
- http.nonProxyHosts http请求不进行代理的主机列表
- https.nonProxyHosts https请求不进行代理的主机列表
如果存在 PAC 脚本那么设置默认代理选择为 PacProxySelector
,如果不存在那么设置 DefaultProxySelector
注:PAC 脚本其实就是一段 JS,指定 URL 对应的代理。
默认代理选择器会读取上述几个参数,如果能找到就组装成一个 Proxy
实例返回,如果找不到那么返回 Proxy.NO_PROXY
。至此第一个疑问解决了,再来看看如何绕过代理。其实绕过代理很简单,只需要为 OkHttpClient
设置 Proxy
,代码如下。
val client = OkHttpClient.Builder() |
继续回到 RouteSelector
,通过调用其 next
方法获取到 RouteSelection
实例,看看具体实现:
// RouteSelector.kt |
方法内部首先会获取当前索引处的代理,然后访问 DNS 服务器获取该代理地址对应的 IP 地址列表,并将其放入 inetSocketAddresses
成员变量中,接着为每一个 inetSocketAddress
创建一个 Route
实例,如果该路由已经在数据库的失败路由中,那么将其放入 postponedRoutes
列表中,否则将其放入 routes
列表中。当该代理的所有路由都处理完毕后,将 postponedRoutes
拼接到 routes
后面,然后构建一个 Selection
实例然后。
到这里为止 RouteSelector
、RouteSelector.Selection
、Route
这三者的作用已经很明了,
RouteSelector
获取每个代理对应的路由并以 Selection
的形式返回。
|
首先判断是否是 Http 请求(虽然 OkHttpClient
中 sslSocketFactory
不做特殊配置一定不为空,但是如果是 Http 请求 Address
中的 sslSocketFactory
就为空)
- 如果是 Http 请求,并且明文传输规格不在连接规格列表中或者应用不允许明文传输则抛出异常。
- 如果是 Https 请求,并且支持的协议列表中含有
Protocol.H2_PRIOR_KNOWLEDGE
(表示预先知道服务端支持明文 Http2 协议)则抛出异常。
最终调用 connectSocket
建立连接,建立完毕后,接着调用 establishProtocol
进行协议的处理。
private fun establishProtocol(...) { |
如果进行明文传输并且协议为 Http2,那么执行 startHttp2
启动。
如果不进行明文传输执行 connectTls
进行握手,如果协议是 Http2 接着执行 startHttp2
启动。
private fun startHttp2(pingIntervalMillis: Int) { |
大致上就是发送了 connectionPreface
以及 setting
,至于更详细的有待后续理解了 Http2 原理后再来补充。
private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) { |
- 创建 SSLSocket。
- 获取 ConnectionSpec。
- 配置 TLS 扩展信息。
- 进行握手这个过程会校验证书的合法性,但是没校验证书是否是本次请求期望的。
- 验证当前请求主机名是否在服务器证书备用名称列表中(Subject Alternative Name)。
CallServerInterceptor
接着再看看 CallServerInterceptor。
public final class CallServerInterceptor implements Interceptor { |
CallServerInterceptor 真正的进行了网络请求,会根据 Request 实例构建出 Http 请求,获取到 Http 响应后再构建出 HttpResponse,网络请求成功后会接着执行前几个 Interceptor 的剩余代码,这里就不看了。直接回到RealCall.execute。
public Response execute() throws IOException { |
可以看出当一次同步请求结束后,会将 RealCall 中队列中移除,然后启动正在等待的异步请求,如果没有异步请求会回调 IdleCallback 。接着看看异步请求过程。
Call.enqueue
enqueue 方法用于执行异步请求。
public void enqueue(Callback responseCallback) { |
这里都和 execute 一样只是最后调用了 Dispatcher 的 enqueue 方法,不过传入的是 AsyncCall 实例。
void enqueue(AsyncCall call) { |
如果调用了 enqueue 发送网络请求,那么最终会在线程池中执行 AsyncCall 的 execute 方法,其内部实现与同步执行基本类似,注意最后会在子线程中直接调用 onResponse ,因此我们不能在 onResponse 里面直接更新 UI 。我们可以写一个 WrapCall 将 Call 进行包装这样就能实现回调在主线程了,代码如下。
class WrapCall(private val call: Call) : Call by call { |
源码分析到这网络流程基本已经清晰,下面再来看看 OkHttp 的连接复用。
ConnectionPoll
首先需要连接复用需要设置请求头 Connection: Keep-Alive ,这个已经在 BridgeInterceptor 里面设置了,当然如果响应头 Connection: false ,那么连接还是不能复用,连接池的具体实现是 RealConnectionPool ,每次在 ConnectInterceptor 的 intercepte 方法都会尝试着先从连接池中取出一个连接,取不到满足条件的才会新建一个连接。
class RealConnectionPool( |
总结
不管是同步请求还是异步请求都是通过 Dispatcher 类进行分发,然后经过从上到下5个 Interceptor 才能发起请求,获取到响应后还会经过这5个拦截器然后才将结果返回到外界,典型的责任链模式与 Android 事件分发差不多 。因此 OkHttp 核心就是 Dispatcher、Interceptor。
Dispatcher:
内部维护了三个双端队列(同步请求队列、异步请求队列、异步准备队列)、一个线程池(同 CacheThreadPoll )。
- 执行同步调用时加入到同步请求队列,请求完毕后移除,然后看看异步准备队列是否为空,不为空就请求。
- 执行异步调用时判断是否达到最大请求数量,以及最大每个 Host 请求数量,如果都不满,那么加入异步请求队列,如果某个满了,那么加入异步准备队列,当执行完毕后异步准备队列是否为空,不为空就请求。
Interceptor:
- RetryAndFollowUpInterceptor 用于错误重试,以及重定向。
- BridgeInterceptor 用于添加请求头(User-Agent、Connection等等),收到响应的时候可能会进行 GZip 解压。
- CacheInterceptor 进行缓存管理,默认不带缓存,如果需要缓存可以给 OkHttpClient 设置 cache 属性,可以使用 OkHttp 内置的 Cache 类。
- ConnectInterceptor 进行连接,首先从连接池中取出可以复用的连接,取不到就新建一个然后通过InetAddress 获取到域名对于的IP地址,然后创建 Socket 与服务端进行连接,连接成功后如果是Https请求还会进行握手验证证书操作。
- CallServerInterceptor 用于真正的发起请求,从 Socket 获取的输出流写入请求数据,从输入流中读取到响应数据。