转换的整体过程

基本的概念

OkHttp中与url有关的有下面这些概念:

  • url

    即我们一般使用的url,如https://jiangkang.tech

  • Address

    保存了一些静态信息,如使用的协议,端口,https相关的设置等信息

  • Router

    我们都知道一个域名可能对应多个IP地址,路由就是一个域名,IP地址对,对于一个url来说可能存在多个路由

  • Connection

    连接指的是一个Socket连接,我们的请求数据,响应数据最终都是通过socket的输入输出流来承载的

转换过程

先来看一张简单的时序图:

具体流程

url -> HttpUrl

我们一般用下面这种方式创建一个Request:

    val request = Request.Builder()
        .url("https://jiangkang.tech")
        .build()

其中的url()方法是url转换成HttpUrl的关键:

    open fun url(url: String): Builder {
      // Silently replace web socket URLs with HTTP URLs.
      val finalUrl: String = when {
        url.startsWith("ws:", ignoreCase = true) -> {
          "http:${url.substring(3)}"
        }
        url.startsWith("wss:", ignoreCase = true) -> {
          "https:${url.substring(4)}"
        }
        else -> url
      }

      return url(finalUrl.toHttpUrl())
    }

可以看到最终调用的url方法仍然传递的是HttpUrl对象,而这里url转换到HttpUrl的过程,则是由方法 toHttpUrl()实现的:

fun String.toHttpUrl(): HttpUrl = Builder().parse(null, this).build()

这个方法最终会对url进行解析,进而获取协议(http或者https),端口号,host,query,path,segment等信息封装成一个HttpUrl对象,后续的处理都是直接针对这个HttpUrl对象进行的。

HttpUrl -> Address

从文章开始的图可以看出,Address存储的也是一些诸如host,端口等一些静态信息,和HttpUrl几乎一样,那么为什么还需要抽象初Address这么一个概念来呢?

原因是HttpUrl是与应用无关的,给你一个url,就可以构造初一个HttpUrl对象,其中保存的信息自然也是不依赖与具体的配置的,但是我们知道,对于https的请求我们需要一个处理SSL,域名校验,证书校验相关的事情,这就是Address存在的意义。Address中保存了开发这设置的(或者默认的)SslSocketFactory,HostnameVerifier(用于校验域名),CertificatePinner(证书固定,用于自行校验https证书)。

我们发送请求的时候,经常会经过一个或者多个代理(可以是本地的VPN,也可以是代理工具,也可以是后端的代理服务器)才会到达服务器。Address中也保存了代理相关的信息。

如上面的时序图所示,会调用Transmitter的createAddress方法将HttpUrl转换成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(url.host, url.port, client.dns, client.socketFactory,
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator,
        client.proxy, client.protocols, client.connectionSpecs, client.proxySelector)
  }

Address -> Router

正如前面所说,一个url可能又多个IP地址,而一个url对应一个Address。从我前面的文章OkHttp是如何使用Socket实现HTTP请求和响应的?中可知,OkHttp最终会使用Socket真正处理Http请求。而Socket中使用的是一个InetSocketAddress表示一个IP地址,因此OkHttp中的Router实际保存的是一个<Address,InetSocketAddress>对。

这里的转换会分为两步:

  1. 通过Address生成RouterSelector:
private val routeSelector: RouteSelector = RouteSelector(
    address, connectionPool.routeDatabase, call, eventListener)

  1. 通过RouterSelector迭代生成对应的Router

    在创建Socket 连接的时候,会从RouterSelector中迭代生成Router对象,并封装到一个RouterSelection对象中,表示选中的路由;

   this.routeSelection = routeSelector.next()

   operator fun next(): Selection {
    ...
    val routes = mutableListOf<Route>()
    while (hasNextProxy()) {
      val proxy = nextProxy()
      // 遍历InetSocketAddress列表
      for (inetSocketAddress in inetSocketAddresses) {
        // 生成Router
        val route = Route(address, proxy, inetSocketAddress)
        ...
      }
    }
    ...
    return Selection(routes)
   }

这个RouterSelection其实就是一个Router列表表示成迭代的形式:

  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++]
    }
  }

Router -> Connection

一个Router表示一个Address到InetSocketAddress的对应关系,既然InetSocketAddress都出现了,Socket连接还会远吗?

核心的逻辑还是在ExchangeFinder中的findConnection方法中,下面列出核心的代码:


    // 连接的抽象表示
    var result: RealConnection? = null
 
    // 当前选中的路由
   selectedRoute = routeSelection!!.next()

   // 生成当前选中路由的连接
   result = RealConnection(connectionPool, selectedRoute!!)
   
   // 创建socket连接 
   result!!.connect(
        connectTimeout,
        readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
    )


从上面的代码中可以很清楚的看到,一个Router对应一个Connection,会使用当前选中的路由去创建Connection。

到这里从URL到Connection的过程就分析完了。 至于OkHttp针对这些过程的优化,可以看后续的文章,这里可以提前说一下,主要是针对Router的缓存,和针对Connection的缓存,用来提升连接性能。