OkHttp源码剖析(四) 报文读写工具ExchangeCodec
缘起:需要一个connection
由前面几章知识可知,拦截器ConnectInterceptor
会经过ExchangeFinder.findConnection()
,
private fun findConnection(...): RealConnection {
...
// Nothing in the pool. Figure out what route we'll try next.
val routes: List<Route>?
val route: Route
...
// 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
// 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.
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
route = localRouteSelection.next()
...
...
// Connect. Tell the call about the connecting call so async cancels work.
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
} finally {
call.connectionToCancel = null
}
call.client.routeDatabase.connected(newConnection.route())
...
}
可以看到,在倒数第七行获取连接前,connectionPool.callAcquirePooledConnection()
方法需要利用routes
才能在池里寻找可多路复用/非多路复用的连接。在获取不到可用的连接需要自己进行连接创建时,newConnection.connect()
方法也需要RealConnection(connectionPool, route)
中的路由才能获取到newConnection
。
可以看到,需要连接的过程离不开Route
。
Route
Route
是用来描述路由的类,它包括了你的ip地址(InetSocketAddress
),tcp端口(Address
),以及代理模式(Proxy
)。
connection
利用 Route
类连接到具体的服务器。
class Route(
@get:JvmName("address") val address: Address,
@get:JvmName("proxy") val proxy: Proxy,
@get:JvmName("socketAddress") val socketAddress: InetSocketAddress
) {
/**
* Returns true if this route tunnels HTTPS through an HTTP proxy.
* See [RFC 2817, Section 5.2][rfc_2817].
* [rfc_2817]: http://www.ietf.org/rfc/rfc2817.txt
*/
fun requiresTunnel() = address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP
...
}
在RealConnection
类中有isEligible()
方法,该方法在寻找连接时,对可连接合并的连接进行了判断:
/**
* Returns true if this connection can carry a stream allocation to `address`. If non-null
* `route` is the resolved route for a connection.
*/
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
assertThreadHoldsLock()
// If this connection is not accepting new exchanges, we're done.
if (calls.size >= allocationLimit || noNewExchanges) return false
// If the non-host fields of the address don't overlap, we're done.
if (!this.route.address.equalsNonHost(address)) return false
// If the host exactly matches, we're done: this connection can carry the address.
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/
// 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.
}
这段代码判断是否可以进行连接合并,判断是否可以连接合并需要Route
才能判定,因为需要知道ip地址,端口,代理等是否都一致,一致才能进行接口接口合并。
Proxy
接着我们了解一下 Proxy
类,它是由 Android原生提供(其实是jdk1.5开始的原生类)的:
public class Proxy {
public static final Proxy NO_PROXY = null;
...
public static enum Type {
// 不使用代理
DIRECT,
// HTTP代理
HTTP,
// SOCKS代理
SOCKS;
private Type() {
}
}
}
RouteSelector:
在OkHtttp中,Route组成RouteSelection,RouteSelection实际上是Selection类,它是同样端口,同样代理类型下的不同ip的route集合。RouteSelection组成RouteSelector。需要先关注到,在寻找连接时,需要拿到Route,然后根据Route中的ip、端口、代理等,判断连接是否可以被合并(Http2)。
代理的由来
RouteSelector
在创建 ExchangeFinder
时创建
RouteSelector
在初始化时执行操作:
init {
resetNextProxy(address.url, address.proxy)
}
让我们看 resetNextProxy
方法,它用于准备服务器要使用的代理:
private fun resetNextProxy(url: HttpUrl, proxy: Proxy?) {
fun selectProxies(): List<Proxy> {
// 若用户有设定代理,使用用户设置的代理
if (proxy != null) return listOf(proxy)
// 如果 URI 缺少主机(如“http://”),则不要调用 ProxySelector。
val uri = url.toUri()
if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)
// 尝试每个 ProxySelector 的选择,直到一个连接成功。
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)
}
可以看到, resetNextProxy
方法首先检查了一下我们的 address
中有没有用户设定的代理(通过 OkHttpClient
传入),若有用户设定的代理,则直接使用用户设定的代理。
若用户没有设定的代理,则尝试使用 ProxySelector.select
方法来获取代理列表。这里的 ProxySelector
也可以通过 OkHttpClient
进行设置,默认情况下会使用系统默认的 ProxySelector
来获取系统配置中的代理列表。
路由的选择
在缘起中寻找连接的代码中,有这样一行代码 val localRouteSelection = localRouteSelector.next()
,它用于寻找可用的RouteSelection
。
RouteSelector
类用于管理所有的路由信息,并选择路由,值得注意的是它返回的不是Route
,而是Route
的集合RouteSelection
。下面通就通过过其RouteSelector
的next()
方法具体分析。
operator fun next(): Selection {
if (!hasNext()) throw NoSuchElementException()
// 计算要尝试的下一组路由
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()) {
// 所有代理均不行,所以退到了延迟路由。
routes += postponedRoutes
postponedRoutes.clear()
}
return Selection(routes)
}
借助Route的connection连接
HTTP 请求的过程中,需要先找到一个可用的代理路由,再根据代理协议规则与目标建立 TCP 连接。
如下代码所示:
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
}
...
可以看到,在ExchangeFinder.findConnection()
方法中,RealConnection
以route
为参实例化newConnection
并进行了连接。
RealConnection
中的connect()
方法如下:
fun connect( ...) {
check(protocol == null) { "already connected" }
...
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) {
...
}
}
}
查看connectSocket()
方法,如下可以看到,请求连接的建立过程,需要先找到一个代理路由,再根据代理协议规则与目标建立 TCP 连接。
private fun connectSocket(
connectTimeout: Int,
readTimeout: Int,
call: Call,
eventListener: EventListener
) {
val proxy = route.proxy
val address = route.address
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 {
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 = rawSocket.source().buffer()
sink = rawSocket.sink().buffer()
} catch (npe: NullPointerException) {
if (npe.message == NPE_THROW_WITH_NULL) {
throw IOException(npe)
}
}
}