OkHttp源码剖析(五) 代理路由

OkHttp源码剖析(一) 初识okhttp

OkHttp源码剖析(二) 设计模式下的okhttp

OkHttp源码剖析(三) 任务调度器Dispatcher

OkHttp源码剖析(四) 报文读写工具ExchangeCodec

OkHttp源码剖析(五) 代理路由

缘起:需要一个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。下面通就通过过其RouteSelectornext()方法具体分析。

  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()方法中,RealConnectionroute为参实例化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)
    }
  }
}
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许进进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值