OkHttp源码分析
前言
OkHttp
最重要的两个技术线程复用
,连接复用
,线程复用
体现在Dispatch
类中,在OkHttp源码分析(1)中已经分析,而连接复用
则体现在OkHttp
的拦截器
中。
承接上篇文章,OkHttp
的核心是拦截器
,一系列的拦截链通过责任链的设计模式
实现了上下文的传递与整体功能的衔接。在RealCall
类中getResponseWithInterceptorChain()
中,可以看到这个责任链的整体结构
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
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)
...
}
其中,ConnectInterceptor
拦截器主要实现 Http连接
的相关功能,CallServerInterceptor
拦截器则实现服务访问的相关功能,今天主要分析这两个拦截器,并且最终得到如下两个问题的结论
OkHttp
是如实现Http
连接的;OkHttp
是如何实现连接复用的;
CallServerInterceptor
CallServerInterceptor
处于责任链的底端,作为最后一个拦截器,他主要是实现服务器的访问,实质是将http
报文写入连接流中,相应的将响应报文从流中读取出来,形成响应。
写请求报文
try {
// 写入报文的起始行与首部
exchange.writeRequestHeaders(request)
// 通过HTTP方法判断是否有请求体,GET与HEAD请求是不存在请求体的
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
// 有请求体需要写入
// 有些服务器,当请求体超过1024字节的情况下,直接访问不会响应,此时,访问分两个步骤
// 先发送携带 “Expect:100-continue”的请求头,询问是否接受数据
// 当服务端应答后,再把请求体发送给服务端;
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
// 当存在上述情况时,先把特定的首部内容“Except:100-continue”发送服务端
exchange.flushRequest()
// 发送后,读取服务端的响应
responseBuilder = exchange.readResponseHeaders(expectContinue = true)
exchange.responseHeadersStart()
invokeStartEvent = false
}
if (responseBuilder == null) {
// 第一种情况,服务端正常响应了"Expect:100-continue" ,下面可以发送请求体
if (requestBody.isDuplex()) {
// 判断是否支持双共通信,也就是HTTP/2协议,双共通信,读写互不影响
exchange.flushRequest()
// 此时可以马上写入请求体
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
requestBody.writeTo(bufferedRequestBody)
} else {
// 不支持双工通信,此时为HTTP/1 协议
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
// 写入请求提
requestBody.writeTo(bufferedRequestBody)
bufferedRequestBody.close()
}
} else {
// 第二种情况,服务段没有响应“Expect:100-continue”,或者响应错误码;
exchange.noRequestBody()
if (!exchange.connection.isMultiplexed) {
// 没有响应,并且不支持多路复用,此处应该关闭该连接;
exchange.noNewExchangesOnConnection()
}
}
} else {
// 不需要写入请求体
exchange.noRequestBody()
}
// 不存在请求体,或者不支持双共通信,都关闭连接(exchange字段为false,表示不再有数据交互)
if (requestBody == null || !requestBody.isDuplex()) {
exchange.finishRequest()
}
} catch (e: IOException) {
if (e is ConnectionShutdownException) {
throw e
}
if (!exchange.hasFailure) {
throw e
}
sendRequestException = e
}
Http
报文如下
POST Https://xxx.xxx HTTP/1.1 -----首行
Accept:application/json -----请求头
Accept-Language: zh-CN
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -----请求体
写入报文的第一步是通过下面方法写入首行与请求头
exchange.writeRequestHeaders(request)
exchange
对象负责Http
连接的数据交互。这个对象里面包含了一个ExchangeCodec
的实例,并且主要负责Http
请求响应的编解码。exchange
的writeRequestHeaders
就是调用该对象
fun writeRequestHeaders(request: Request) {
try {
eventListener.requestHeadersStart(call)
// ExchangeCode 写入首行请求头
codec.writeRequestHeaders(request)
eventListener.requestHeadersEnd(call, request)
} catch (e: IOException) {
eventListener.requestFailed(call, e)
trackFailure(e)
throw e
}
}
在HTTP/1
协议中,writeRequestHeadersStart
实现如下
override fun writeRequestHeaders(request: Request) {
val requestLine = RequestLine.get(request, connection.route().proxy.type())
writeRequest(request.headers, requestLine)
}
其中,RequestLine.get
方法如下
fun get(request: Request, proxyType: Proxy.Type) = buildString {
append(request.method)
append(' ')
if (includeAuthorityInRequestLine(request, proxyType)) {
append(request.url)
} else {
append(requestPath(request.url))
}
append(" HTTP/1.1")
}
最终会返回如下的大概如下的字符串
// 方法 URL 协议
POST Http://xxx.xxx HTTP/1.1
而writeRequest
方法如下
fun writeRequest(headers: Headers, requestLine: String) {
check(state == STATE_IDLE) { "state: $state" }
sink.writeUtf8(requestLine).writeUtf8("\r\n")
for (i in 0 until headers.size) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n")
}
sink.writeUtf8("\r\n")
state = STATE_OPEN_REQUEST_BODY
}
最终会返回如下的字符串
Accept:application/json -----请求头
Accept-Language: zh-CN
....
这两者最终组成了请求头。
需要注意的是在writeRequest
方法中出现的sink
对象,它属于okio
中的类,还有跟sink
对标的一个对象source
,这两个跟java/io
中的outputString
与inputString
具有相同的功能。此处的sink
对应的是RealConnection
中的写入流,这意味着,他将直接写入Socket
通道,这后面会讲。
在写完请求头后,则需要写入请求体。但是在此之前,必须先排除不存在请求体的情况。HEAD
与GET
请求是没有请求体的。
请求有种特殊的情况存在,在HTTP/1
协议下,有些服务器的在请求负载超过1024字节情况下会拒绝访问,此时,客户端需要将访问分为两步
- 发送
Expect:100-continue
请求给服务端,询问是否接收数据; - 服务端响应接受数据后,再将真正的请求体发送服务端;
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
// 将包含`Expect:100-continue`的请求头发送给服务器
exchange.flushRequest()
// 读取读取服务端的响应
responseBuilder = exchange.readResponseHeaders(expectContinue = true)
exchange.responseHeadersStart()
invokeStartEvent = false
}
通过exchange.readResponseHeaders
读取服务端的响应,如果服务端响应了Expect:100-continue
的请求,则返回null
。后续针对该情况,写入请求体,其中还设计到流是否支持双共通信的判断。需要注意的是,如果在服务端没有响应Expect:100-continue
的情况下,并且建立的连接不支持多路复用,也就是仅支持HTTP/1.0
协议,此时,因该关闭该流。因为没有存在的意义。
读响应报文
读取响应报文跟写请求报文是相反的过程。但是思路是一样的
try {
if (responseBuilder == null) {
// 读取响应首行与响应头
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
if (invokeStartEvent) {
exchange.responseHeadersStart()
invokeStartEvent = false
}
}
// 构建response,把读取的响应头放入response对象中
var response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
var code = response.code
if (code == 100) {
// 响应码为100,说明是`Expect:100-continue`的响应,则再次读取响应头。
// 因为服务气响应后,客户端回发送真正的请求。
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
if (invokeStartEvent) {
exchange.responseHeadersStart()
}
// 把响应头放入response对象中
response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
code = response.code
}
exchange.responseHeadersEnd(response)
response = if (forWebSocket && code == 101) {
// 如果为websocket协议,HTTP状态码101,此时服务端理解客户端,并且切换到websocket协议;
response.newBuilder()
.body(EMPTY_RESPONSE)
.build()
} else {
// 读取响应体
response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
}
// 如果响应行包含"Conection:close",则说明服务端不再继续长链接,主动断开TCP通道
// 此时该连接已经丧失数据交互功能。
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
"close".equals(response.header("Connection"), ignoreCase = true)) {
exchange.noNewExchangesOnConnection()
}
// HTTP204表示服务器成功处理了请求,不返回任何实体内容,仅仅返回跟新的元信息(响应头)
// HTTP205表示服务器成功处理了请求,不返回任何实体内容,并且希望客户端重置视图
// 两种状态都是不存在响应体
if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
throw ProtocolException(
"HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
}
// 返回response
return response
} catch (e: IOException) {
if (sendRequestException != null) {
sendRequestException.addSuppressed(e)
throw sendRequestException
}
throw e
}
先读取响应首行与响应头
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
该方法依然是调用Exchange
类中的ExchangeCodec
实例方法readResponseHeaders
@Throws(IOException::class)
fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
try {
// codec 负责报文的编解码;
val result = codec.readResponseHeaders(expectContinue)
result?.initExchange(this)
return result
} catch (e: IOException) {
eventListener.responseFailed(call, e)
trackFailure(e)
throw e
}
}
在读请求报文时,分析的是HTTP/1
的协议,此处分析HTTP/2
协议,readResponseHeaders
的实现如下
// Http2ExchangeCodec.kt
override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
val stream = stream ?: throw IOException("stream wasn't created")
val headers = stream.takeHeaders()
val responseBuilder = readHttp2HeadersList(headers, protocol)
return if (expectContinue && responseBuilder.code == HTTP_CONTINUE) {
null
} else {
responseBuilder
}
}
此处读取的对象为stream
,HTTP/2
支持多路复用,也就是流复用
,所以此处用到stream
也就不难理解。
同时,HTTP/2
报文请求头都是以键值对
的方式组织,所以上述的实现中没有像HTTP/1
中的专门读取首行的方法。而是简单的调用stream.takeHeaders()
,需要说明的是该方法是阻塞的,并且甚至了超时限制,具体细节可以查看实现。在读取响应头后,下一步组织响应头,调用readHttp2HeadersList
/** Returns headers for a name value block containing an HTTP/2 response. */
fun readHttp2HeadersList(headerBlock: Headers, protocol: Protocol): Response.Builder {
var statusLine: StatusLine? = null
val headersBuilder = Headers.Builder()
for (i in 0 until headerBlock.size) {
// key-value 类似于字典一般的参数组织形式
val name = headerBlock.name(i)
val value = headerBlock.value(i)
// `:status:`在HTTP/2中表示状态码
if (name == RESPONSE_STATUS_UTF8) {
statusLine = StatusLine.parse("HTTP/1.1 $value")
} else if (name !in HTTP_2_SKIPPED_RESPONSE_HEADERS) {
// HTTP/2明确不再支持一些响应头;
headersBuilder.addLenient(name, value)
}
}
if (statusLine == null) throw ProtocolException("Expected ':status' header not present")
return Response.Builder()
.protocol(protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(headersBuilder.build())
}
Stream
读取的报文是以类似与字典键值对的方式存储的。其中需要注意的两个点:
HTTP/2
一些特殊的响应头的表示
":status" 表示响应状态
":method": 请求方法
":path": 请求路径
":scheme" 请求的协议
":authority" 请求的认证状态
HTTP/2
不再支持一些特殊的响应头,具体如下
"connection"
"host"
"keep-alive"
"proxy-connection"
"transfer-encoding"
"te"
"encoding"
"upgrade"
以上两点均是基于HTTP/2
采用头部压缩的特性。最后,解析出的报文如下
HTTP/1.1 200 OK
Date:Tue, 10 Jul 2020 06:40:22 GMT
Content-Length:1024
.....
读取出报文后,需要进一步读取响应体,此时存在一些特殊的情况
if (code == 100) {
// 响应码为100,说明是`Expect:100-continue`的响应,则再次读取响应头。
// 因为服务气响应后,客户端回发送真正的请求。
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
if (invokeStartEvent) {
exchange.responseHeadersStart()
}
// 把响应头放入response对象中
response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
code = response.code
}
如果HTTP
响应码为100,说明服务端希望客户端继续发送请求,此时的情况对应请求头中包含Expect:100-continue
的情况,响应码100就是对该请求头的响应,在该响应触发后,客户端会发出包含数据的真正请求。此时,应该继续读取后续的响应,所以上述代码在分析出此种情况后,会重复exchange.responseHeadersStart
的操作。
在读取完响应头后,此时可以继续读取响应体。此时存在一种特殊情况,在使用WebSocket
协议时候,服务端在接收到请求时候,会先返回101
的状态码,表示服务端已经响应请求,后续将升级协议,使用客户端要求的WebSocket
。此时,返回101
的响应是不包含响应体的。
response = if (forWebSocket && code == 101) {
// 如果为websocket协议,HTTP状态码101,此时服务端理解客户端,并且切换到websocket协议;
response.newBuilder()
.body(EMPTY_RESPONSE)
.build()
} else {
// 读取响应体
response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
}
读取响应体后,如果服务断关闭连接,即响应头中包含Connection:close
,则表示该连接不在具备数据交互能力,此时应该释放该连接。
// 如果响应行包含"Conection:close",则说明服务端不再继续长链接,主动断开TCP通道
// 此时该连接已经丧失数据交互功能。
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
"close".equals(response.header("Connection"), ignoreCase = true)) {
exchange.noNewExchangesOnConnection()
}
最后,处理一些特殊的HTTP
状态码,204
与205
均表示响应不包含响应体的情况。
到目前为止,该拦截器完成与服务器数据交互的完整功能。
ConnectInterceptor
在CallServerInterceptor
拦截器的分析中,可以发现,所有报文的I/O
操作都是通过Exchange
类中的ExchangeCodec
的实例。而所有的报文最终都是通过HTTP连接
与服务端交互的,因此,确认了责任链中的Exchange
实例的来源就可以确认Http连接
的相关信息。那ConnectInterceptor
中使用的Exchange
实例到底从何而来?回溯责任链,CallServerInterceptor
是责任链的底端,他的上层链ConnectInterceptor
逻辑如下
object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
// 获得Exchange对象
val exchange = realChain.call.initExchange(chain)
// 把Exchange对象传入责任链
val connectedChain = realChain.copy(exchange = exchange)
// 调用下个拦截器
return connectedChain.proceed(realChain.request)
}
}
该拦截器的功能非常明确,通过realCall.initExchange
方法获得了Exchange
的实例,将此对象加入传入责任链上下文,传递给下个拦截器,CallServerInterceptor
中调用的Exchange
对象即来源于此。
realChain.call.initExchange
初始化了一个数据交换对象Exchange
// RealCall.kt
internal fun initExchange(chain: RealInterceptorChain): Exchange {
...
val exchangeFinder = this.exchangeFinder!!
// ExchangeFinder对象主要负责查找匹配的连接
val codec = exchangeFinder.find(client, chain)
// 新建了一个Exchange实例
val result = Exchange(this, eventListener, exchangeFinder, codec)
this.interceptorScopedExchange = result
this.exchange = result
synchronized(this) {
this.requestBodyOpen = true
this.responseBodyOpen = true
}
if (canceled) throw IOException("Canceled")
return result
}
该方法最终会返回一个新建的Exchange
实例,该实例需要一个ExchangeCodec
对象,ExchangeCodec
的主要功能在CallServerInterceptor
中已经分析过,主要将报文写入连接中。而ExchangeCodec
对象是通过ExchangFinder
对象的find
的方法。其实查找ExchangeCodec
实例的过程就是查找Connection
的过程。同时需要明确的是ExchangeFinder
类的核心功能就是根据请求查找Connection
。
查看ExchangeFinder
的find
方法
fun find(
client: OkHttpClient,
chain: RealInterceptorChain
): ExchangeCodec {
try {
// 查找连接
val resultConnection = findHealthyConnection(
connectTimeout = chain.connectTimeoutMillis,
readTimeout = chain.readTimeoutMillis,
writeTimeout = chain.writeTimeoutMillis,
pingIntervalMillis = client.pingIntervalMillis,
connectionRetryEnabled = client.retryOnConnectionFailure,
doExtensiveHealthChecks = chain.request.method != "GET"
)
// 通过连接,返回一个ExchangeCodec实例
return resultConnection.newCodec(client, chain)
} catch (e: RouteException) {
trackFailure(e.lastConnectException)
throw e
} catch (e: IOException) {
trackFailure(e)
throw RouteException(e)
}
}
find
方法会调用findHealthyConnection
找到健康的HTTP
连接,然后通过
resultConnection.newCodec(client, chain)
返回一个ExchangeCodec
实例,查看newCodec
方法
// RealConnection.kt
internal fun newCodec(client: OkHttpClient, chain: RealInterceptorChain): ExchangeCodec {
val socket = this.socket!!
val source = this.source!!
val sink = this.sink!!
val http2Connection = this.http2Connection
return if (http2Connection != null) {
Http2ExchangeCodec(client, this, chain, http2Connection)
} else {
socket.soTimeout = chain.readTimeoutMillis()
source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS)
sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS)
Http1ExchangeCodec(client, this, source, sink)
}
}
Http1ExchangeCodec
与Http2ExchangeCodec
均实现了ExchangeCodec
接口,他们分别是针对HTTP/1
与HTTP/2
协议实现的对Connection
的I/O
,HTTP/2
协议实现了流的复用,所以,传递的参数是Http2Connection
,而HTTP/1
传递的是source
与sink
。这两者是okio
中的类,分别为Connection
中Socket
连接的输入流与输出流。在CallServerInterceptor
拦截器中对报文的读写操作,最终都是使用上述三种对象。
回到正文,此处的关键应该是findHealthyConnection
方法,找到健康的连接。
private fun findHealthyConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean,
doExtensiveHealthChecks: Boolean
): RealConnection {
while (true) {
// 找到候选(备选)的连接
val candidate = findConnection(
connectTimeout = connectTimeout,
readTimeout = readTimeout,
writeTimeout = writeTimeout,
pingIntervalMillis = pingIntervalMillis,
connectionRetryEnabled = connectionRetryEnabled
)
// 确认候选的连接是否是健康的
if (candidate.isHealthy(doExtensiveHealthChecks)) {
return candidate
}
// 如果不健康,则移除出连接池
candidate.noNewExchanges()
// 如果线程池中找不到符合条件的连接,则不断尝试新的路由
if (nextRouteToTry != null) continue
val routesLeft = routeSelection?.hasNext() ?: true
if (routesLeft) continue
val routesSelectionLeft = routeSelector?.hasNext() ?: true
if (routesSelectionLeft) continue
throw IOException("exhausted all routes")
}
}
findHealtyConnection
方法通过开启一个无限循环,要么找到符合条件的连接,要么触发异常。在找到连接后,通过isHealthy
方法确认连接是有效可用的,如果找不到连接,则通过不断的调整连接的路由,多次去尝试,如果所有的路由都已经尝试过,则触发IOException
,避免死循环。上述的逻辑中,关键在于如何找到备选的路由,通过findConnection
方法,该方法比较长,内部逻辑比较复杂,却是核心。
private fun findConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean
): RealConnection {
if (call.isCanceled()) throw IOException("Canceled")
// No1:查看请求本身是否存在连接,如果存在并且可用,则使用请求本身的连接,第一步是承上启下的作用
val callConnection = call.connection
if (callConnection != null) {
var toClose: Socket? = null
synchronized(callConnection) {
if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
// 在CallServerInterCeptor拦截器中,经常通过设置noNewExchange来关闭连接
// 如果连接不具备交互功能,或者连接路由不同,则释放该连接;
toClose = call.releaseConnectionNoEvents()
}
}
// 如果请求原来的连接符合条件,则复用该连接
if (call.connection != null) {
check(toClose == null)
return callConnection
}
// 不具备交互功能的连接,关闭socket;
toClose?.closeQuietly()
eventListener.connectionReleased(call, callConnection)
}
// No2:如果请求没有连接,则尝试在连接迟中查找可以复用的连接;
// 重置状态
refusedStreamCount = 0
connectionShutdownCount = 0
otherFailureCount = 0
// 从连接池中查找可用的连接,注意此时传入的路由为null,这很重要
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
// 在查找的过程中,如果找到可用路由,会直接赋值给call.connection,所以下面的语句是安全的
val result = call.connection!!
eventListener.connectionAcquired(call, result)
// 如果连接池中存在符合条件的可复用路由,直接返回。
return result
}
// No3:如果连接池中暂时找不到可用路由,则不断调整路由,继续从连接池中查找
val routes: List<Route>?
val route: Route
if (nextRouteToTry != null) {
// Use a route from a preceding coalesced connection.
routes = null
route = nextRouteToTry!!
nextRouteToTry = null
} else if (routeSelection != null && routeSelection!!.hasNext()) {
// Use a route from an existing route selection.
routes = null
route = routeSelection!!.next()
} else {
// Compute a new route selection. This is a blocking operation!
var localRouteSelector = routeSelector
if (localRouteSelector == null) {
localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
this.routeSelector = localRouteSelector
}
val localRouteSelection = localRouteSelector.next()
routeSelection = localRouteSelection
routes = localRouteSelection.routes
if (call.isCanceled()) throw IOException("Canceled")
// 调整路由后,routers为一系列的地址,此时,再在连接池中查看是否有匹配的连接。
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
route = localRouteSelection.next()
}
// NO4:确认连接池中没有匹配的路由,只有重新创建一条新的连接;
val newConnection = RealConnection(connectionPool, route)
// 连接创建成功后,是直接赋值给call的。
call.connectionToCancel = newConnection
try {
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
} finally {
call.connectionToCancel = null
}
// 此处,routeDatabase是一个连接黑名单,如果创建的连接在黑名单之内,则将此连接从黑名单中移除
call.client.routeDatabase.connected(newConnection.route())
// NO5:连接的创建也是具备竟性条件的
// 连接创建成功后,在放入连接池之前,又去遍历连接池中是否存在符合条件的连接,
// 如果存在,则说明在创建新连接过程中,已经有人抢先一步。此时,将先创建的链接释放掉。
// 并且返回那个抢先一步创建的连接;
if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
val result = call.connection!!
nextRouteToTry = route
newConnection.socket().closeQuietly()
eventListener.connectionAcquired(call, result)
return result
}
// 如果新创建的连接没有竞争,则放入连接池中服用。
synchronized(newConnection) {
connectionPool.put(newConnection)
call.acquireConnectionNoEvents(newConnection)
}
eventListener.connectionAcquired(call, newConnection)
return newConnection
}
查找连接
查找连接的整体流程图如下
整体流程图如上所示。下面具体分析
- 先从请求中查找连接。
// No1:查看请求本身是否存在连接,如果存在并且可用,则使用请求本身的连接,第一步是承上启下的作用
val callConnection = call.connection
if (callConnection != null) {
var toClose: Socket? = null
synchronized(callConnection) {
if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
// 在CallServerInterCeptor拦截器中,经常通过设置noNewExchange来关闭连接
// 如果连接不具备交互功能,或者连接路由不同,则释放该连接;
toClose = call.releaseConnectionNoEvents()
}
}
// 如果请求原来的连接符合条件,则复用该连接
if (call.connection != null) {
check(toClose == null)
return callConnection
}
// 不具备交互功能的连接,关闭socket;
toClose?.closeQuietly()
eventListener.connectionReleased(call, callConnection)
}
为什么要先确认call
本身是否已经存在可用连接的情况,我认为这是一个承上启下的作用。如果请求本身已经存在了连接,则使用请求已经存在的连接,如果连接已经不具备交互能力,则释放该连接,并且关闭Socket
通道。为什么Call
只能被执行一次的情况下会已经存在连接?需要明确的是Call
确实是只能被执行一次,但是执行之前已经存在连接的情况确实存在。那就是是请求头中携带Connect:100-continue
的情况,在CallServerInterceptor
拦截器的代码中已经分析过此种情况了,该请求会分为两个步骤,先向服务端发送请求头,如果服务端响应了100
则继续发送请求体。在HTTP/1
协议下,连接不可复用,通过设置Connection.onNewChanges =true
来关闭连接。关闭的动作就在此处执行。而HTTP/2
协议具备连接复用,则可继续使用原来的连接发送请求体,这种情况就是Call
已经具备了连接的情况
,这里的连接就是发送请求头可以复用的连接。我感觉这里释放连接
的意义更大!
- 从连接池中查找
Call
中已经存在可用连接的情况很少,第二步则去连接池中查找。
// 从连接池中查找可用的连接,注意此时传入的路由为null,这很重要
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
// 在查找的过程中,如果找到可用路由,会直接赋值给call.connection,所以下面的语句是安全的
val result = call.connection!!
eventListener.connectionAcquired(call, result)
// 如果连接池中存在符合条件的可复用路由,直接返回。
return result
}
连接池中查找连接的调用方法为connectionPool.callAcquirePooledConnection
,该方法需要传入四个参数:地址,请求,路由列表,是否支持多路复用。注意,此处,传入的路由列表为null
,看一下具体的方法
// RealConnectionPool
fun callAcquirePooledConnection(
address: Address,
call: RealCall,
routes: List<Route>?,
requireMultiplexed: Boolean
): Boolean {
// 遍历连接池
for (connection in connections) {
synchronized(connection) {
// 排除不符合多路复用的条件
if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
// 排除不合格的连接
if (!connection.isEligible(address, routes)) return@synchronized
// 找到合格的连接后,直接将连接赋值给请求call
call.acquireConnectionNoEvents(connection)
return true
}
}
return false
}
在方法里会遍历连接池,for
循环中的connections
就是复用的连接池。针对多路复用的条件,先排除部分连接,在通过isEligible
实例方法,确认连接的有效性。
// RealConnection.kt
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
assertThreadHoldsLock()
// 此处默认一个连接同时只能被一个Call对象引用,如果引用对象超过限制,则不合格
// 如果该连接不再具备数据交互能力,也不合格
if (calls.size >= allocationLimit || noNewExchanges) return false
// 如果非主机字段不完全一致,也是不符合条件的
// 非主机字段指的是:协议、代理、端口、认证等等
if (!this.route.address.equalsNonHost(address)) return false
// 主机地址一样,并且非主机字段也一样,则完全匹配,最理想的情况
if (address.url.host == this.route().address.url.host) {
return true
}
// 如果主机名不一致,则查看是否满足合并请求的条件
// 1. 必须是HTTP/2 协议
if (http2Connection == null) return false
// 2. 所有路由必须使用相同的地址,并且不使用代理,因为代理无法知道真正主机的地址
if (routes == null || !routeMatchesAny(routes)) return false
// 3. 主机名验证,符合规定的URI格式
if (address.hostnameVerifier !== OkHostnameVerifier) return false
if (!supportsUrl(address.url)) return false
// 4. 证书与服务器匹配,防止中间人攻击的情况;
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.
}
连接池中连接可以具备复用的的情况分为两种
- 具备直接使用的连接,该情况为最完美的情况
- 该连接的存在的请求在允许范围内,并且该连接具备数据交互能力
- 连接的主机名与请求的主机名相同,非主机字段也相同
- 具备合并请求的连接,该情况可以将新请求合并在此连接中
- 必须是
HTTP/2
协议 - 请求中的所有路由必须是相同的地址,并且没有使用代理,因为代理无法知道真实服务器的地址
- 请求中的主机名验证必须有效,符合相关规范
- 请求中主机名与证书中的身份必须匹配
其中具体的要求可以参考HTTP
相关文档,不做具体讨论。符合上述两种情况则说明连接是合格的。则可以复用该连接,并且调用call.acquireConnectionNoEvents
方法直接将该连接附加在Call
对象上。
// RealCall.kt
fun acquireConnectionNoEvents(connection: RealConnection) {
connection.assertThreadHoldsLock()
check(this.connection == null)
// 设置Call的connection属性
this.connection = connection
connection.calls.add(CallReference(this, callStackTrace))
}
从连接池中查找连接的方法分析完了,但是该方法会被执行多次。后续还会多次进入到该方法中。如果连接池中存在符合条件的连接,则直接返回连接,否则进入下一步。
- 调整路由后,查找具备合并请求条件的连接
其实,上一步在线程池中查找可用连接是查找地址完全匹配
的可用连接,即主机名和非主机字段都匹配的情况。如果这种完美情况无法实现,则下一部尝试进行符合请求合并
的连接,所以在第二步中传入的路由参数为null
,这一步主要是调整路由参数后,继续在线程池中查找。
//ExchangeFinder.kt
val routes: List<Route>?
val route: Route
if (nextRouteToTry != null) {
// Use a route from a preceding coalesced connection.
routes = null
route = nextRouteToTry!!
nextRouteToTry = null
} else if (routeSelection != null && routeSelection!!.hasNext()) {
// Use a route from an existing route selection.
routes = null
route = routeSelection!!.next()
} else {
// 使用新的路由选择器
var localRouteSelector = routeSelector
if (localRouteSelector == null) {
// 创建新的路由选择器
localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
this.routeSelector = localRouteSelector
}
val localRouteSelection = localRouteSelector.next()
routeSelection = localRouteSelection
routes = localRouteSelection.routes
if (call.isCanceled()) throw IOException("Canceled")
// 在具备一系列的地址后,再次去连接池中查找符合条件的连接,逻辑都是跟上面一样的
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
route = localRouteSelection.next()
}
上述的情况针对的是用户设置代理的情况。在一开始阶段,因为不存在路由对象,所以会通过实例化一个RouterSelector
对象,获取相关的路由列表。
在RouterSelector
类中,会初始化一个方法
init {
resetNextProxy(address.url, address.proxy)
}
该方法如下
private fun resetNextProxy(url: HttpUrl, proxy: Proxy?) {
fun selectProxies(): List<Proxy> {
// 此处的代理对象为用户在`OkHttpClient`类中设立的代理对象
if (proxy != null) return listOf(proxy)
// 主机名缺失的情况
val uri = url.toUri()
if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)
// Try each of the ProxySelector choices until one connection succeeds.
val proxiesOrNull = address.proxySelector.select(uri)
if (proxiesOrNull.isNullOrEmpty()) return immutableListOf(Proxy.NO_PROXY)
return proxiesOrNull.toImmutableList()
}
eventListener.proxySelectStart(call, url)
proxies = selectProxies()
nextProxyIndex = 0
eventListener.proxySelectEnd(call, url, proxies)
}
这里如果用户在OkHttpClient
中设置了代理,则返回代理对象,否则会逐步尝试address.proxySelector
,这里的proxySelector
是在OkHttpClient
中设置的
// OkHttpClient.kt
val proxySelector: ProxySelector =
when {
// 如果用户未设置代理,返回NullProxySelector
builder.proxy != null -> NullProxySelector
else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector
}
NullProxySelector
对象如下
object NullProxySelector : ProxySelector() {
override fun select(uri: URI?): List<Proxy> {
requireNotNull(uri) { "uri must not be null" }
return listOf(Proxy.NO_PROXY)
}
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
}
}
如果通过调整路由后,依然无法找到可以合并请求的连接,则创建一条新的连接,代码如下
// NO4:确认连接池中没有匹配的路由,只有重新创建一条新的连接;
val newConnection = RealConnection(connectionPool, route)
// 连接创建成功后,是直接赋值给call的。
call.connectionToCancel = newConnection
try {
// 建立通道
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
} finally {
call.connectionToCancel = null
}
// 此处,routeDatabase是一个连接黑名单,如果创建的连接在黑名单之内,则将此连接从黑名单中移除
call.client.routeDatabase.connected(newConnection.route())
首先实例化RealConnection
创建一个新的连接,随后通过connect
方法通道的建立。
创建连接
直接查看newConnection.connect
方法,看具体的连接过程
fun connect(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean,
call: Call,
eventListener: EventListener
) {
check(protocol == null) { "already connected" }
var routeException: RouteException? = null
val connectionSpecs = route.address.connectionSpecs
val connectionSpecSelector = ConnectionSpecSelector(connectionSpecs)
if (route.address.sslSocketFactory == null) {
// 如果使用的是SSL,则不支持使用明文协议
if (ConnectionSpec.CLEARTEXT !in connectionSpecs) {
throw RouteException(UnknownServiceException(
"CLEARTEXT communication not enabled for client"))
}
val host = route.address.url.host
if (!Platform.get().isCleartextTrafficPermitted(host)) {
// 如果使用的是SSL,则不支持使用明文协议
throw RouteException(UnknownServiceException(
"CLEARTEXT communication to $host not permitted by network security policy"))
}
} else {
// 如果使用的不是SSL,则不支持使用HTTPS协议
if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
throw RouteException(UnknownServiceException(
"H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"))
}
}
// 直接进入死循环
while (true) {
try {
// 如果遇到通过Http协议代理Https协议的服务器,则必须现在客户端与被代理服务器端建立隧道。
if (route.requiresTunnel()) {
// 先建立与被代理服务器的隧道,然后在建立通信渠道。
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
if (rawSocket == null) {
break
}
} else {
// 如果不存在上述代理情况,则直接建立Socket连接
connectSocket(connectTimeout, readTimeout, call, eventListener)
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
break
} catch (e: IOException) {
socket?.closeQuietly()
rawSocket?.closeQuietly()
socket = null
rawSocket = null
source = null
sink = null
handshake = null
protocol = null
http2Connection = null
allocationLimit = 1
eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e)
if (routeException == null) {
routeException = RouteException(e)
} else {
routeException.addConnectException(e)
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException
}
}
}
if (route.requiresTunnel() && rawSocket == null) {
throw RouteException(ProtocolException(
"Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS"))
}
idleAtNs = System.nanoTime()
}
该方法首先对协议的兼容性做了判断,如HTTPS
是不支持明文的,而HTTP
则是不支持加密等。协议兼容性问题解决后,开始正式的Socket
连接,一般情况下直接调用connectSocket
,
private fun connectSocket(
connectTimeout: Int,
readTimeout: Int,
call: Call,
eventListener: EventListener
) {
val proxy = route.proxy
val address = route.address
val rawSocket = when (proxy.type()) {
// 存在代理的情况下,通过socket工厂方法创建Socket
Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
// 不存在情况,则直接实例化Socket,最原始的Socket
else -> Socket(proxy)
}
this.rawSocket = rawSocket
eventListener.connectStart(call, route.socketAddress, proxy)
rawSocket.soTimeout = readTimeout
try {
// 建立Socket连接通道
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
} catch (e: ConnectException) {
throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
initCause(e)
}
}
try {
// 建立连接信道后,就可以直接获取I/O流了。
source = rawSocket.source().buffer()
sink = rawSocket.sink().buffer()
} catch (npe: NullPointerException) {
if (npe.message == NPE_THROW_WITH_NULL) {
throw IOException(npe)
}
}
}
实例化Socket
对象,再通过平台相关API
实现Socket
的连接
// 平台实现Socket的连接
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
Socket
通道建立后,获取相应的I/O
流,后续通过该I/O
流实现报文的读写。
// 建立连接信道后,就可以直接获取I/O流了。
source = rawSocket.source().buffer()
sink = rawSocket.sink().buffer()
上述情况是比较正常的情况,比较不正常的情况是遇到HTTP
协议的服务器代理HTTPS
协议的服务器,这种情况需要先在客户端与被代理服务器之间建立通信隧道。
if (route.requiresTunnel()) {
// 先建立与被代理服务器的隧道,然后在建立通信渠道。
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
if (rawSocket == null) {
break
}
}
connectTunnel
就是建立通信隧道的过程。
private fun connectTunnel(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
call: Call,
eventListener: EventListener
) {
// 创建创建隧道的请求。
var tunnelRequest: Request = createTunnelRequest()
val url = tunnelRequest.url
for (i in 0 until MAX_TUNNEL_ATTEMPTS) {
// 连接Socket,此处是跟代理服务器建立连接。
connectSocket(connectTimeout, readTimeout, call, eventListener)
// 实现隧道的通信连接
tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url)
?: break
rawSocket?.closeQuietly()
rawSocket = null
sink = null
source = null
eventListener.connectEnd(call, route.socketAddress, route.proxy, null)
}
}
建立隧道时先创建一个请求建立隧道的请求,调用createTunnelRequest
方法
@Throws(IOException::class)
private fun createTunnelRequest(): Request {
// 如果不存在身份验证的情况下,直接携带`CONNECT`请求头
val proxyConnectRequest = Request.Builder()
.url(route.address.url)
.method("CONNECT", null)
.header("Host", route.address.url.toHostHeader(includeDefaultPort = true))
.header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
.header("User-Agent", userAgent)
.build()
// 如果存在身份验证,则使用抢先身份验证的方式,即先mock一个407的响应,这可以让
// 身份验证器返回自定义的连接请求,或者身份验证器返回null表示拒绝。
val fakeAuthChallengeResponse = Response.Builder()
.request(proxyConnectRequest)
.protocol(Protocol.HTTP_1_1)
.code(HTTP_PROXY_AUTH)
.message("Preemptive Authenticate")
.body(EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(-1L)
.header("Proxy-Authenticate", "OkHttp-Preemptive")
.build()
// 如果需要身份验证,则采用抢先身份验证的方式,返回自定义的连接请求。
val authenticatedRequest = route.address.proxyAuthenticator
.authenticate(route, fakeAuthChallengeResponse)
return authenticatedRequest ?: proxyConnectRequest
}
在createTunnelRequest
方法中,不存在身份验证则正常返回携带CONNECT
的请求,如果存在身份验证,则使用抢先身份验证方式,即自己先Mock
一个407
的响应,通过407
的响应,用户在后续的请求中可以自定义连接请求,将响应传给身份验证器authenticated
,返回一个自定义的请求。查看authenticate
方法的实现
@Override public Request authenticate(Route route, Response response) throws IOException {
if (route == null) throw new NullPointerException("route == null");
if (response == null) throw new NullPointerException("response == null");
responses.add(response);
routes.add(route);
if (!schemeMatches(response) || credential == null) return null;
// 如果407响应码,则添加`Proxy-Authorization:证书xxxx`
String header = response.code() == 407 ? "Proxy-Authorization" : "Authorization";
return response.request().newBuilder()
.addHeader(header, credential)
.build();
}
上面的方法之做了一件事,如果响应码为407
,则增加Proxy-Authorization:cerdential
的请求头,至于cerdential
是从何而来,这跟责任链之前的拦截器有关,此处不分析。
获得request
后,接下来将次request
发给HTTPS
的被代理服务器,但是在这之前,则必须先跟代理服务器先建立Socket
连接,connectSocket
的连接过程已经分析过,但此处建立的连接是客户端与代理服务器。与代理服务器建立连接后,客户端就可以跟被代理服务器建立隧道了,调用createTunnel
方法如下
private fun createTunnel(
readTimeout: Int,
writeTimeout: Int,
tunnelRequest: Request,
url: HttpUrl
): Request? {
var nextRequest = tunnelRequest
// 首行
val requestLine = "CONNECT ${url.toHostHeader(includeDefaultPort = true)} HTTP/1.1"
while (true) {
val source = this.source!!
val sink = this.sink!!
// 写入Socket流中,因为使用的是`HTTP代理`,所以使用是`HTTP1ExchangeCodec`
val tunnelCodec = Http1ExchangeCodec(null, this, source, sink)
source.timeout().timeout(readTimeout.toLong(), MILLISECONDS)
sink.timeout().timeout(writeTimeout.toLong(), MILLISECONDS)
tunnelCodec.writeRequest(nextRequest.headers, requestLine)
tunnelCodec.finishRequest()
val response = tunnelCodec.readResponseHeaders(false)!!
.request(nextRequest)
.build()
tunnelCodec.skipConnectBody(response)
when (response.code) {
// 200 则建立隧道成功
HTTP_OK -> {
if (!source.buffer.exhausted() || !sink.buffer.exhausted()) {
throw IOException("TLS tunnel buffered too many bytes!")
}
return null
}
// 407,则继续身份验证
HTTP_PROXY_AUTH -> {
nextRequest = route.address.proxyAuthenticator.authenticate(route, response)
?: throw IOException("Failed to authenticate with proxy")
if ("close".equals(response.header("Connection"), ignoreCase = true)) {
return nextRequest
}
}
else -> throw IOException("Unexpected response code for CONNECT: ${response.code}")
}
}
}
建立隧道的方法,在创建与代理服务器连接的基础上,向代理服务器发送CONNECT
请求(不含请求体),代理服务器会转发请求到被代理服务器。如果响应码为200
则说明隧道建立成功,如果响应码为407
则说明需要身份验证,后续的操作就是通过身份验证器生成携带证书的请求进行下次的请求,继续建立隧道。
至此连接的过程结束。