探索OkHttp系列 (五) 连接建立与复用
前言
上一篇文章我们介绍了CacheInterceptor
拦截器,这篇文章我们要介绍的拦截器是ConnectInterceptor
,该拦截器的作用是获得一个健康可用的与目标服务器的连接,然后就将请求交给下一个拦截器处理。
该拦截器的内部实现非常的复杂,涉及到OkHttp
许多的机制,例如路由选择机制、连接的建立与复用机制,我们在下面的分析中,先对其大体流程进行分析,然后再一个一个点地深入分析,逐个突破。
ConnectInterceptor::intercept
ConnectInterceptor::intercept
/**
* Opens a connection to the target server and proceeds to the next interceptor. The network might
* be used for the returned response, or to validate a cached response with a conditional GET.
*/
object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
val exchange = realChain.call.initExchange(chain)
val connectedChain = realChain.copy(exchange = exchange)
return connectedChain.proceed(realChain.request)
}
}
从该方法的注释可以看出,该拦截器的主要作用是:打开一个到目标服务器的连接,然后将请求交给下一个拦截器处理。这个连接其实就是TCP
连接,用于Http
的请求和响应。
获取连接的大体流程
intercept
方法的关键代码是RealCall::initExchange
,该方法会返回一个Exchange
对象,Exchange
是什么呢?我们后面会提到。
RealCall::initExchange
/** Finds a new or pooled connection to carry a forthcoming request and response. */
internal fun initExchange(chain: RealInterceptorChain): Exchange {
synchronized(this) {
check(expectMoreExchanges) { "released" }
check(!responseBodyOpen)
check(!requestBodyOpen)
}
val exchangeFinder = this.exchangeFinder!!
// 创建ExchangeCodec对象
val codec = exchangeFinder.find(client, chain)
// 利用ExchangeCodec实例,创建了一个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")
// 返回创建的Exchange对象
return result
}
从该方法的注释可以看出:该方法用于找到一个新的、或者连接池里一个健康可用的连接,来承载一个即将到来的请求和响应。上面出现了ExchangeCodec
、Exchange
两个类,分别查看它们的注释。
ExchangeCodec
ExchangeCodec
:
/** Encodes HTTP requests and decodes HTTP responses. */
interface ExchangeCodec {
...
}
它是一个接口,负责对Http
请求进行编码和对Http
响应进行解码,它有两个实现类,分别是Http1ExchangeCodec
、Http2ExchangeCodec
。以Http1ExchangeCodec
为例,查看其中的writeRequestHeaders
方法
override fun writeRequestHeaders(request: Request) {
val requestLine = RequestLine.get(request, connection.route().proxy.type())
writeRequest(request.headers, requestLine)
}
...
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
}
这就是其中的一个编码操作,负责将Request
转化为报文的格式,以便发送给服务端。另外,ExchangeCodec
包含了一个RealConnection
对象。
Exchange
Exchange
:
/**
* Transmits a single HTTP request and a response pair. This layers connection management and events
* on [ExchangeCodec], which handles the actual I/O.
*/
class Exchange(...){
...
}
该类传输单个的Http
请求和响应对,负责连接管理的工作。Exchange
类的内部包含了一个ExchangeCodec
对象,我们查看Exchange
类内部的writeRequestHeaders
方法
fun writeRequestHeaders(request: Request) {
try {
eventListener.requestHeadersStart(call)
// 调用了ExchangeCodec对象的方法
codec.writeRequestHeaders(request)
eventListener.requestHeadersEnd(call, request)
} catch (e: IOException) {
eventListener.requestFailed(call, e)
trackFailure(e)
throw e
}
}
可以看到,它内部的I/O
操作,实际上是调用了ExchangeCodec
对象的方法来实现的,我们可以认为Exchange
是封装ExchangeCodec
的一个工具类,Exchange
负责连接管理,而ExchangeCodec
负责处理实际的I/O
。
ExchangeFinder::find
RealCall::initExchange
调用了ExchangeFinder::find
,获取一个ExchangeCodec
对象,我们查看该方法
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)
}
}
上面的方法中,获取了一个健康可用的连接,并且利用该连接,创建了一个编码解码的ExchangeCodec
对象,有了与服务器的连接和处理I/O的ExchangeCodec
对象,我们其实就可以和服务器进行通信了。
ExchangeFinder::findHealthyConnection
上面调用了ExchangeFinder::findHealthyConnection
方法,获取了一个健康可用的连接,该方法如下
/**
* Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
* until a healthy connection is found.
*/
@Throws(IOException::class)
private fun findHealthyConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean,
doExtensiveHealthChecks: Boolean
): RealConnection {
while (true) {
// 获取一个可用的连接,RealConnection对象代表一个连接
val candidate = findConnection(
connectTimeout = connectTimeout,
readTimeout = readTimeout,
writeTimeout = writeTimeout,
pingIntervalMillis = pingIntervalMillis,
connectionRetryEnabled = connectionRetryEnabled
)
// Confirm that the connection is good.
// 检查连接是否健康,该连接是否已准备好去承载新的流
if (candidate.isHealthy(doExtensiveHealthChecks)) {
return candidate
}
// If it isn't, take it out of the pool.
// 如果该连接不健康,就给该连接做一个标记,不再使用该连接
candidate.noNewExchanges()
// Make sure we have some routes left to try. One example where we may exhaust all the routes
// would happen if we made a new connection and it immediately is detected as unhealthy.
// 确保我们还有可尝试的路由
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")
}
}
从该方法的注释可以看出:该方法会查找到一个可用且健康的连接并将其返回,如果找到的可用连接是不健康的,那么会一直重复查找可用连接的这个过程,直到一个可用且健康的连接被找到。
该方法内部有一个while(true)
的循环,在循环代码里面,会不断地去获取一个可用连接,并检查该连接是否健康,如果该连接健康,就将其返回,如果连接不健康,在有可尝试的路由的前提下,会重复前面查找可用连接的过程。注意,这里说的可用和健康是两个不同的指标。
判断连接是否健康使用了RealConnection::isHealthy
方法,如下
// 如果该连接准备好去承载新的流,就返回true
fun isHealthy(doExtensiveChecks: Boolean): Boolean {
assertThreadDoesntHoldLock()
val nowNs = System.nanoTime()
val rawSocket = this.rawSocket!!
val socket = this.socket!!
val source = this.source!!
// 判断socket是否可用
if (rawSocket.isClosed || socket.isClosed || socket.isInputShutdown ||
socket.isOutputShutdown) {
return false
}
// 如果是HTTP/2连接,检测该HTTP/2连接是否是健康的
val http2Connection = this.http2Connection
if (http2Connection != null) {
return http2Connection.isHealthy(nowNs)
}
val idleDurationNs = synchronized(this) { nowNs - idleAtNs }
// 若空闲时间达到某个值,则检测socket是否是健康的
if (idleDurationNs >= IDLE_CONNECTION_HEALTHY_NS && doExtensiveChecks) {
return socket.isHealthy(source)
}
return true
}
ExchangeFinder::findConnection
上面调用了findConnection
方法,该方法用于获取一个可用连接。该方法的实现逻辑较为复杂,我们先介绍它的大体执行流程,后面再对它的各个点进行详细地分析
/**
* Returns a connection to host a new stream. This prefers the existing connection if it exists,
* then the pool, finally building a new connection.
*
* This checks for cancellation before each blocking operation.
*/
// 返回一个连接去承载新的流,优先使用现有连接,接着是连接池中的连接,最后是创建一个新的连接
@Throws(IOException::class)
private fun findConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean
): RealConnection {
// 检查取消事件
if (call.isCanceled()) throw IOException("Canceled")
// 1.尝试去重用call的连接
val callConnection = call.connection
if (callConnection != null) {
var toClose: Socket? = null
synchronized(callConnection) {
// 检查这个连接是否可用和可复用
if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
// 连接不可用,在call中移除该连接,并返回该连接对应的Socket,随后要关闭它
toClose = call.releaseConnectionNoEvents()
}
}
// If the call's connection wasn't released, reuse it. We don't call connectionAcquired() here
// because we already acquired it.
// 如果连接可以使用,那么就返回该连接
if (call.connection != null) {
check(toClose == null)
return callConnection
}
// The call's connection was released.
// 关闭Socket
toClose?.closeQuietly()
eventListener.connectionReleased(call, callConnection)
}
// We need a new connection. Give it fresh stats.
refusedStreamCount = 0
connectionShutdownCount = 0
otherFailureCount = 0
// 2.尝试从连接池中获取连接(第一次)
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
// 返回连接
return result
}
// Nothing in the pool. Figure out what route we'll try next.
// 连接池里没有东西,计算下一条要尝试的路由
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()) {
// 从现有的routeSelection中获取一个路由
routes = null
route = routeSelection!!.next()
} else {
// 计算一个新的routeSelector,这是一个阻塞操作
var localRouteSelector = routeSelector
// 如果routeSelector为null,那么就先创建一个RouteSelector
if (localRouteSelector == null) {
localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
this.routeSelector = localRouteSelector
}
// 从routeSelector中获取一个新的routeSelection
val localRouteSelection = localRouteSelector.next()
routeSelection = localRouteSelection
// 获取routeSelection中的路由列表
routes = localRouteSelection.routes
if (call.isCanceled()) throw IOException("Canceled")
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. We have a better chance of matching thanks to connection coalescing.
// 3.现在我们有了一组IP地址,再次尝试从连接池中获取连接,由于连接合并,这次我们有更大的希望
// 从连接池里获取一个连接(第二次)
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
// 返回连接
return result
}
// 从routeSelection中获取一个路由,用于新连接的创建
route = localRouteSelection.next()
}
// Connect. Tell the call about the connecting call so async cancels work.
// 4.创建新连接
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
// 执行TCP+TLS握手(Https请求才会做TLS握手),这是一个阻塞的操作
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
} finally {
call.connectionToCancel = null
}
call.client.routeDatabase.connected(newConnection.route())
// If we raced another call connecting to this host, coalesce the connections. This makes for 3
// different lookups in the connection pool!
// 5.如果有另一个调用也是连接到相同的主机,并且该调用已经创建了新连接,将连接放到了连接池里,
// 那么就使用连接池里的连接(第三次)
if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
val result = call.connection!!
// 保存路由
nextRouteToTry = route
// 将前面新创建的连接的Socket关闭
newConnection.socket().closeQuietly()
eventListener.connectionAcquired(call, result)
// 返回连接池中的连接
return result
}
/* 第三次在连接池中获取连接,依然没找到,意味着要使用新创建的连接 */
synchronized(newConnection) {
// 6.将先创建的连接放进连接池里面
connectionPool.put(newConnection)
call.acquireConnectionNoEvents(newConnection)
}
eventListener.connectionAcquired(call, newConnection)
// 返回新创建的连接
return newConnection
}
获取一个可用的连接,分为了5步
- 重用
call
当中的连接 - 第一次尝试从连接池获取连接
- 第二次尝试从连接池获取连接
- 自己新创建一个连接
- 第三次尝试从连接池获取连接
重用call
的连接的逻辑:在获取了call
的连接之后,对该连接做了两个判断,分别是
- 判断是否不再接受新的连接
- 判断和当前请求是否有相同的主机名和端口号
关于第二点,既然要复用连接,那么就需要「该请求需要连接到的地方」和「已有连接指向的地方」是同一个地方,同一个地方怎么判断?通过主机名和端口号判断。
如果call
中的连接无法复用,就会调用call
的releaseConnectionNoEvents
方法,释放该连接,如下
# RealCall
internal fun releaseConnectionNoEvents(): Socket? {
val connection = this.connection!!
...
// 将RealCall的connection属性置空
this.connection = null
...
}
还有一个问题,为什么有可能在call
中就已经存在了一个连接呢,我们才刚开始寻找连接呢?还记得我们前面提到的RetryAndFollowUpInterceptor
拦截器吗?当请求失败需要重试或者重定向的时候,这时候连接还在呢,是可以直接进行复用的。
在上面获取连接的时候,还存在的问题是
- 为什么要三次从连接池当中获取连接?它们之间有什么区别?
- 出现了
route
、routeSelection
、routeSelector
的概念,它们分别是什么?
要解答上面的问题,我们需要一些前置知识:OkHttp
中的代理与路由、Http2
的合并连接机制。
代理与路由
代理即代理服务器(Proxy Server
),代理服务器是介于客户端和服务器之间的一台服务器,客户端发送给服务器的请求都由代理服务器进行转发,如果没有代理,则客户端直接与服务器进行交互。通过代理服务器,客户端可以隐藏身份,防止受到外来攻击。
在OkHttp
中出现两种代理类型:
HTTP
代理:能够代理客户端进行HTTP
访问,主要是代理浏览器访问网页,它的端口号一般为80
、8080
SOCKS
代理:SOCKS
代理与其他类型的代理不同,它只是简单地传递数据包,并不关心是何种应用协议,因此SOCKS
代理服务器比其他类型的代理服务器速度要快得多。
其中SOCKS4
只支持TCP
协议,而SOCKS5
既支持TCP
协议又支持UDP
协议。
在OkHttp
中,对于SOCKS
代理,代理服务器完成TCP
数据包的转发工作,而HTTP
代理,除了转发数据之外,还会解析HTTP
的请求及响应,并根据请求及响应的内容做一些处理。
代理
Proxy
在Java
中,通过Java.net.Proxy
类来描述一个代理服务器:
public class Proxy {
public enum Type {
// 不使用代理
DIRECT,
// HTTP代理
HTTP,
// SOCKS代理
SOCKS
};
// 代理类型
private Type type;
// Socket地址
private SocketAddress sa;
...
}
该类主要包含了代理类型以及代理服务器对应的SocketAddress
,其中代理类型有三种:
DIRECT
:不使用代理HTTP
:使用HTTP代理SOCKS
:使用SOCKS代理
ProxySelector
ProxySelector
可以根据用户传入的URI
,返回该URI
对应的代理服务器列表,即List<Proxy>
public abstract class ProxySelector {
private static ProxySelector theProxySelector;
static {
try {
Class<?> c = Class.forName("sun.net.spi.DefaultProxySelector");
if (c != null && ProxySelector.class.isAssignableFrom(c)) {
// 默认的代理选择器
theProxySelector = (ProxySelector) c.newInstance();
}
} catch (Exception e) {
theProxySelector = null;
}
}
// getDefault方法用于获取当前已注册的代理选择器,若用户没有注册代理选择器,那么该方法返回一个
// 一个系统提供的代理选择器
public static ProxySelector getDefault() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.GET_PROXYSELECTOR_PERMISSION);
}
return theProxySelector;
}
// setDefault方法用于注册一个代理选择器
public static void setDefault(ProxySelector ps) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.SET_PROXYSELECTOR_PERMISSION);
}
theProxySelector = ps;
}
// 子类实现该方法,外界通过调用select方法,可以获取该URI所有适用的代理
public abstract List<Proxy> select(URI uri);
// 子类实现该方法,外界调用该方法,通知代理服务器不可用
public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe);
}
ProxySelector
是一个抽象类,在OkHttp
中只有一个实现类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?) {
}
}
无论外界传入什么URI
,该代理选择器都会返回一个包含Proxy.NO_PROXY
的列表,Proxy.NO_PROXY
是Proxy
内部已经预定义好的,如下
public class Proxy {
...
public final static Proxy NO_PROXY = new Proxy();
private Proxy() {
type = Type.DIRECT;
sa = null;
}
...
}
可以看出,Proxy.NO_PROXY
表示的代理类型为Type.DIRECT
,也就是不使用代理。
所以,对于NullProxySelector
,外界调用它的select
方法,无论传入的URI
是什么,它都会返回一个不使用代理的Proxy NO_PROXY
。
用户可以在初始化OkHttpClient
的时候,对proxy
和proxySelector
进行配置
@get:JvmName("proxy") val proxy: Proxy? = builder.proxy
@get:JvmName("proxySelector") val proxySelector: ProxySelector =
when {
// Defer calls to ProxySelector.getDefault() because it can throw a SecurityException.
builder.proxy != null -> NullProxySelector
else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector
}
用户设置的Proxy | 用户设置的ProxySelector | OkHttpClient中的Proxy | OkHttpClient中的ProxySelector |
---|---|---|---|
null |
null |
null |
系统提供的ProxySelector 或NullProxySelector |
null |
ProxySelector |
null |
用户设置的ProxySelector |
Proxy |
null |
Proxy |
NullProxySelector |
Proxy |
ProxySelector |
Proxy |
NullProxySelector |
路由
Route
在OkHttp
中抽象出Route
来描述网络数据包的传输路径,最主要还是描述直接与其建立TCP
连接的目标端点,它表示一个路由信息
class Route(
// 记录请求url相关信息,包括请求的源服务器主机名、端口等信息
@get:JvmName("address") val address: Address,
// 此路由的代理服务器信息
@get:JvmName("proxy") val proxy: Proxy,
// 连接目标地址
@get:JvmName("socketAddress") val socketAddress: InetSocketAddress
) {
...
}
Route
中主要记录了这条路由通过的代理服务器信息Proxy
、连接目标地址InetSocketAddress
,根据代理协议的不同,这里的InetSocketAddress
会有不同的含义:
- 不使用代理:它包含的信息是
HTTP
服务器经过了DNS
解析的IP
地址以及协议的端口号。 HTTP
代理:它包含的信息是代理服务器经过DNS
解析的IP
地址以及端口号。SOCKS
代理:它包含的信息是HTTP
服务器的域名和协议端口号。
RouteSelector
在OkHttp
中通过RouteSelector
类来管理所有的路由信息,并选择可用路由。RouteSelector
的主要工作:
- 收集所有可用的路由
- 选择可用的路由
- 维护连接失败的路由信息
收集所有可用的路由
收集路由分为两个步骤:
-
收集所有的代理
-
收集特定代理服务器选择情况下的所有路由
a. 收集该代理对应的连接目标地址
b. 利用
address
、代理、连接目标地址等信息,构建路由
先看第一步如何收集所有的代理
class RouteSelector(
private val address: Address,
private val routeDatabase: RouteDatabase,
private val call: Call,
private val eventListener: EventListener
) {
/* State for negotiating the next proxy to use. */
private var proxies = emptyList<Proxy>()
private var nextProxyIndex: Int = 0
/* State for negotiating the next socket address to use. */
private var inetSocketAddresses = emptyList<InetSocketAddress>()
/* State for negotiating failed routes */
private val postponedRoutes = mutableListOf<Route>()
init {
resetNextProxy(address.url, address.proxy)
}
...
}
在RouteSelector
的构造方法中,调用到了resetNextProxy
,该方法就是用于收集所有代理,该方法传入了两个参数,分别是address.url
和address.proxy
,其实就是用户要请求的url
和用户手动设置的代理,后面会提到这个address
的构造时机
private fun resetNextProxy(url: HttpUrl, proxy: Proxy?) {
fun selectProxies(): List<Proxy> {
// 如果用户指定了代理,则使用用户指定的代理
if (proxy != null) return listOf(proxy)
// 如果URI缺少了host,那么就不使用代理
val uri = url.toUri()
if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)
// address的proxySelector其实就是OkHttpClient的proxySelector,它的类型在上面已经
// 提过了,这里使用proxySelector寻找URI可用的代理
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)
}
在这个方法中,就完成了代理的收集。
当我们调用RouteSelector
的next
方法的时候,就可以获取一个Selection
,这个方法其实就是收集一个特定代理服务器选择情况下的所有路由:
@Throws(IOException::class)
operator fun next(): Selection {
// 没有下一个代理,并且没有被延迟的路由,则抛出异常
if (!hasNext()) throw NoSuchElementException()
// Compute the next set of routes to attempt.
// 计算下一组要尝试的路由
val routes = mutableListOf<Route>()
// 存在需要尝试的代理
while (hasNextProxy()) {
// 延迟路由总是最后被尝试
val proxy = nextProxy()
// 遍历某个代理下的连接目标地址
for (inetSocketAddress in inetSocketAddresses) {
// 根据连接目标地址创建路由
val route = Route(address, proxy, inetSocketAddress)
// 如果该路由最近连接失败,则延迟它
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes += route
} else {
routes += route
}
}
// 如果没有可用路由,继续尝试下一个代理服务器
if (routes.isNotEmpty()) {
break
}
}
// 遍历完所有代理服务器,依然没有找到可用路由,使用之前记录的延迟路由(最近连接失败的路由)
if (routes.isEmpty()) {
// We've exhausted all Proxies so fallback to the postponed routes.
routes += postponedRoutes
postponedRoutes.clear()
}
// 返回可用的路由集合
return Selection(routes)
}
我们查看nextProxy
方法,该方法用于获取下一个代理,并且计算该代理对应的连接目标地址:
// 返回下一个尝试的代理,可能是PROXY.NO_PROXY但不可能为null
@Throws(IOException::class)
private fun nextProxy(): Proxy {
if (!hasNextProxy()) {
throw SocketException(
"No route to ${address.url.host}; exhausted proxy configurations: $proxies")
}
// 获取下一个proxy
val result = proxies[nextProxyIndex++]
// 计算该proxy对应的连接目标地址
resetNextInetSocketAddress(result)
// 返回proxy
return result
}
我们查看resetNextInetSocketAddress
方法,该方法用于计算某个代理对应的连接目标地址
// 为当前的proxy收集socket addresses
@Throws(IOException::class)
private fun resetNextInetSocketAddress(proxy: Proxy) {
// 通过新建一个列表,清空上一个代理服务器的连接地址
val mutableInetSocketAddresses = mutableListOf<InetSocketAddress>()
inetSocketAddresses = mutableInetSocketAddresses
// 记录主机名和端口号
val socketHost: String
val socketPort: Int
// 不使用代理或者使用SOCKS代理,记录HTTP服务器主机名以及协议端口号
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
socketHost = address.url.host
socketPort = address.url.port
} else {
// 使用HTTP代理,记录代理服务器的主机名以及端口号
val proxyAddress = proxy.address()
require(proxyAddress is InetSocketAddress) {
"Proxy.address() is not an InetSocketAddress: ${proxyAddress.javaClass}"
}
socketHost = proxyAddress.socketHost
socketPort = proxyAddress.port
}
// 端口号不合法
if (socketPort !in 1..65535) {
throw SocketException("No route to $socketHost:$socketPort; port is out of range")
}
// 使用SOCKS代理
if (proxy.type() == Proxy.Type.SOCKS) {
// 根据HTTP服务器的主机名和协议端口号,创建连接目标地址,此时创建的InetSocketAddress
// 实例是未经解析的
mutableInetSocketAddresses += InetSocketAddress.createUnresolved(socketHost, socketPort)
} else {
// 不使用代理或者使用HTTP代理,则会进行DNS解析
eventListener.dnsStart(call, socketHost)
// Try each address for best behavior in mixed IPv4/IPv6 environments.
// 对Http服务器的主机名进行DNS解析
val addresses = address.dns.lookup(socketHost)
if (addresses.isEmpty()) {
throw UnknownHostException("${address.dns} returned no addresses for $socketHost")
}
eventListener.dnsEnd(call, socketHost, addresses)
// 为每个解析到的IP地址创建连接目标地址
for (inetAddress in addresses) {
mutableInetSocketAddresses += InetSocketAddress(inetAddress, socketPort)
}
}
}
从上面的代码可以看出,一个特定代理服务器选择下的连接目标地址因代理类型不同而不同
- 直接连接:对
HTTP
服务器域名进行解析,并为每个解析到的IP
地址创建连接目标地址。 - 使用
SOCKS
代理:直接以HTTP
服务器域名以及协议端口号创建连接目标地址。 - 使用
HTTP
代理:对代理服务器的域名进行解析,并为每个解析到的IP
地址创建连接目标地址。
通过以上对RouteSelector
的分析,我们可以知道
- 在创建
RouteSelector
的时候,它会先在构造函数中收集所有的代理 - 当调用
RouteSelector
的next
方法的时候,它会返回一个Selection
对象,Selection
对象代表的是某个代理服务器下可用的路由集合,代理服务器有三种类型:不使用代理、使用HTTP
代理、使用SOCKS
代理 - 在获取
Selection
对象的时候,会先获取该代理对应的连接目标地址列表,再根据连接目标地址列表去构造该代理对应的路由Route
列表 RouteSelector
会先尝试某个代理对应的Selection
,如果所有代理下都找不到可用的路由,那么就使用「延迟路由列表」去构建一个Selection
对象,也就是说,延迟路由总是最后被尝试
Selection
是RouteSelector
的一个内部类,它的实现很简单
class Selection(val routes: List<Route>) {
private var nextRouteIndex = 0
operator fun hasNext(): Boolean = nextRouteIndex < routes.size
operator fun next(): Route {
if (!hasNext()) throw NoSuchElementException()
return routes[nextRouteIndex++]
}
}
调用Selection
的next
方法,即可获取该Selection
的下一个路由Route
另外,一个域名是可以对应多个IP地址的,可以参考 一个域名最多能对应几个IP地址?。结合上面的分析,我们可以画出下面的这个图
当然,前面提到过,Proxy
有三种类型,每种类型的连接目标地址InetSocketAddress
会有不同,也就是上面的IP
和Port
会有不同,根据Proxy
类型的不同,InetSocketAddress
会有不同的含义:
- 不使用代理:它包含的信息是
HTTP
服务器经过了DNS
解析的IP
地址以及协议的端口号。 HTTP
代理:它包含的信息是代理服务器经过DNS
解析的IP
地址以及端口号。SOCKS
代理:它包含的信息是HTTP
服务器的域名和协议端口号。
现在还有一个问题,ExchangeFinder
、Address
和RouteSelector
类分别是什么时候创建的呢?ExchangeFinder
是在RetryAndFollowUpInterceptor
当中被创建的
class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
...
var newExchangeFinder = true
...
while (true) {
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
...
}
}
...
}
这里调用了RealCall
的enterNetworkInterceptorExchange
方法,该方法会根据newExchangeFinder
的值,判断是否要创建一个ExchangeFinder
,newExchangeFinder
表示是否要创建一个ExchangeFinder
fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
...
if (newExchangeFinder) {
this.exchangeFinder = ExchangeFinder(
connectionPool,
createAddress(request.url),
this,
eventListener
)
}
}
这里创建了一个ExchangeFinder
,并将其引用存储在RealCall
当中,另外createAddress
方法会去创建一个Address
对象,如下
private fun createAddress(url: HttpUrl): Address {
var sslSocketFactory: SSLSocketFactory? = null
var hostnameVerifier: HostnameVerifier? = null
var certificatePinner: CertificatePinner? = null
if (url.isHttps) {
sslSocketFactory = client.sslSocketFactory
hostnameVerifier = client.hostnameVerifier
certificatePinner = client.certificatePinner
}
return Address(
uriHost = url.host,
uriPort = url.port,
dns = client.dns,
socketFactory = client.socketFactory,
sslSocketFactory = sslSocketFactory,
hostnameVerifier = hostnameVerifier,
certificatePinner = certificatePinner,
proxyAuthenticator = client.proxyAuthenticator,
proxy = client.proxy,
protocols = client.protocols,
connectionSpecs = client.connectionSpecs,
proxySelector = client.proxySelector
)
}
其中url
参数就是请求的url
。从上面可以看出,我们创建的Address
对象会存储url
的host
和port
信息,另外Address
对象还有一些其它的信息,这些信息均取自OkHttpClient
。
另外,一个RouteSelector
对象,可能会在ExchangeFinder
的findConnection
方法中被创建
@Throws(IOException::class)
private fun findConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean
): RealConnection {
...
if (nextRouteToTry != null) {
...
} else if (routeSelection != null && routeSelection!!.hasNext()) {
...
} 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
...
}
}
插叙:逻辑梳理
在介绍了上面的知识后,我们将目前逻辑梳理一下。
首先,在RetryAndFollowUpInterceptor
的intercept
方法里面,会调用RealCall
的enterNetworkInterceptorExchange
方法,在该方法里面,会根据newExchangeFinder
的值,去判断是否要创建一个新的ExchangeFinder
对象,若创建,则将ExchangeFinder
对象的引用存于RealCall
中
class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
...
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
...
}
...
}
class RealCall(
val client: OkHttpClient,
val originalRequest: Request,
val forWebSocket: Boolean
) : Call {
...
fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
...
if (newExchangeFinder) {
this.exchangeFinder = ExchangeFinder(
connectionPool,
createAddress(request.url),
this,
eventListener
)
}
}
...
}
创建ExchangeFinder
对象的同时,也会调用createAddress
方法,去创建一个Address
对象,Address
会记录请求url
的host
和port
。
接着在获取一个健康可用连接的时候,调用链为 RealCall::initExchange -> ExchangeFinder::find -> ExchangeFinder::findHealthyConnection -> ExchangeFinder::findConnection
,在ExchangeFinder
的findConnection
方法里面,若RouteSelector
还没创建,就会去创建RouteSelector
,逻辑如下
@Throws(IOException::class)
private fun findConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean
): RealConnection {
...
val routes: List<Route>?
val route: Route
if (nextRouteToTry != null) {
...
} else if (routeSelection != null && routeSelection!!.hasNext()) {
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
...
route = localRouteSelection.next()
}
...
}
我们重点关注If
的这大段语句,nextRouteToTry
先不去管它,暂时认为它为null
。上面routeSelector
和routeSelection
的类型分别是RouteSelector
、RouteSelector.Selection
,它们的含义在上面已经介绍过了。
从ExchangeFinder
的findHealthyConnection
方法我们可以知道,findConnection
方法是有可能多次被调用的,在第一次被调用的时候,routeSelector
和routeSelection
均为null
,因此会在else
分支下创建RouteSelector
对象,并且会调用routeSelector
的next
方法,获取一个Selection
并将其赋值给routeSelection
,然后再调用routeSelection
的next
方法,获取一个路由route
。在第二次进入findConnection
方法的时候,会先在else if
语句判断routeSelection
是否还有可用的路由,如果有,则直接获取routeSelection
的下一个路由,如果当前routeSelection
的路由已经耗尽,则会在else
分支中,使用routeSelector
的next
方法获取下一个routeSelection
,然后再从routeSelection
中获取一个路由route
。
选择可用路由
RouteSelector
选择可用路由的逻辑主要在next()
方法中,上面已经对next()
方法进行了分析:
- 如果当前有可被尝试的代理服务器,获取该代理服务器对应的所有连接目标地址。
- 根据连接目标地址创建对应路由,如果该路由最近连接失败,将该路由加入延迟路由集合,否则加入正常路由集合。
- 如果当前有正常路由,返回正常路由集合,否则继续1、2步骤。
- 遍历完所有可尝试代理服务器仍然没有正常路由,则返回延迟路由集合(最近连接失败的路由)。
维护路由连接失败信息
RouteSelector
借助RouteDatabase
维护失败的路由信息,避免浪费时间去连接一些不可用的路由:
class RouteDatabase {
private val failedRoutes = mutableSetOf<Route>()
/** Records a failure connecting to [failedRoute]. */
@Synchronized fun failed(failedRoute: Route) {
failedRoutes.add(failedRoute)
}
/** Records success connecting to [route]. */
@Synchronized fun connected(route: Route) {
failedRoutes.remove(route)
}
/** Returns true if [route] has failed recently and should be avoided. */
@Synchronized fun shouldPostpone(route: Route): Boolean = route in failedRoutes
}
该类实现非常简单,里面维护了一个连接失败路由容器,当路由连接失败,调用failed
方法将该路由加入容器中,当路由连接成功,调用connected
方法将该路由从容器中删除,通过调用shouldPostpone
方法判断该路由是否在连接失败路由容器中,从而知道该路由最近是否连接失败。
HTTP中的连接复用机制
在OkHttp
中出现了「连接合并」的概念,它和HTTP/2
的多路复用机制有关。我们先介绍HTTP中的连接复用机制,然后再介绍OkHttp
中的连接复用机制。
虽然说 HTTP 是一个无连接、无状态的协议,但由于它基于 TCP,因此也存在着管理 TCP 连接的管理问题。
HTTP/1.0 及之前
使用TCP协议往往存在粘包问题,出现粘包问题的核心原因是:TCP 协议是面向字节流的,它的数据并没有边界。
由于 TCP 连接的数据之间没有数据边界,从而导致 HTTP 数据包粘连,无法处理,因此 HTTP/1.0 及之前,采用了一种非常暴力的方式进行解决:
每进行一次 HTTP 通信,就要先建立一条 TCP 连接,在通信结束后,就需要断开一次 TCP 连接,而每次TCP 连接的建立需要经过三次握手,而 TCP 连接的关闭需要四次挥手,这显然非常浪费资源。
其实 HTTP/1.0 中还引入了 Connection:Keep-Alive
这个 Header,它就是用来支持 TCP 连接的维持的,但在 大部分基于 HTTP/1.0 的服务器中并没有实现。
HTTP/1.1
HTTP/1.1真正引入Keep-Alive
机制,默认开启,可以通过Connection:close
关闭。在 HTTP 通信结束时,若启用了 Keep-Alive
机制,则该连接并不会立即关闭,此时如果有新的请求到来,且 host 和 port 相同,则会复用这条 TCP 连接进行请求,减少了 TCP 连接的频繁建立与关闭的资源消耗。
这样就避免了每次HTTP通信都要建立和关闭 TCP 连接,从而也就避免了频繁的握手和挥手(三次握手和四次挥手),使得 HTTP 请求的效率大大提高
粘包问题
现在多个HTTP请求可以复用同一条TCP连接,而HTTP数据包也没有通过分隔符确定数据的头和尾,那么它是如何解决粘包的问题的呢?
实际上它是通过 Content-Length
这个 Header 解决的,它标明了数据部分所占用的大小,从而可以通过它来确定这个数据包的边界,避免粘包。这也是 HTTP/1.1 引入 Content-Length
的原因。
同时,HTTP/1.1 中还有一个 Keep-Alive
请求头,可以对 timeout
和 max
进行设置,用来指定空闲连接保持打开的时间以及连接关闭前这条连接可以发送请求数的最大值。
HTTP/2.0
HTTP1.1 存在的问题
HTTP/1.1 中,虽然实现了 TCP 连接的复用,但仍有如下几个缺陷:
- 如果客户端想要发起并行的请求,则必须建立多个 TCP 连接,这对网络资源的消耗也是十分严重的。
- 不会读对请求及响应的 Header 进行压缩,造成了网络流量的浪费。
关于第一点:例如,打开一个网页,浏览器会先向服务器请求一份HTML文件,在拿到HTML文件后,就需要向服务器请求HTML中的图片等资源。若是使用HTTP 1.1协议,虽然TCP连接是可以复用的,但是TCP连接上无法并行请求,需要等到上一个请求结束后,才能复用该 TCP 连接进行下一个请求,如果想并行请求,只能建立多个TCP连接,在每个TCP连接上都进行一个HTTP请求;若是使用HTTP2.0协议,则可以在同一个TCP连接上并行地发送多个HTTP请求,同时去请求HTML文件中的多份资源,然后等待服务端的响应,这就是HTTP2.0的多路复用机制。
多路复用
HTTP/2.0 引入了一种多路复用机制,同时引入了几个新的概念:
- 数据流:基于 TCP 连接上的一个双向的字节流,每发起一个请求,就会建立一个数据流,后续的请求过程的数据传递都通过该流进行
- 数据帧:HTTP/2 中的数据最小切片单位,其中又分为了
Header Frame
、Data Frame
等等。 - 消息:一个请求或响应对应的一系列数据帧。
引入了这些概念之后,在 HTTP 请求的过程中,服务端/客户端首先会将我们的请求/响应切分为不同的数据帧,当另一方接收到后再将其组装从而形成完整的请求/响应,如下所示
这样,就实现了对 TCP 连接的多路复用,将一个请求或响应分为了一个个的数据帧,使得多个请求可以并行地进行。
多路复用与 Keep-Alive 的区别
- Keep-Alive 机制虽然解决了复用 TCP 连接问题,但没有解决请求阻塞的问题,需要等到上一个请求结束后,才能复用该 TCP 连接进行下一个请求。
- HTTP/1.x 对数据的传递仍然是以一个整体进行传递,而在 HTTP/2 中引入了数据帧的概念,使得多个请求可以同时在流中进行传递。
- HTTP/2 采用了 HPACK 压缩算法对 Header 进行压缩,降低了请求的流量消耗。
连接合并
英文名为:connection coalescing。
「连接合并」出现的背景:在 HTTP/1.1 中,浏览器为了更快地获取数据,对于每个「主机名+端口号」会使用 6 个连接。有些网站站点还会启用更多的主机名,这些主机名往往被解析为相同的 IP 地址或者 IP 地址集合,网站站点这么做的目的是为了触发浏览器使用更多的连接。在 HTTP/2 引入了多路复用后,它允许多个流在一个连接上同时进行传输,那么原先为了让浏览器使用更多连接而使用多个主机名的网站站点,与现在 HTTP/2 浏览器使用一个 TCP 连接进行多路复用的意思,就背道而驰了。那些网站站点不希望切换回单一的主机名,主要是基于下面的考虑
- 这会是一个重大的架构变化。
- 还有一些浏览器仍然是在使用 HTTP/1.1。
「连接合并」:对于连接合并,不同的浏览器有不同的做法,一些浏览器甚至不去处理它。下面是个例子:假设某个站点 "example.com" 在 DNS 中有两个主机名,分别是 "A.example.com" 和 "B.example.com",每个主机名都对应着一个 IP 列表。另外,HTTP/2 是基于 HTTPS 的,使用 HTTP/2 的浏览器,在 TLS 握手过程中可以获取服务端的两个主机名 "A.example.com"、"B.example.com"。假设两个主机名 DNS 解析后的 IP 列表为:
# A.example.com
192.168.0.1 and 192.168.0.2
# B.example.com
192.168.0.2 and 192.168.0.3
下面是 Chrome 浏览器的连接合并的做法
- 假设一开始浏览器连接到了A站点的 192.168.0.1,现在有个要连接 "B.example.com" 的请求,浏览器会先去 DNS 解析获取它的 IP 列表,发现其中并没有 192.168.0.1,于是会为 "B.example.com" 创建一个新的连接。
- 假设一开始浏览器连接到了A站点的 192.168.0.2,现在有个要连接 "B.example.com" 的请求,浏览器会先去 DNS 解析获取它的 IP 列表,发现B对应的 IP 地址也有 192.168.0.2,于是浏览器会重用到A站点的 TCP 连接,去请求到B站点的数据。
OkHttp的连接复用机制
OkHttp使用连接池实现了连接复用机制,我们看下连接池ConnectionPool
的实现。
ConnectionPool
ConnectionPool
只是连接池的入口,真正的实现其实是RealConnectionPool
。
初始化
在OkHttpClient.Builder
中默认创建一个ConnectionPool
对象,用户也可以自己传入一个ConnectionPool
对象
open class OkHttpClient internal constructor(
builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {
...
val connectionPool: ConnectionPool = builder.connectionPool
...
class Builder constructor() {
...
internal var connectionPool: ConnectionPool = ConnectionPool()
...
fun connectionPool(connectionPool: ConnectionPool) = apply {
this.connectionPool = connectionPool
}
...
}
}
ConnectionPool
的实现如下
class ConnectionPool internal constructor(
internal val delegate: RealConnectionPool
) {
constructor(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) : this(RealConnectionPool(
taskRunner = TaskRunner.INSTANCE,
maxIdleConnections = maxIdleConnections,
keepAliveDuration = keepAliveDuration,
timeUnit = timeUnit
))
constructor() : this(5, 5, TimeUnit.MINUTES)
/** Returns the number of idle connections in the pool. */
fun idleConnectionCount(): Int = delegate.idleConnectionCount()
/** Returns total number of connections in the pool. */
fun connectionCount(): Int = delegate.connectionCount()
/** Close and remove all idle connections in the pool. */
fun evictAll() {
delegate.evictAll()
}
}
可以看出,其内部依靠了RealConnectionPool
实现。
RealConnectionPool
我们查看RealConnectionPool
的构造参数和成员变量
class RealConnectionPool(
taskRunner: TaskRunner,
/** The maximum number of idle connections for each address. */
private val maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
private val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration)
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce() = cleanup(System.nanoTime())
}
private val connections = ConcurrentLinkedQueue<RealConnection>()
...
}
由ConnectionPool
对RealConnectionPool
对象的构造,我们可以知道默认情况下,OkHttp中的连接池最大空闲连接的数量为5,并且最大的空闲时间为5分钟,这里的最大空闲连接数量是相对于一个address
而言。
keepAliveDurationNs
也就是将空闲时间使用纳秒来表示。
connections
用于存放连接。
上面还出现了Task cleanupTask
和TaskQueue cleanupQueue
这两个对象。
Task
类:从Task
类的注释可以得知,它代表一个可以执行一次或多次的任务。
- 复发性:
Task
是一个抽象类,它有一个抽象方法runOnce
,子类需要去实现该方法,在该方法中定义要执行的任务,runOnce
返回一个Long
值,该值表示该Task
下一次被调度执行的延时,特别地,若该值为-1,则表示该Task
不再被调度执行。 - 可取消:
Task
存放于TaskQueue
当中,当Task
在队列中等待被执行或者正在执行时,是可以被取消的,取消一个在等待被执行的Task
,那么该Task
就不会被执行了,取消一个正在执行的Task
,不会影响该Task
的本次执行,但之后该Task
就不会再被调度执行了。 - 与
TaskQueue
关系:一个Task
会被绑定到一个TaskQueue
,一个TaskQueue
中可以有多个Task
,但一个Task
只可以对应一个TaskQueue
,TaskQueue
中的Task
是有顺序的,队列中的Task
是不会并发执行的。
TaskQueue
类:里面存放着一组任务,这组任务按顺序执行,不是并发执行。
在Task
类的runOnce
方法中,调用了cleanup
方法,该方法执行「清理空闲连接」的任务,后面会提到。
RealConnectionPool
中几个比较重要的操作方法为:put
、cleanup
、callAcquirePooledConnection
、connectionBecameIdle
,分别对应放入连接、清理空闲连接、获取可用连接、通知连接空闲操作。
放入连接
放入连接对应put
方法,RealConnectionPool::put
如下
fun put(connection: RealConnection) {
connection.assertThreadHoldsLock()
// 存入队列
connections.add(connection)
// 启动清理空闲连接的任务
cleanupQueue.schedule(cleanupTask)
}
该方法会将connection
存入连接缓存池中,并且启动清理空闲连接的任务。
清理空闲连接
cleanupTask
表示一个清理空闲连接的任务,该任务会执行cleanup
方法
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce() = cleanup(System.nanoTime())
}
cleanup
方法用于清理空闲连接,如下
// 该方法用于维护连接池,如果有「空闲时间」超过「允许的最长空闲时间」的连接,或者「空闲连接的数量」
// 超过「允许的最大空闲连接数量」,那么就从连接缓存池中移除「空闲时间最长的连接」
fun cleanup(now: Long): Long {
// 正在使用的连接数量
var inUseConnectionCount = 0
// 空闲的连接数量
var idleConnectionCount = 0
// 空闲时间最长的连接
var longestIdleConnection: RealConnection? = null
// 空闲时间最长的连接 的 空闲时间
var longestIdleDurationNs = Long.MIN_VALUE
// Find either a connection to evict, or the time that the next eviction is due.
// 找到需要移除的连接,或者计算下一次需要清理的时间
for (connection in connections) {
synchronized(connection) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
// 如果当前连接正在使用
inUseConnectionCount++
} else {
// 如果当前连接是空闲的
idleConnectionCount++
// If the connection is ready to be evicted, we're done.
// 计算连接的空闲时间
val idleDurationNs = now - connection.idleAtNs
// 与之前的空闲时间进行对比,记录空闲时间最长的连接、以及其空闲时间
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
} else {
Unit
}
}
}
}
when {
// 最长空闲时间 超过 允许的最长空闲时间,或者,空闲连接的数量 超过 允许的最大空闲数量
// 表示要清理连接了,这时候会选择「空闲时间最久」的连接进行清理
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
// 空闲时间最长的连接
val connection = longestIdleConnection!!
synchronized(connection) {
// 不再是空闲连接,立即执行下次清理任务
if (connection.calls.isNotEmpty()) return 0L
// 不再是空闲时间最长的,立即执行下次清理任务
if (connection.idleAtNs + longestIdleDurationNs != now) return 0L
// 标记该连接不可用
connection.noNewExchanges = true
// 从连接缓存池中移除该连接
connections.remove(longestIdleConnection)
}
connection.socket().closeQuietly()
// 若当前的连接池为空,那么就没有必要再清理连接了,所以调用TaskQueue的cancelAll方法,
// 取消所有Task,也就是取消「所有清理连接的」任务
if (connections.isEmpty()) cleanupQueue.cancelAll()
// 立即执行下次清理任务
return 0L
}
// 有空闲的连接,但是还没有达到清理的标准(空闲连接 || 空闲连接数)
idleConnectionCount > 0 -> {
// 返回下次执行清理任务的时间
return keepAliveDurationNs - longestIdleDurationNs
}
// 没有连接是空闲的,所有连接都在使用
inUseConnectionCount > 0 -> {
// 待「允许的最长空闲时间」之后,再次执行清理连接的任务
return keepAliveDurationNs
}
// 没有任何的连接
else -> {
return -1
}
}
}
由于该方法在Task
的runOnce
方法中调用,并且其返回值作为runOnce
方法的返回值,前面我们提到过,runOnce
方法的返回值表示该Task
下一次被调度执行的延时,或者返回-1
表示该Task
不再被调度执行,因此这里cleanup
方法的返回值意味着下一次执行「清理空闲连接」任务的延时,或者返回-1
,表示不再执行该「清理空闲连接」的任务。
我们再总结一下cleanup
方法所做的事情:
-
遍历连接缓存池中的连接,记录当前空闲连接数量、正在使用连接数量、当前空闲时间最长的连接及其空闲时间。
-
若最长空闲时间 超过 允许的最长空闲时间,或者,空闲连接的数量 超过 允许的最大空闲数量 ,这时候会从连接缓存池中移除 空闲时间最久 的连接,同时检测当前连接缓存池是否为空,如果为空则取消所有「清理空闲连接」的任务。立即执行下次「清理空闲连接」的任务。 否则往下走。
-
若 有空闲的连接,但是还没有达到清理的标准(空闲连接 || 空闲连接数) ,返回下次执行「清理空闲连接」任务的延时。否则往下走。
-
若 没有连接是空闲的,所有连接都在使用 ,那么就返回
keepAliveDurationNs
,表示下次执行「清理空闲连接」任务的延时。否则往下走。 -
当前连接缓存池中没有任何连接,该「清理空闲连接」任务不需要再执行。
获取可用连接
从连接池获取可用连接对应着callAcquirePooledConnection
方法,RealConnectionPool
的callAcquirePooledConnection
如下
fun callAcquirePooledConnection(
address: Address,
call: RealCall,
routes: List<Route>?, // 请求方路由选择后得到的路由列表
requireMultiplexed: Boolean // 是否要求要「Http/2.0多路复用的连接」
): Boolean {
// 遍历连接缓存池中的连接
for (connection in connections) {
synchronized(connection) {
// 请求方要求要多路复用连接,但该连接不是多路复用的连接,则跳过该连接
if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
// 判断该连接是否符合条件,若不符合则跳过该连接
if (!connection.isEligible(address, routes)) return@synchronized
// RealCall使用该连接
call.acquireConnectionNoEvents(connection)
return true
}
}
return false
}
该方法会遍历连接缓存池中的连接,为请求者寻找一个满足条件的连接,找到了则返回true
,未找到则返回false
。如果该连接满足请求者的条件,则会调用RealCall
的acquireConnectionNoEvents
方法,表示RealCall
获取该连接
fun acquireConnectionNoEvents(connection: RealConnection) {
connection.assertThreadHoldsLock()
check(this.connection == null)
// RealCall中记录该连接
this.connection = connection
connection.calls.add(CallReference(this, callStackTrace))
}
我们还要关注RealConnection
的isEligible
方法,该方法用于判断连接是否符合请求者的条件
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
assertThreadHoldsLock()
// 该connection承载的流数量已经到达最大值,或者该连接不可用,那么返回false
if (calls.size >= allocationLimit || noNewExchanges) return false
// 比较Address的非host字段,如果有不相同的,那么返回false
if (!this.route.address.equalsNonHost(address)) return false
// 如果主机名host也匹配,那么意味着该连接可以复用,返回true
if (address.url.host == this.route().address.url.host) {
return true // This connection is a perfect match.
}
// At this point we don't have a hostname match. But we still be able to carry the request if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 此时 该连接对应的地址 和 请求者的目标地址,虽然它们的host没有匹配上,但是如果它们满足
// 「连接合并」的要求,该连接仍然可以被复用,承载请求
// 若该连接不是Http/2(多路复用)连接,则返回false
if (http2Connection == null) return false
// 2. The routes must share an IP address.
// 用户需要传入routes,并且传入的routes至少要有一个与该连接的路由相匹配
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
// 是否支持该url
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.
}
上面的routeMatchesAny
方法如下:
private fun routeMatchesAny(candidates: List<Route>): Boolean {
return candidates.any {
it.proxy.type() == Proxy.Type.DIRECT &&
route.proxy.type() == Proxy.Type.DIRECT &&
route.socketAddress == it.socketAddress
}
}
可以看出,它返回true
的条件:该coneection
对应的路由是直连,并且要求用户传入的routes
中至少要有一条路由为直连,并且该路由和coneection
的路由的目标地址要相同。
supportsUrl
方法:
private fun supportsUrl(url: HttpUrl): Boolean {
assertThreadHoldsLock()
val routeUrl = route.address.url
if (url.port != routeUrl.port) {
return false // Port mismatch.
}
if (url.host == routeUrl.host) {
return true // Host match. The URL is supported.
}
// We have a host mismatch. But if the certificate matches, we're still good.
return !noCoalescedConnections && handshake != null && certificateSupportHost(url, handshake!!)
}
我们再总结一下获取可用连接的callAcquirePooledConnection
方法:
- 遍历缓存连接池中的连接,对于每个连接,处理如下
- 若用户要求"多路复用"的连接,但该连接不是"多路复用"的连接,则跳过该连接
- 若该连接不满足条件,则跳过该连接
- 用户要求的
address
和connection
的route
,它们host
和port
等信息都匹配,则满足条件 - 若满足
Http/2.0
的「连接合并」的要求,也认为该连接满足条件 - 否则该连接不满足条件
- 用户要求的
- 该连接满足用户的要求,则
RealCall
获取该连接,并返回true
,表示连接池获取连接成功 - 若所有连接都不符合用户要求,则返回
false
,表示连接池获取连接失败
三次连接池的获取的区别
回到ExchangeFinder::findConnection
方法中,它三次从连接池获取连接,它们之间有什么区别呢?
// 第一次从连接池获取连接
connectionPool.callAcquirePooledConnection(address, call, null, false)
// 第二次从连接池获取连接
connectionPool.callAcquirePooledConnection(address, call, routes, false)
// 第三次从连接池获取连接
connectionPool.callAcquirePooledConnection(address, call, routes, true)
它们的区别在于第三个参数和第四个参数,刚刚我们提到了RealConnectionPool
的callAcquirePooledConnection
方法,它用于获取可用的连接
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.acquireConnectionNoEvents(connection)
return true
}
}
return false
}
-
第三个参数
routes
:客户端到目的地(有可能是源服务器,也有可能是代理服务器)的路由列表,它由我们前面介绍的RouteSelector
生成的 -
第四个参数
requireMultiplexed
:表示客户端是否需要「多路复用」的连接,多路复用连接是HTTP/2.0
的特性
三次从连接池获取连接的区别:
- 第一次:
requireMultiplexed
参数为false
,说明先不去管连接池中的连接是不是HTTP/2
的连接(多路复用),只要在isEligible
方法中,判断目标请求地址和connection
对应路由的主机名、端口等信息都匹配,就使用该连接,若不匹配,则不使用该连接(由于routes
参数为null
,因此不会去使用「连接合并」的特性) - 第二次:
requireMultiplexed
参数为false
,说明先不去管连接池中的连接是不是HTTP/2
的连接(多路复用),只要在isEligible
方法中,判断目标请求地址和connection
对应路由的主机名、端口等信息都匹配,就使用该连接,若只有主机名不匹配,由于routes
的参数不为null
,也就是请求方传入经过了路由选择的路由列表,这时候会去判断是否满足「连接合并」的特性,满足则使用该连接,否则不使用 - 第三次:
requireMultiplexed
参数为true
,表明只需要多路复用的连接,在callAcquirePooledConnection
方法中会筛去不是多路复用的连接,对于连接池的每一个多路复用的连接,会先去看主机名、端口等信息是否匹配,匹配则直接使用,若只有主机名不匹配,则判断是否满足「连接合并」的特性,满足则使用该连接,否则不使用
简单总结:
- 第一次:不使用「连接合并」的特性,只有目标请求地址和
connection
对应路由的主机名、端口等信息都匹配,才使用该连接 - 第二次:使用「连接合并」的特性,不管是不是多路复用连接,优先判断主机名和端口等信息是否匹配,匹配则直接使用,若只有主机名不匹配,判断是否符合「连接合并」的要求,满足则使用,否则不使用
- 第三次:使用「连接合并」的特性,这一次只使用多路复用的连接。也是优先判断主机名和端口等信息是否匹配,匹配则直接使用,不匹配则判断是否符合「连接合并」的要求,满足则使用,否则不使用
通知连接空闲
这对应RealConnectionPool
的connectionBecameIdle
方法
/**
* Notify this pool that [connection] has become idle. Returns true if the connection has been
* removed from the pool and should be closed.
*/
// 通知连接池该connection已空闲,如果该connection从连接池中移除则返回true
fun connectionBecameIdle(connection: RealConnection): Boolean {
connection.assertThreadHoldsLock()
// 如果该连接无法承载新的stream,或者连接池允许的最大空闲连接数为0,则从连接池中移除该连接
return if (connection.noNewExchanges || maxIdleConnections == 0) {
connection.noNewExchanges = true
connections.remove(connection)
if (connections.isEmpty()) cleanupQueue.cancelAll()
true
} else {
// 否则执行清理空闲连接的任务
cleanupQueue.schedule(cleanupTask)
false
}
}
PS:当调用RealConnectionPool
的put
方法或者connectionBecameIdle
方法时都立即执行空闲连接清理任务。
建立新连接
在ExchangeFinder::findConnection
中,如果call
中没有可复用的连接,并且前两次从连接池获取连接的时候,也没能成功地获取到连接,那么这时候就会创建一个新的连接
...
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
}
...
我们查看RealConnection
的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)
// 一些异常处理
...
// 开始连接
while (true) {
try {
// 是否需要使用隧道技术
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
// 我们无法连接隧道,但妥善关闭了我们的资源。
break
}
} else {
// 不需要使用隧道技术
connectSocket(connectTimeout, readTimeout, call, eventListener)
}
// 建立协议
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
break
} catch (e: IOException) {
// 出现异常,释放一些资源
...
}
}
// 默认尝试建立隧道的次数为21,超过该最大次数则抛异常
if (route.requiresTunnel() && rawSocket == null) {
throw RouteException(ProtocolException(
"Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS"))
}
idleAtNs = System.nanoTime()
}
这里使用了一个while(true)
循环,直到成功建立连接,主要步骤:
-
判断是否使用隧道技术
-
是 : 调用
connectTunnel
方法 -
否 : 调用
connectSocket
方法
-
-
调用
establishProtocol
方法建立协议
不使用隧道(直接连接)
直接连接,调用的是RealConnection
的connectSocket
方法
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
// 在原始套接字上,构建一个完整的HTTP或HTTPS连接
@Throws(IOException::class)
private fun connectSocket(
connectTimeout: Int,
readTimeout: Int,
call: Call,
eventListener: EventListener
) {
val proxy = route.proxy
val address = route.address
// 根据代理类型,初始化rawSocket
val rawSocket = when (proxy.type()) {
Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
else -> Socket(proxy)
}
this.rawSocket = rawSocket
eventListener.connectStart(call, route.socketAddress, proxy)
rawSocket.soTimeout = readTimeout
try {
// 连接socket,之所以这样写是因为支持不同的平台
// 里面的实现调用了 socket.connect(address, connectTimeout) 方法
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
} catch (e: ConnectException) {
throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
initCause(e)
}
}
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
// 获取source和sink,分别用于输入和输出
source = rawSocket.source().buffer()
sink = rawSocket.sink().buffer()
} catch (npe: NullPointerException) {
if (npe.message == NPE_THROW_WITH_NULL) {
throw IOException(npe)
}
}
}
该方法的大致流程如下:
- 根据代理类型创建
Socket
对象。 - 调用
socket
的connect
方法进行socket
连接,这是 Java 原生的方法,在这里进行了TCP三次握手。 - 调用
Okio
库提供的方法获取输入输出流source
以及sink
。
通过隧道连接
没有看懂 okhttp 在这一部分的操作,这里就简单记录下隧道的概念。
在从IPv4向IPv6迁移的过程中,我们就有提到过建隧道的方法,我们回顾下当时的应用。首先,要明确的一点是IPv6是向后兼容(backward)的,也就是可以发送、路由和接收IPv4数据报,但已部署的具有IPv4能力的系统却不能够处理IPv6数据报。
我们看下面这个图
假定图中的B和E结点要使用IPv6数据报进行交互,但它们是经由中间IPv4路由器互联的,我们将两台IPv6路由器之间的中间IPv4路由器的集合称为一个隧道,借助于隧道,在隧道发送端的B结点可将整个IPv6数据报放到一个IPv4数据报的有效载荷中,该IPv4数据报的地址设为指向隧道接收端的E结点,再发送给隧道中的第一个节点(在此例中为C)。隧道中的中间IPv4路由器在它们之间为该数据报提供路由,就像对待其他数据报一样,完全不知道该IPv4数据报自身就含有一个完整的IPv6数据报。隧道接收端的E节点最终收到该IPv4数据报(它是该IPv4数据报的目的地),并确定该IPv4数据报含有一个IPv6数据报(通过观察在IPv4数据报中的协议号字段是41,指示该IPv4有效载荷是IPv6数据报),从中取出IPv6数据报,然后再为该IPv6数据报提供路由,就好像它是从一个直接相连的IPv6邻居那里接收到该IPv6数据报一样。
说明:IPv4向IPv6迁移只是建隧道其中的一个应用场景,在其它的一些地方也有建隧道的概念与应用。
Http
中如何打开隧道呢?
HTTP 提供了一个特殊的 method
—— CONNECT
,它是 HTTP/1.1 协议中预留的方法,客户端发送一个 CONNECT
请求给隧道网关请求打开一条 TCP
连接,当隧道打通之后,客户端通过 HTTP
隧道发送的所有数据会转发给 TCP
连接,服务器响应的所有数据会通过隧道发给客户端。
建立协议
无论是直接连接,还是通过隧道连接,在建立了Socket
连接之后,都需要调用establishProtocol
方法,建立协议。这一步发生在TCP
连接建立之后,在TCP
连接上发送数据之前,这一步会做TLS
握手(使得数据可以加密传输)、HTTP/2
的协议协商等。
RealConnection::establishProtocol
@Throws(IOException::class)
private fun establishProtocol(
connectionSpecSelector: ConnectionSpecSelector,
pingIntervalMillis: Int,
call: Call,
eventListener: EventListener
) {
// 普通的HTTP请求,而非HTTPS请求,则协议版本定义为HTTP/1.1
if (route.address.sslSocketFactory == null) {
// 协议中包含h2_prior_knowledge
if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
socket = rawSocket
protocol = Protocol.H2_PRIOR_KNOWLEDGE
startHttp2(pingIntervalMillis)
return
}
socket = rawSocket
protocol = Protocol.HTTP_1_1
return
}
eventListener.secureConnectStart(call)
// TLS握手
connectTls(connectionSpecSelector)
eventListener.secureConnectEnd(call, handshake)
// 若同时为HTTPS请求和HTTP/2
if (protocol === Protocol.HTTP_2) {
startHttp2(pingIntervalMillis)
}
}
该方法主要进行以下工作:
- 如果是
HTTP
请求,就将协议protocol
定义为HTTP/1.1
,如果协议中包含了h2_prior_knowledge
,则采用HTTP/2
进行请求,调用startHttp2
方法。 - 如果是
HTTPS
请求,就调用connectTls
方法进行TLS
握手,再判断是不是HTTP/2
协议,从而决定是否调用startHttp2
方法开启HTTP2
连接
开启HTTP2连接
查看RealConnection::startHttp2
@Throws(IOException::class)
private fun startHttp2(pingIntervalMillis: Int) {
val socket = this.socket!!
val source = this.source!!
val sink = this.sink!!
socket.soTimeout = 0 // HTTP/2 connection timeouts are set per-stream.
// 初始化一个Http2连接
val http2Connection = Http2Connection.Builder(client = true, taskRunner = TaskRunner.INSTANCE)
.socket(socket, route.address.url.host, source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build()
this.http2Connection = http2Connection
// 最大并发流数
this.allocationLimit = Http2Connection.DEFAULT_SETTINGS.getMaxConcurrentStreams()
// 开启Http2连接
http2Connection.start()
}
我们在RealConnection
的isEligible
方法中曾见过allocationLimit
的身影,
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
...
if (calls.size >= allocationLimit || noNewExchanges) return false
...
}
当时主要用于判断connection
承载的流数量是否已经到达最大值,从而决定该connection
是否可用,allocationLimit
的初始值是1
,也就是说,在HTTP/1
中,一个connection
同时最多承载一个流,但是在HTTP/2
中,由于有多路复用的机制,它的allocationLimit
值就不再是1
了。
startHttp2
的主要工作就是初始化一个Http2Connection
,将Http2Connection
的listener
设置为RealConnection
本身,接着设置allocationLimit
的值,最后调用Http2Connection.start
方法开启Http2
连接。我们查看Http2Connection::start
方法
// Sends any initial frames and starts reading frames from the remote peer.
// This should be called after Builder.build for all new connections.
// Params:sendConnectionPreface - true to send connection preface frames.
// 该方法必须在Builder.build方法之后调用
@Throws(IOException::class) @JvmOverloads
fun start(sendConnectionPreface: Boolean = true, taskRunner: TaskRunner = TaskRunner.INSTANCE) {
if (sendConnectionPreface) {
// 发送初始帧
writer.connectionPreface()
// 将OkHttp的设置以设置帧的形式发送给对等方
writer.settings(okHttpSettings)
// 窗口更新
val windowSize = okHttpSettings.initialWindowSize
if (windowSize != DEFAULT_INITIAL_WINDOW_SIZE) {
writer.windowUpdate(0, (windowSize - DEFAULT_INITIAL_WINDOW_SIZE).toLong())
}
}
// 开启一个线程,用于读取对等方发送的初始帧和设置帧
taskRunner.newQueue().execute(name = connectionName, block = readerRunnable)
}
该方法主要内容如下:
- 发送初始帧给服务器
- 将
OkHttp
的设置以设置帧的形式发送给服务器 - 开启一个线程,读取服务器发送的初始帧和设置帧
在HTTP/2
中,每个终端在使用HTTP/2
协议之前都要发送一个初始帧作为最终的确认,同时初始帧后面还需跟着一个设置帧,作为HTTP/2
连接建立的初始化设置。
HTTP/2
协议中的基本单位是帧,每个帧都有不同的类型和用途, 例如,报头(HEADERS
)和数据(DATA
)帧组成了基本的HTTP
请求和响应,其他帧例如 设置(SETTINGS
)、窗口更新(WINDOW_UPDATE
)和推送承诺(PUSH_PROMISE
) 是用来实现HTTP/2
的其他功能。
TLS握手
如果是HTTPS
请求,则需要调用connectTls
方法进行TLS
握手,RealConnection::connectTls
@Throws(IOException::class)
private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
val address = route.address
val sslSocketFactory = address.sslSocketFactory
var success = false
var sslSocket: SSLSocket? = null
try {
// Create the wrapper over the connected socket.
// 基于前面TCP连接的Socket包装一个SSLSocket对象
sslSocket = sslSocketFactory!!.createSocket(
rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket
// Configure the socket's ciphers, TLS versions, and extensions.
// 配置TLS相关信息
val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)
if (connectionSpec.supportsTlsExtensions) {
Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)
}
// Force handshake. This can throw!
// 进行TLS握手
sslSocket.startHandshake()
// block for session establishment
// 获取SSLSession
val sslSocketSession = sslSocket.session
val unverifiedHandshake = sslSocketSession.handshake()
// Verify that the socket's certificates are acceptable for the target host.
// 验证socket证书对该主机是否有效
if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {
...
}
...
// Success! Save the handshake and the ALPN protocol.
val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
Platform.get().getSelectedProtocol(sslSocket)
} else {
null
}
socket = sslSocket
// 获取输入输出流
source = sslSocket.source().buffer()
sink = sslSocket.sink().buffer()
protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1
success = true
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket)
}
if (!success) {
sslSocket?.closeQuietly()
}
}
}
connectTls
方法主要进行以下工作:
- 基于前面
TCP
连接创建的Socket
包装为一个SSLSocket
对象。 - 配置
TLS
相关信息。 - 进行
TLS
握手。 - 验证证书对该主机是否有效。
- 获取输入输出流
source
以及sink
。
连接获取流程梳理
我们再梳理一下ExchangeFinder::findConnection
获取连接的流程,它一共有五步:
- 重用
call
当中的连接 - 第一次尝试从连接池获取连接
- 第二次尝试从连接池获取连接
- 自己新创建一个连接
- 第三次尝试从连接池获取连接
重用call
当中的连接:存在RetryAndFollowUpInterceptor
拦截器,当请求失败需要重试或者重定向的时候,这时候call
中的连接还在,如果该连接满足条件,则可以进行复用;
第一次尝试从连接池获取连接:不使用「连接合并」的特性,只有目标请求地址和connection
对应路由的主机名、端口等信息都匹配,才使用该连接;
第二次尝试从连接池获取连接:使用「连接合并」的特性,不管是不是多路复用连接,优先判断主机名和端口等信息是否匹配,匹配则直接使用,若只有主机名不匹配,判断是否符合「连接合并」的要求,满足则使用,否则不使用;
自己新创建一个连接:首先创建一个RealConnection
对象,然后调用该对象的connect
方法,在该方法中,会进行TCP
连接(三次握手),并建立协议(根据情况,判断是否要进行TLS
握手、开启HTTP2
连接);
第三次尝试从连接池获取连接:使用「连接合并」的特性,这一次只使用多路复用的连接。也是优先判断主机名和端口等信息是否匹配,匹配则直接使用,不匹配则判断是否符合「连接合并」的要求,满足则使用,否则不使用
为什么在新创建了一个连接后,还要第三次尝试从连接池获取连接呢?
ExchangeFinder::findConnection
方法中对于第三次从连接池获取连接的注释如下:
// If we raced another call connecting to this host, coalesce the connections. This
// makes for 3 different lookups in the connection pool!
有可能另外一个call
也创建了一个连接到host
的connection
,并且那个call
先于当前的call
将该连接放到了连接池当中,这时候我们这个call
就尝试从连接池中去获取连接,如果满足「连接合并」的特性,则复用另外一个call
的连接,当前call
新建的连接则将其关闭,节省资源。
另外,如果在新建了连接后,第三次从连接池又成功获取了连接,那么会将成功新建的连接的路由 route 记录在 nextRouteToTry 变量里面,如果连接池获取的连接被判定为不健康的话,那么重新调用 ExchangeFinder::findConnection 方法获取连接的时候,可以直接使用 nextRouteToTry 记录的路由去新建一个 TCP 连接,而不用再去路由列表里面找一个路由。
整个流程梳理
ConnectInterceptor
拦截器的作用是获得一个健康可用的与目标服务器的连接,然后就交给下一个拦截器处理。获取健康可用的连接的调用链如下:
ConnectInterceptor::intercept
->RealCall::initExchange
->ExchangeFinder::find
->ExchangeFinder::findHealthyConnection
->ExchangeFinder::findConnection
-
在
ExchangeFinder::findConnection
中,会去获取一个可用的连接,里面又分为了5步。 -
在
ExchangeFinder::findHealthyConnection
中,会判断获取到的连接是否健康,若健康则返回,若不健康,在有可以尝试的路由的前提下,继续调用findConnection
方法获取一个可用的连接。 -
在
ExchangeFinder::find
方法中,会通过已获取的健康可用的连接,去创建一个ExchangeCodec
对象,该对象负责对Http
请求进行编码和对Http
响应进行解码,此时,有了与服务器的连接和处理I/O的ExchangeCodec
对象,我们其实就可以和服务器进行通信了。 -
在
RealCall::initExchange
方法中,会利用ExchangeCodec
实例,创建了一个Exchange
对象,我们可以认为Exchange
是封装ExchangeCodec
的一个工具类,Exchange
负责连接管理,而ExchangeCodec
负责处理实际的I/O
。 -
接着在
ConnectInterceptor::intercept
方法中,将获取到的Exchange
对象放到拦截器链RealInterceptorChain
里面,此时已经获得了一个健康可用的与目标服务器的连接,然后将请求交给下一个拦截器处理。
大致流程如下: