​OkHttp源码深度解析

点击关注“OPPO互联网技术”,阅读更多技术干货

OkHttp应该是目前Android平台上使用最为广泛的开源网络库了,Android 在6.0之后也将内部的HttpUrlConnection的默认实现替换成了OkHttp。

大部分开发的同学可能都有接触过OkHttp的源码,但很少有比较全面的阅读和了解的,目前网络上的大部分源码解析文章也都是点到为止,并且大段的贴源码,这种解析方式是我无法认可的,因此才有了想要重新写一篇解析OkHttp源码的想法。

这篇文章的目的,一个是要比较全面的介绍OkHttp源码,另一个是要尽量避免大段的贴出源码,涉及到源码的部分,会尽量通过调用关系图来展示。

本篇选用的OkHttp源码是目前最新的4.4.0版本,面向的读者也是有一定使用基础的Android开发同学。

OkHttp的源码可以从github上下载https://github.com/square/OkHttp

直奔主题,文章将从一下几个方面开始来拆解OkHttp的源码:

  1. 整体结构

  2. 拦截器

  3. 任务队列

  4. 连接复用和连接池

1. 从一个例子出发

首先来看一个最简单的Http请求是如何发送的。

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder().url("https://www.google.com").build();

Response response = client.newCall(request).execute();

return response.body().string();

这一段代码就是日常使用OkHttp最常见的用法,跟进源码后,可以得到一张更为详细的流程图,通过这张图来看下内部的逻辑是如何流动的。

​OkHttp源码深度解析

涉及到了几个核心类,我们一个个来看下。

  1. OkHttpClient

  2. Request 和 Response

  3. RealCall

OkHttpClient
:这个是整个OkHttp的核心管理类,所有的内部逻辑和对象归OkHttpClient统一来管理,它通过Builder构造器生成,构造参数和类成员很多,这里先不做具体的分析。

Request 和Response
:Request是我们发送请求封装类,内部有url, header , method,body等常见的参数,Response是请求的结果,包含code, message, header,body ;这两个类的定义是完全符合Http协议所定义的请求内容和响应内容。

RealCall
:负责请求的调度(同步的话走当前线程发送请求,异步的话则使用OkHttp内部的线程池进行);同时负责构造内部逻辑责任链,并执行责任链相关的逻辑,直到获取结果。虽然OkHttpClient是整个OkHttp的核心管理类,但是真正发出请求并且组织逻辑的是RealCall类,它同时肩负了调度和责任链组织的两大重任,接下来我们来着重分析下RealCall类的逻辑。

(RealCal类的源码地址:https://github.com/square/OkHttp/blob/master/OkHttp/src/main/java/OkHttp3/RealCall.kt)

RealCall类并不复杂,有两个最重要的方法,execute() 和 enqueue(),一个是处理同步请求,一个是处理异步请求。跟进enqueue的源码后发现,它只是通过异步线程和callback做了一个异步调用的封装,最终逻辑还是会调用到execute()这个方法,然后调用了getResponseWithInterceptorChain()获得请求结果。

看来是 getResponseWithInterceptorChain() 方法承载了整个请求的核心逻辑,那么只需要把这个方法分析清楚了,真个OkHttp的请求流程就大体搞明白了。既然这么重要的方法,还是不能免俗的贴下完整的源码。

val interceptors = mutableListOf<Interceptor>()

interceptors += client.interceptors

interceptors += RetryAndFollowUpInterceptor(client)

interceptors += BridgeInterceptor(client.cookieJar)

interceptors += CacheInterceptor(client.cache)

interceptors += ConnectInterceptor

if (!forWebSocket) {

interceptors += client.networkInterceptors

}

interceptors += CallServerInterceptor(forWebSocket)


val chain = RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this,

client.connectTimeoutMillis, client.readTimeoutMillis, client.writeTimeoutMillis)


var calledNoMoreExchanges = false

try {

val response = chain.proceed(originalRequest)

............

从源码可以看到,即使是 getResponseWithInterceptorChain() 方法的逻辑其实也很简单,它生成了一个Interceptors拦截器的List列表,按顺序依次将:

  • client.Interceptors

  • RetryAndFollowUpInterceptor,

  • BridgeInterceptor

  • CacheInterceptor

  • ConnectInterceptor

  • client.networkInterceptors

  • CallServerInterceptor

这些成员添加到这个List中,然后创建了一个叫RealInterceptorChain的类,最后的Response就是通过chain.proceed获取到的。

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

具体这些拦截器是如何串联,每个拦截器都有什么功能,下面的内容会作更详细的分析。

2. OkHttp的核心:拦截器

我们已经知道了OkHttp的核心逻辑就是一堆拦截器,那么它们是如何构造并关联到一起的呢?
这里就要来分析RealInt
erceptorChain这个类了。

通过前面的分析可知,RealCall将Interceptors一个一个添加到List之后 ,就构造生成了一个RealInterceptorChain对象,并调用chain.proceed获得响应结果。那么就来分析下chain.proceed这个方法到底干了啥。为了不让篇幅太长,这里就不贴出源码内容,仅给出分析后的结论,大家对照源码可以很快看懂。

(RealInterceptorChain的源码:https://github.com/square/OkHttp/blob/master/OkHttp/src/main/java/OkHttp3/internal/http/RealInterceptorChain.kt)

根据对RealInterceptorChain的源码解析,可得到如下示意图(省略了部分拦截器):

​OkHttp源码深度解析

​OkHttp源码深度解析

结合源码和该示意图,可以得到如下结论:

  1. 拦截器按照添加顺序依次执行

  2. 拦截器的执行从RealInterceptorChain.proceed()开始,进入到第一个拦截器的执行逻辑

  3. 每个拦截器在执行之前,会将剩余尚未执行的拦截器组成新的RealInterceptorChain

  4. 拦截器的逻辑被新的责任链调用next.proceed()切分为start、next.proceed、end这三个部分依次执行

  5. next.proceed() 所代表的其实就是剩余所有拦截器的执行逻辑

  6. 所有拦截器最终形成一个层层内嵌的嵌套结构

了解了上面拦截器的构造过程,我们再来一个个的分析每个拦截器的功能和作用。

从这张局部图来看,总共添加了五个拦截器(不包含自定义的拦截器如client.interceptors和client.networkInterceptors,这两个后面再解释)。

先来大概的了解下每一个拦截器的作用

  • retryAndFollowUpInterceptor——失败和重定向拦截器

  • BridgeInterceptor——封装request和response拦截器

  • CacheInterceptor——缓存相关的过滤器,负责读取缓存直接返回、更新缓存

  • ConnectInterceptor——连接服务,负责和服务器建立连接 这里才是真正的请求网络

  • CallServerInterceptor——执行流操作(写出请求体、获得响应数据) 负责向服务器发送请求数据、从服务器读取响应数据 进行http请求报文的封装与请求报文的解析

下面就来一个个的拦截器进行分析。

2.1 RetryAndFollowUpInterceptor

源码地址:https://github.com/square/OkHttp/blob/master/OkHttp/src/main/java/OkHttp3/internal/http/RetryAndFollowUpInterceptor.kt

根据源码的逻辑走向,直接画出对应的流程图(这段逻辑在RetryAndFollowUpInterceptor的intercept()方法内部):

​OkHttp源码深度解析

从上图中可以看出,RetryAndFollowUpInterceptor开启了一个while(true)的循环,并在循环内部完成两个重要的判定,如图中的蓝色方框:

  1. 当请求内部抛出异常时,判定是否需要重试

  2. 当响应结果是3xx重定向时,构建新的请求并发送请求

重试的逻辑相对复杂,有如下的判定逻辑(具体代码在RetryAndFollowUpInterceptor类的recover方法):

  • 规则1: client的retryOnConnectionFailure参数设置为false,不进行重试

  • 规则2: 请求的body已经发出,不进行重试

  • 规则3: 特殊的异常类型不进行重试(如ProtocolException,SSLHandshakeException等)

  • 规则4: 没有更多的route(包含proxy和inetaddress),不进行重试

前面这四条规则都不符合的条件下,则会重试当前请求。重定向的逻辑则相对简单,这里就不深入了。



2.2 Interceptors和NetworkInterceptors的区别


前面提到,在OkHttpClient.Builder的构造方法有两个参数,使用者可以通过addInterceptor 和 addNetworkdInterceptor 添加自定义的拦截器,分析完 RetryAndFollowUpInterceptor 我们就可以知道这两种自动拦截器的区别了。

从前面添加拦截器的顺序可以知道 Interceptors 和 networkInterceptors 刚好一个在 RetryAndFollowUpInterceptor 的前面,一个在后面。

结合前面的责任链调用图可以分析出来,假如一个请求在 RetryAndFollowUpInterceptor 这个拦截器内部重试或者重定向了 N 次,那么其内部嵌套的所有拦截器也会被调用N次,同样 networkInterceptors 自定义的拦截器也会被调用 N 次。而相对的 Interceptors 则一个请求只会调用一次,所以在OkHttp的内部也将其称之为 Application Interceptor。



2.3 BridgeInterceptor 和 CacheInterceptor


BridageInterceptor 拦截器的功能如下:

  1. 负责把用户构造的请求转换为发送到服务器的请求 、把服务器返回的响应转换为用户友好的响应,是从应用程序代码到网络代码的桥梁

  2. 设置内容长度,内容编码

  3. 设置gzip压缩,并在接收到内容后进行解压。省去了应用层处理数据解压的麻烦

  4. 添加cookie

  5. 设置其他报头,如User-Agent,Host,Keep-alive等。其中Keep-Alive是实现连接复用的必要步骤

CacheInterceptor 拦截器的逻辑流程如下:

  1. 通过Request尝试到Cache中拿缓存,当然前提是OkHttpClient中配置了缓存,默认是不支持的。

  1. 根据response,time,request创建一个缓存策略,用于判断怎样使用缓存。

  2. 如果缓存策略中设置禁止使用网络,并且缓存又为空,则构建一个Response直接返回,注意返回码=504

  3. 缓存策略中设置不使用网络,但是又缓存,直接返回缓存

  4. 接着走后续过滤器的流程,chain.proceed(networkRequest)

  5. 当缓存存在的时候,如果网络返回的Resposne为304,则使用缓存的Resposne。

  6. 构建网络请求的Resposne

  7. 当在OkHttpClient中配置了缓存,则将这个Resposne缓存起来。

  8. 缓存起来的步骤也是先缓存header,再缓存body。

  9. 返回Resposne

接下来的两个应该是所有内部拦截器里最重要的两个了,一个负责处理Dns和Socket连接,另一个则负责Http请求体的发送。



2.4 ConnectInterceptor


上面已经提到了,connectInterceptor应该是最重要的拦截器之一了,它同时负责了Dns解析和Socket连接(包括tls连接)。

源码地址:https://github.com/square/OkHttp/blob/master/OkHttp/src/main/java/OkHttp3/internal/connection/ConnectInterceptor.kt

这个类本身很简单,从源码来看,关键的代码只有一句。

val exchange = transmitter.newExchange(chain, doExtensiveHealthChecks)

从Transmitter获得了一个新的ExChange的对象,这句简单的代码仔细跟进去以后,会发现其实埋藏了非常多的逻辑,涉及整个网络连接建立的过程,其中包括dns过程和socket连接的过程,这里我们通过两个图来了解下整个网络连接的过程。

​OkHttp源码深度解析

先来看方法调用的时序图,梳理出关键步骤:

  1. ConnectInterceptor调用transmitter.newExchange

  2. Transmitter先调用ExchangeFinder的find()获得ExchangeCodec

  3. ExchangeFinder调用自身的findHealthConnectio获得RealConnection

  4. ExchangeFinder通过刚才获取的RealConnection的codec()方法获得ExchangeCodec

  5. Transmitter获取到了ExchangeCodec,然后new了一个ExChange,将刚才的ExchangeCodec包含在内

通过刚才的5步,最终Connectinterceptor通过Transmitter获取到了一个Exchange的类,这个类有两个实现,一个是Http1ExchangeCodec,一个是Http2Exchangecodec,分别对应的是Http1协议和Http2协议。

那么获取到Exchange类有什么用呢?再来看这几个类的关系图,如下:

​OkHttp源码深度解析

从上面可以看到,前面获得的Exchange类里面包含了ExchangeCodec对象,而这个对象里面又包含了一个RealConnection对象,RealConnection的属性成员有socket、handlShake、protocol等,可见它应该是一个Socket连接的包装类,而ExchangeCode对象是对RealConnection操作(writeRequestHeader、readResposneHeader)的封装。

通过这两个图可以很清晰的知道,最终获得的是一个已经建立连接的Socket对象,也就是说,在ConnectInterceptor内部已经完成了socket连接,那么具体是哪一步完成的呢?

看上面的时序图,可以知道,获得RealConnection的ExchangeFinder调用的findHealthConnection()方法,因此,socket连接的获取和建立都是在这里完成的。

同样,在socket进行连接之前,其实还有一个dns的过程,也是隐含在findHealthConnection 里的内部逻辑,详细的过程在后面DNS的过程再进行分析,这里ConnectionInterceptor的任务已经完成了。

另外还需要注意的一点是,在执行完ConnectInterceptor之后,其实添加了自定义的网络拦截器networkInterceptors,按照顺序执行的规定,所有的networkInterceptor执行执行,socket连接其实已经建立了,可以通过realChain拿到socket做一些事情了,这也就是为什么称之为network Interceptor的原因。



2.5 CallServerInterceptor


CalllServerInterceptor是最后一个拦截器了,前面的拦截器已经完成了socket连接和tls连接,那么这一步就是传输http的头部和body数据了。

CallServerInterceptor源码:https://github.com/square/OkHttp/blob/master/OkHttp/src/main/java/OkHttp3/internal/http/CallServerInterceptor.kt

CallServerInterceptor由以下步骤组成:

  1. 向服务器发送 request header

  2. 如果有 request body,就向服务器发送

  3. 读取 response header,先构造一个 Response 对象

  4. 如果有 response body,就在 3 的基础上加上 body 构造一个新的 Response 对象

这里我们可以看到,核心工作都由 HttpCodec 对象完成,而 HttpCodec 实际上利用的是 Okio,而 Okio 实际上还是用的 Socket,只不过一层套一层,层数有点多。

3. 整体架构

至此为止,所有的拦截器都讲完了,我们已经知道了一个完整的请求流程是如何发生的。那么这个时候再来看OkHttp的架构图就比较清晰了

​OkHttp源码深度解析

整个OkHttp的架构纵向来看就是五个内部拦截器,横向来看被切分成了几个部分,而纵向的拦截器就是通过对横向分层的调用来完成整个请求过程,从这两个方面来把握和理解OkHttp就比较全面了。

针对横向的部分将在接下来的部分进行详细分析

3.1 连接复用,DNS和Socket的连接

通过前面的分析知道,Socket连接和Dns过程都是在ConnecInterceptor中通过Transmitter和ExchangeFinder来完成的,而在前面的时序图中可以看到,最终建立Socket连接的方法是通过ExchangeFinder的findConnection来完成的,可以说一切秘密都是findConnection方法中。

因此接下来详细解析下findConnection(),这里贴出源码和注释

synchronized(connectionPool) {

//前面有一大段判定当前的conencection是否需要释放,先删除

.....


if (result == null) {

// 1, 第一次尝试从缓冲池里面获取RealConnection(Socket的包装类)

if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {

foundPooledConnection = true

result = transmitter.connection

} else if (nextRouteToTry != null) {

//2, 如果缓冲池中没有,则看看有没有下一个Route可以尝试,这里只有重试的情况会走进来

selectedRoute = nextRouteToTry

nextRouteToTry = null

} else if (retryCurrentRoute()) {

//3,如果已经设置了使用当前Route重试,那么会继续使用当前的Route

selectedRoute = transmitter.connection!!.route()

}

}

}

if (result != null) {

// 4,如果前面发现ConnectionPool或者transmiter中有可以复用的Connection,这里就直接返回了

return result!!

}


// 5, 如果前面没有获取到Connection,这里就需要通过routeSelector来获取到新的Route来进行Connection的建立

var newRouteSelection = false

if (selectedRoute == null && (routeSelection == null || !routeSelection!!.hasNext())) {

newRouteSelection = true

//6,获取route的过程其实就是DNS获取到域名IP的过程,这是一个阻塞的过程,会等待DNS结果返回

routeSelection = routeSelector.next()

}


var routes: List<Route>? = null

synchronized(connectionPool) {

if (newRouteSelection) {

// Now that we have a set of IP addresses, make another attempt at getting a connection from

// the pool. This could match due to connection coalescing.

routes = routeSelection!!.routes

//7,前面如果通过routeSelector拿到新的Route,其实就是相当于拿到一批新的IP,这里会再次尝试从ConnectionPool

// 中检查是否有可以复用的Connection

if (connectionPool.transmitterAcquirePooledConnection( address, transmitter, routes, false)) {

foundPooledConnection = true

result = transmitter.connection

}

}

if (!foundPooledConnection) {

if (selectedRoute == null) {

//8,前面我们拿到的是一批IP,这里通过routeSelection获取到其中一个IP,Route是proxy和InetAddress的包装类

selectedRoute = routeSelection!!.next()

}


// Create a connection and assign it to this allocation immediately. This makes it possible

// for an asynchronous cancel() to interrupt the handshake we're about to do.

//9,用新的route创建RealConnection,注意这里还没有尝试连接

result = RealConnection(connectionPool, selectedRoute!!)

connectingConnection = result

}

}


// If we found a pooled connection on the 2nd time around, we're done.

// 10,注释说得很清楚,如果第二次从connectionPool获取到Connection可以直接返回了

if (foundPooledConnection) {

eventListener.connectionAcquired(call, result!!)

return result!!

}


// Do TCP + TLS handshakes. This is a blocking operation.

//11,原文注释的很清楚,这里是进行TCP + TLS连接的地方

result!!.connect(

connectTimeout,

readTimeout,

writeTimeout,

pingIntervalMillis,

connectionRetryEnabled,

call,

eventListener

)

//后面一段是将连接成功的RealConnection放到ConnectionPool里面,这里就不贴出来了

}

return result!!

从上面的流程可以看到,findConnection这个方法做了以下几件事:

  1. 检查当前exchangeFinder所保存的Connection是否满足此次请求

  2. 检查当前连接池ConnectionPool中是否满足此次请求的Connection

  3. 检查当前RouteSelector列表中,是否还有可用Route(Route是proxy,IP地址的包装类),如果没有就发起DNS请求

  4. 通过DNS获取到新的Route之后,第二次从ConnectionPool查找有无可复用的Connection,否则就创建新的RealConnection

  5. 用RealConnection进行TCP和TLS连接,连接成功后保存到ConnectionPool

Connection的连接复用

可以看到,第二步和第四步对ConnectionPool做了两次复用检查,第五步创建了新的RealConnection之后就会写会到ConnectionPool中。

因此这里就是OkHttp的连接复用其实是通过ConnectionPool来实现的,前面的类图中也反映出来,ConnectionPool内部有一个connections的ArrayDeque对象就是用来保存缓存的连接池。

DNS过程

从前面解析的步骤可知,Dns的过程隐藏在了第三步RouteSelector检查中,整个过程在findConnection方法中写的比较散,可能不是特别好理解,但是只要搞明白了RouteSelector, RouteSelection,Route这三个类的关系,其实就比较容易理解了,如下图中展示了三个类之间的关系。

​OkHttp源码深度解析

从图中可以得到如下结论:

  • RouteSelector在调用next遍历在不同proxy情况下获得下一个Selection封装类,Selection持有一个Route的列表,也就是每个proxy都对应有Route列表

  • Selection其实就是针对List<Route>封装的一个迭代器,通过next()方法获得下一个Route,Route持有proxy、address和inetAddress,可以理解为Route就是针对IP和Proxy配对的一个封装

  • RouteSelector的next()方法内部调用了nextProxy(), nextProxy()又会调用resetNextInetSocketAddres()方法

  • resetNextInetSocketAddres通过address.dns.lookup获取InetSocketAddress,也就是IP地址

通过上面一系列流程知道,IP地址最终是通过address的dns获取到的,而这个dns又是怎么构建的呢?

反向追踪代码,定位到address的dns是transmitter在构建address的时候,将内置的client.dns传递进来,而client.dns是在OkHttpclient的构建过程中传递进来Dns.System,里面的lookup是通过InetAddress.getAllByName 方法获取到对应域名的IP,也就是默认的Dns实现。

至此,整个DNS的过程就真相大白了。OkHttp在这一块设计的时候,为了强调接耦和开放性,将DNS的整个过程隐藏的比较深,如果不仔细debug跟代码的话,可能还不是很容易发现。

3.2 Socket连接的建立

通过Dns获得Connectoin之后,就是建立连接的过程了,在findConnection中只体现为一句代码,如下:

result!!.connect(

connectTimeout,

readTimeout,

writeTimeout,

pingIntervalMillis,

connectionRetryEnabled,

call,

eventListener

)

这里的result是RealConnection类型的对象,就是调用了RealConnection.connect方法,终于离开findConnection 了,接下来看下connect 方法的源码。

//省略前面一大段

....


while (true) {

try {

if (route.requiresTunnel()) {

//这里进入的条件是,通过http代理了https请求,有一个特殊的协议交换过程

connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)

} else {

//建立socket连接

connectSocket(connectTimeout, readTimeout, call, eventListener)

}

//如果前面判定是https请求,这里就是https的tls建立过程

establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)

break

} catch (e: IOException) {

//清理资源

socket?.closeQuietly()

//对异常做二次封装,然后抛出

if (routeException == null) {

routeException = RouteException(e)

} else {

routeException.addConnectException(e)

}

if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {

throw routeException

}

}

}

connect方法并不复杂,先会判定是否有代理的情况做一些特殊处理,然后调用系统方法建立socket连接。

如果是https请求,还有一个tls的连接要建立,这中间如果有抛出异常,会做一个二次封装再抛出去。

4. 总结

到此为止,基本上OkHttp源码设计的一个全貌都有了,有一些内容因为日常使用经常会遇到,比如OkHttpClient的Builde参数,比如Request和Response的用法,这里就不再多讲。

另外针对http2和https的支持,因为涉及到更为复杂的机制和原理,以后有机会另开一篇文章来说。

☆ 

END


 ☆



招聘信息

OPPO互联网基础技术团队招聘一大波岗位,涵盖 C++、Go、OpenJDK、Java、DevOps、Android、ElasticSearch
等多个方向,
请点击这里查看详细信息及JD



你可能还喜欢

原文 

http://mp.weixin.qq.com/s?__biz=Mzg4MzE2MzY1OA==&mid=2247484396&idx=1&sn=d4bab245fc4a53a1b6541431137f3f04

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » ​OkHttp源码深度解析

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址