本文概要
- okhttp拦截器实现
- 代理和路由
- 连接池实现
- 任务调度
1、okhttp拦截器实现
首先看下okhttp的简单使用:
val client = OkHttpClient()
val request = Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
// print headers
for ((name, value) in response.headers) {
println("$name: $value")
}
// print body
println(response.body!!.string())
}
OkHttpClient理解成一个配置类,用于设置一些基础参数(比如:代理,DNS,SSL,超时时间等等),然后通过newCall将这些基础参数和request参数组装成HTTP请求对象RealCall。
再通过execute()方法执行http请求,内部调用了getResponseWithInterceptorChain()方法,这个方法则通过一系列Interceptor嵌套调用得到Response,Response包含了响应Headers和ResponseBody(内部通过okio实现的字节流),拿到字节流就可以读取http请求数据了。
getResponseWithInterceptorChain() 方法部分代码如下:
fun getResponseWithInterceptorChain(): Response {
// 请注意Interceptor的添加顺序,proceed方法按照这个顺序调用的拦截器的
val interceptors = mutableListOf()
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)
// ...
val response = chain.proceed(originalRequest)
// ...
return response
}
整个调用流程如下图:
上图包含了okhttp内置的5个拦截器,主要功能如下:
RetryAndFollowUpInterceptor
在请求抛出异常时负责重试,但需要满足一定条件才会重试。比如ssl握手失败就不会重试,建立连接成功在读写数据出现IOException时会重试。后面会专门介绍重试机制!
BridgeInterceptor
负责桥接应用层和网络层代码,在网络层请求前后做一些准备和善后操作,比如设置请求头(Content-Length,Accept-Encoding,Cookie等),网络层请求完成后对结果ResponseBody进行处理(比如gzip解压)
CacheInterceptor
负责缓存查找处理,如果缓存命中则后面的拦截器就不会执行,直接返回Response
ConnectInterceptor
负责建立HTTP连接:建立新的连接或从连接池中找到可复用的连接。ConnectInterceptor相对其他拦截器功能较多:可复用连接查找,路由查找,域名解析,Socket连接,建立协议版本(HTTP 1.1或HTTP 2),SSL握手等等
CallServerInterceptor
建立连接后跟服务器端交互,主要是写入request和读取response,内部通过Exchange对象完成,Exchange对象实现了对http读写的基本操作(比如写入header,写入body,读取header等),Exchange内部则使用ExchangeCodec格式化写入和读取数据,ExchangeCodec有两个实现,分别是:Http1ExchangeCodec和Http2ExchangeCodec,对应HTTP1和HTTP2
另外除了上述的5个内置拦截器外,okhttp还提供了两类自定义扩展拦截器:Application Interceptors 和 Network Interceptors,在OKHttpClient初始化时可通过addInterceptor()和addNetworkInterceptor()添加,从图中可以看出Application Interceptors是最开始执行的拦截器,是建立TCP连接前调用的,Network Interceptors是在连接建立后调用的,借用官方的图更具体一点:
两者的区别主要根据他们在拦截器调用链中的位置来分析,如下:
Application Interceptors
- 不需要关心是否重定向或者失败重连
- 应用拦截器只会调用一次,因为它比RetryAndFollowUpInterceptor先执行
- 应用拦截器是第一个执行的,可以决定是否调用其他拦截器,(不调用Chain.proceed()则表示拦截剩余的拦截器)
Network Interceptors
- 网络拦截器是在连接拦截器后调用的,可以执行重定向和重连操作
- 可以查看连接相关信息
拦截器调用链实现
整个okhttp拦截器调用流程还是挺清晰的,拦截器调用是通过RealInterceptorChain.proceed(request)和Inteceptor.intercept(chain)两个方法结合实现。
RealInterceptorChain
- 内部包含了所有拦截器的列表interceptors和当前要执行的拦截器在列表中的索引index
- 执行proceed()方法时会取出当前index对应的Interceptor,同时将index加一后保存到一个新的RealInterceptorChain中(除了index不一样,其他数据都一样),再将这个chain作为参数调用前面取得的Interceptor的拦截方法intercept(chain)
- 如果intercept(chain)方法内部继续调用chain.proceed()就会执行下一个拦截器,这样类似递归调用使所有拦截器按顺序执行,直到最后一个拦截器(CallServerInterceptor)
- 如果某个拦截器intercept(chain)方法内部不调用chain.proceed(),那么后面剩余的拦截器就没有机会执行
RealInterceptorChain.proceed() 方法部分代码如下:
fun proceed(request: Request, transmitter: Transmitter, exchange: Exchange?): Response {
...
// index + 1记录下一个待执行的拦截器
val next = RealInterceptorChain(interceptors, transmitter, exchange,
index + 1, request, call, connectTimeout, readTimeout, writeTimeout)
// 取出当前拦截器
val interceptor = interceptors[index]
...
// 执行当前拦截器
val response = interceptor.intercept(next) ?: throw NullPointerException(
"interceptor $interceptor returned null")
...
return response
}
Interceptor
- 只有一个方法:fun intercept(chain: Chain): Response
- 参数chain对象内包含了所有拦截器列表和下一个要执行的拦截器,如果调用chain.proceed()就会执行下一个拦截器
自定义拦截器示例
class MyInterceptor:Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// do something
// 这里也可以不调用chain.proceed(chain.reqest()),这样chain里面剩余的拦截器就不会执行
val response = chain.proceed(chain.request())
// do something
return response
}
}
到此,okhttp的拦截器实现就这些,拦截器可以拦截(比如CacheInterceptor),也可以不拦截(比如LogInterceptor),更多的是提供了一种面向切面编程的能力,在下游实现的前后做一些加工处理,或者直接拦截掉下游的执行!
2、路由和代理
什么是okhttp路由,通过域名发起HTTP请求,首先要将域名转成IP这一过程叫域名解析,也就是DNS协议干的事!okhttp的路由就是通过request的域名或OKHttpClient设置的代理(如过有设置代理)找到合适的IP地址!
几个主要类
Dns
抽象了DNS解析过程,抽象方法为:fun lookup(hostname: String): List
Route
DNS解析完成后的对象,可以简单的理解为包含了IP地址的对象,内部属性socketAddress为DNS解析后的地址
RouteDatabase
记录了之前连接成功,但读写过程中失败了对应的路由,后续再路由选择时会优先排除这里记录的路由(除非一个可用的都没有)
RouteSelector
路由选择实现,RouteSelector有以下工功能:
- 收集所有(代理代理由OkHttpClient.Builder.proxy(proxy)和OkHttpClient.Builder.proxySelector(selector)配置, 默认会使用proxy,在proxy不存在的情况下才会使用proxySelector)!
- 通过域名或代理的域名 调用dns.lookup(socketHost)进行域名解析
- RouteDatabase会记录请求失败的路由,路由选择时会优先排除这部分路由,除非没有其他路由可用了,就不排除
DNS解析可以通过OkHttpClient.Builder.dns(dns)方法配置,默认是调用系统DNS解析InetAddress.getAllByName(hostname), 如下:
val SYSTEM: Dns = DnsSystem()
private class DnsSystem : Dns {
override fun lookup(hostname: String): List {
try {
return InetAddress.getAllByName(hostname).toList()
} catch (e: NullPointerException) {
throw UnknownHostException("Broken system behaviour for dns lookup of $hostname").apply {
initCause(e)
}
}
}
}
3、连接池实现(连接复用)
首先TCP连接复用需要Keep-Alive,所以在BridgeInterceptor中如果用户没有设置Connection,则默认将Connection有设置为: Keep-Alive
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}
但实际连接是否可复用还得取决于Server返回的Connection来决定,如果Server返回close,则将TCP连接标识为不可复用,这部分代码在CallServerInterceptor.intercept()方法中:
// 如果response返回 Connection为close,则将连接标识为不可复用
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
"close".equals(response.header("Connection"), ignoreCase = true)) {
// 内部是将RealConnection标识为不可复用
exchange.noNewExchangesOnConnection()
}
连接可复用后,就需要对其进行管理,由RealConnectionPool对象实现,主要方法如下:
- fun put(connection: RealConnection)
当新的连接建立时,会调用该方法将连接加入到连接池
- fun transmitterAcquirePooledConnection(
address: Address,
transmitter: Transmitter,
routes: List?,
requireMultiplexed: Boolean
): Boolean该方法为查找可复用的连接,判断条件有:连接是否可复用,连接是否超过最大复用数(HTTP1.1只能为1,HTTP2默认为4),request请求参数匹配(包括:代理,协议版本,ssl,端口号等等)
- fun connectionBecameIdle(connection: RealConnection): Boolean
通知连接池,该连接空闲,如果返回true说明该连接已从连接池移除,则需手动关闭连接
- fun cleanup(now: Long): Long
清理连接,并返回下一次清理的间隔时间。满足这些条件的连接会从连接池中移除关闭Scoket:空闲时间超过最大值,被标识为不能再复用
连接加入连接池
在RealConnection为单个连接对象,在RealConnection.connect()方法连接成功后会将其加入连接池,源码在ExchangeFinder.findConnection方法中:
synchronized(connectionPool) {
connectingConnection = null
// Last attempt at connection coalescing, which only occurs if we attempted multiple
// concurrent connections to the same host.
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 省略代码
} else {
// 新建的连接加入连接池
connectionPool.put(result!!)
transmitter.acquireConnectionNoEvents(result!!)
}
}
连接复用查找
在建立新的TCP连接前会调用transmitterAcquirePooledConnection方法从连接池查找可复用连接,如果没有查询到可复用的连接才会新建一个连接,代码在ExchangeFinder.findConnection()方法中:
// Attempt to get a connection from the pool.
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
foundPooledConnection = true
result = transmitter.connection
} else if (nextRouteToTry != null) {
selectedRoute = nextRouteToTry
nextRouteToTry = null
} else if (retryCurrentRoute()) {
selectedRoute = transmitter.connection!!.route()
}
连接复用查找匹配规则
RealConnection内部维护了一个被上层引用的Transmitter对象列表transmitters,transmitters的个数就为复用数,默认一个连接使用HTTP 1.1同时只能有一个request在处理,即transmitters最大size只能为1,当HTTP请求完成时会释放这个连接,即将Transmitter的引用从transmitters列表中移除,这时size为0就可以被其他请求服用了。HttP 2默认最大允许4个request。
连接复用匹配过程主要在RealConnection.isEligible和RealConnection.equalsNonHost方法中:
internal fun isEligible(address: Address, routes: List?): Boolean {
// 如果超过最大复用数或者已标识为不可复用,表明不能复用
if (transmitters.size >= allocationLimit || noNewExchanges) return false
// 除host以外的参数如果都匹配不上,不可复用
if (!this.route.address.equalsNonHost(address)) return false
// 如果host相同,则可复用
if (address.url.host == this.route().address.url.host) {
return true // This connection is a perfect match.
}
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false
// 2. The routes must share an IP address.
if (routes == null || !routeMatchesAny(routes)) return false
// 3. This connection's server certificate's must cover the new host.
if (address.hostnameVerifier !== OkHostnameVerifier) return false
if (!supportsUrl(address.url)) return false
// 4. Certificate pinning must match the host.
try {
address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates)
} catch (_: SSLPeerUnverifiedException) {
return false
}
return true // The caller's address can be carried by this connection.
}
// 匹配除了host以外的参数
internal fun equalsNonHost(that: Address): Boolean {
return this.dns == that.dns &&
this.proxyAuthenticator == that.proxyAuthenticator &&
this.protocols == that.protocols &&
this.connectionSpecs == that.connectionSpecs &&
this.proxySelector == that.proxySelector &&
this.proxy == that.proxy &&
this.sslSocketFactory == that.sslSocketFactory &&
this.hostnameVerifier == that.hostnameVerifier &&
this.certificatePinner == that.certificatePinner &&
this.url.port == that.url.port
}
连接何时被清理
- 如果后台返回的Header的Connection字段为Close;说明不能复用连接,则将RealConnection标识为不能再复用(将其内部变量赋值为:noNewExchanges = true)
- HTTP读写出现异常时也为将noNewExchanges = true
- RealConnectionPool.cleanup()方法会清理掉空闲超过最大keepAliveDurationNs的连接
- 在连接被使用完后会调用Transmitter.releaseConnectionNoEvents()方法,会将RealConnection内部保存的引用移除,如果连接已经不可复用了就将其Socket关闭掉!
fun releaseConnectionNoEvents(): Socket? {
assert(Thread.holdsLock(connectionPool))
val index = connection!!.transmitters.indexOfFirst { it.get() == this@Transmitter }
check(index != -1)
val released = this.connection
released!!.transmitters.removeAt(index)
this.connection = null
if (released.transmitters.isEmpty()) {
released.idleAtNanos = System.nanoTime()
if (connectionPool.connectionBecameIdle(released)) {
return released.socket()
}
}
return null
}
Response.close()方法
另外说一下Response.close()方法,其实并不会关闭Socket,如果真关闭了那还怎么复用,但是close会做一些善后工作,比如用户发起了一个HTTP请求,写入了request,但是没有读取Response就直接调用了close了,这时close()方法会触发读取Server返回的数据操作,并丢弃掉这部分数据来保证该连接可被复用,如果不读取这部分数据,后面复用该连接,就会读取到上一个请求的错误数据!
close()实际调用的如下方法,如果还有未读数据,其中discard()方法会读取未读的数据并丢弃:
// Http1ExchangeCodec.ChunkedSource.close()
override fun close() {
if (closed) return
// 这里中discard方法为丢弃未读取的数据
if (hasMoreChunks &&
!discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
realConnection!!.noNewExchanges() // Unread bytes remain on the stream.
responseBodyComplete()
}
closed = true
}
// Http1ExchangeCodec.FixedLengthSource.close()
override fun close() {
if (closed) return
// 这里中discard方法为丢弃未读取的数据
if (hasMoreChunks &&
!discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
realConnection!!.noNewExchanges() // Unread bytes remain on the stream.
responseBodyComplete()
}
closed = true
}
4、任务调度
任务调度由Dispatcher实现,主要实现了请求异步执行和一些限制,比如同时最多并行64个请求,同一个host最大并行请求为5个,这部分比较简单,主要代码如下:
private fun promoteAndExecute(): Boolean {
assert(!Thread.holdsLock(this))
val executableCalls = mutableListOf()
val isRunning: Boolean
synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()
if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity 默认 64.
if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue // Host max capacity 默认 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)
}
return isRunning
}
最后看下okhttp主要对象的类图
参考
okhttp源码(4.2.2)