Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

OkHttp source code analysis (a) first know OkHttp

OkHttp source code analysis (two) design mode under OkHttp

OkHttp source code analysis (three) task scheduler Dispatcher

OkHttp source code analysis (4) Message read and write tool ExchangeCodec

OkHttp source code analysis (five) proxy routing

Origin: Need a connection

Knowledge knowable by the preceding chapters, the interceptor can pass ExchangeFinder ConnectInterceptor 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)
        returnresult } 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())
    ...
  }
Copy the code

As you can see, before seven lines for connection, connectionPool. CallAcquirePooledConnection () method needs to use routes to look for in a pool can be multiplexed/of multiplexed connections. The newConnection.connect() method also requires a route in RealConnection(connectionPool, route) to obtain newConnection when no connection is available and you need to create your own connection.

As you can see, the process that requires a connection requires a Route.

Route

Route is a class that describes routing. It includes your IP Address (InetSocketAddress), TCP port (Address), and Proxy mode.

Connection uses the Route class to connect to a specific server.

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(a)= address.sslSocketFactory ! =null && proxy.type() == Proxy.Type.HTTP
 ...
}

Copy the code

In the RealConnection class there is the isEligible() method, which, when looking for connections, determines which connections can be joined and merged:

 /** * 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.
  }
Copy the code

This code determines whether connection merging can be performed, and it requires Route to determine whether connection merging can be performed, because it needs to know whether IP addresses, ports, proxies, etc., are consistent to perform interface merging.

Proxy

Next, let’s look at the Proxy class, which is provided natively by Android (actually native from JDK1.5 onwards) :

public class Proxy {
    public static final Proxy NO_PROXY = null; .public static enum Type {
        // Do not use proxy
        DIRECT,
        / / HTTP proxy
        HTTP,
        / / SOCKS proxy
        SOCKS;

        private Type() {
        }
    }
}
Copy the code

RouteSelector:

In OkHtttp, routes form the RouteSelection, which is actually a Selection class that is a collection of routes from different IP addresses of the same port and proxy type. A RouteSelection constitutes a RouteSelector. [Http2] [Route] [IP] [proxy] [Route] [IP] [proxy] [proxy] [Route] [IP] [proxy] [proxy] [Route] [IP] [proxy] [proxy] [Http2]

Origin of agency

The RouteSelector is created when ExchangeFinder is created

A RouteSelector performs operations on initialization:

 init {
    resetNextProxy(address.url, address.proxy)
  }
Copy the code

Let’s look at the resetNextProxy method, which prepares the proxy to be used by the server:

  private fun resetNextProxy(url: HttpUrl, proxy: Proxy?). {
    fun selectProxies(a): List<Proxy> {
      // If the user has set the proxy, use the proxy set by the user
      if(proxy ! =null) return listOf(proxy)

      // Do not call ProxySelector if the URI lacks a host (such as "http://").
      val uri = url.toUri()
      if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)

      // Try each ProxySelector selection until a 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)
  }
Copy the code

As you can see, the resetNextProxy method first checks to see if there is a user-defined proxy in our address (passed in via OkHttpClient), and if there is one, uses it directly.

If the user does not have a specified proxy, try using the ProxySelector. Select method to get the proxy list. The ProxySelector here can also be set using OkHttpClient, which by default uses the system default ProxySelector to get the list of proxies in the system configuration.

Route selection

In the code for finding connections in origin, there is a line val localRouteSelection = localRoutesElector.next (), which is used to find available Routeselections.

The RouteSelector class is used to manage all the routing information and select routes, notably returning not a Route but a collection of routes, RouteSelection. The next() method of RouteSelector is used to analyze it.

  operator fun next(a): Selection {
    if(! hasNext())throw NoSuchElementException()

    // Calculates the next set of routes to try
    val routes = mutableListOf<Route>()
    while (hasNextProxy()) {
      // A delayed route is always the last to be tried
      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()) {
      // All proxies fail, so we fall back to delayed routing.
      routes += postponedRoutes
      postponedRoutes.clear()
    }

    return Selection(routes)
  }
Copy the code

Use a Route connection

During an HTTP request, you need to find an available proxy route and then establish a TCP connection with the destination according to the proxy protocol rules.

The following code looks like this:

    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } 
    ...
Copy the code

. As you can see, in ExchangeFinder findConnection () method, RealConnection route for instantiation newConnection and connection.

The connect() method in RealConnection is as follows:

  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) { ... }}}Copy the code

The connectSocket() method is used to set up a TCP connection with the destination by finding a proxy route.

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)
    }
  }
}
Copy the code