Okhttp超解析之路:Okhttp中类作用简介

1,785 阅读7分钟

Okhttp中类作用简介

Okhttp中可直接调用的类

包含Okhttp的请求流程中使用到的类,以及可以在OkhttpClient中自定义的实现类。

HttpUrl

Okhttp中解析Url得到的数据,包括以下几种数据:

  • scheme : 协议名称,如http、https。
  • username、password : 登录信息,用于身份认证。
  • host : 服务器地址
  • port : 服务器端口号,http端口默认80,https端口默认443.
  • pathSegments : 带层次的文件路径,指服务器上的文件路径
  • queryNamesAndValues : 查询字符串,根据key=value拼接的参数。
  • fragment : 片段标识符,用来标记以获取资源中的子资源。

Address

表示连接到服务器的规范。对于一个简单的连接来说它包括服务端的IP和端口。对于一个显示代理的请求,它还包括一些代理的信息。对于一个安全的连接来说,它还包括SSL套接字工厂、主机名验证和证书处理器。 Address中包含以下信息:

  final HttpUrl url;
  final Dns dns;
  final SocketFactory socketFactory;
  final Authenticator proxyAuthenticator;
  final List<Protocol> protocols;
  final List<ConnectionSpec> connectionSpecs; //传输层版本和连接协议(SSL/TLS)
  final ProxySelector proxySelector;
  final Proxy proxy;
  final SSLSocketFactory sslSocketFactory;     //安全套层socket工厂 用于https
  final HostnameVerifier hostnameVerifier;     //主机名字确认
  final CertificatePinner certificatePinner;   //证书链

Request

封装HTTP请求的各种参数,包括以下五大类:

  final HttpUrl url;        //请求的连接,包含协议、ip、端口、账户密码等
  final String method;      //请求的方式 ,支持Get、Head、Post、Put、Delete、Patch、Options
  final Headers headers;    //请求的头部
  final RequestBody body;   //请求体
  final Object tag;         //对象标记,用于删除请求

Response

Dns

域名解析系统,将主机名解析成对应的ip地址,Okhttp默认提供了一个DNS实现,调用系统的方法获取对应的ip列表,我们也可以继承Dns接口主动实现。

private class DnsSystem : Dns {
      override fun lookup(hostname: String): List<InetAddress> {
        try {
          return InetAddress.getAllByName(hostname).toList()
        } catch (e: NullPointerException) {
          throw UnknownHostException("Broken system behaviour for dns lookup of $hostname").apply {
            initCause(e)
          }
        }
      }
    }

Dispatcher

分发器,用于执行同步或者异步的请求。

对于同步的请求,会在Dispatcher的同步队列中插入该条请求,然后再调用执行。执行完毕后,会调用Dispatcher的finished方法删除该条请求。

  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
  

对于异步请求,Okhttp中定义了默认的最大请求数为64,对于同一ip的地址来说,最大并发数为5,当然这些都是可以修改的。所以在Dispatcher中会使用两条队列来记录请求:

  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

runningAsyncCalls:运行队列用来存储一条请求,并通过线程池开启一个空闲线程来执行。

readyAsyncCalls:准备队列用来存储超出规定容量的请求。

可以替换Okhttp中默认的Dispatcher对象,创建一个类继承自DisPatcher:

OkHttpClient.Builder().dispatcher(Dispatcher())

CookieJar

Okhttp默认不提供存储Cookie的操作,需要我们实现CookieJar接口来保存Cookies。

OkHttpClient.Builder().cookieJar(CookieJar.NO_COOKIES)

Authenticator

响应来自远程服务器或代理服务器的身份认证质疑。当服务器需要用户代理的认证信息时,会通过返回401或407来通知用户代理。当用户代理在接受到401或407响应码时,需要将认证信息添加到Headers中。例如当我们初次登入一个网站时,有时会弹出认证弹窗需要我们输入账号密码,这就是服务端返回401,需要我们添加认证信息也就是账号密码。

Okhttp中默认给的一个返回null的认证信息,具体的实现需要我们手动加入认证信息,例子如下:

//服务端认证
class HttpAuthenticator : okhttp3.Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val credential = Credentials.basic("test_name","test-pwd")
       return response.request.newBuilder().addHeader("Authorization",credential).build()
    }
}

//代理服务器认证
class ProxyAuthenticator : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val credential = Credentials.basic("test_name", "test-pwd")
        return response.request.newBuilder().addHeader("Proxy-Authorization", credential).build()
    }
}

401: 服务端返回需要用户身份认证信息。

在Okhttp中会调用OkhttpClient中的authenticator对象来生成一个带有header("Authorization",credential)的头部。

RetryAndFollowUpInterceptor拦截器中在收到401时,会判断是否进行重试操作

407: 代理服务器需要认证信息。

在Okhttp中会调用OkhttpClient中的proxyAuthenticator对象来生成一个带有header("Proxy-Authorization",credential)的头部。

ConnectInterceptor拦截器在创建隧道时,如果收到407时,会调用该对象进行重新连接

通过上述的再次构建一个Request请求并发送。

Cache

OKhttp中有一层拦截器CacheInterceptor用来处理缓存的请求和响应。在OkhttpClient中可以设置缓存的路径大小,默认是不缓存任何响应的。

如果要设置缓存的话,我们首先在OkhttpClient中定义一个Cache对象,并替换Okhttp中的null对象,需要指定缓存的路径,和缓存的大小,内部采用DiskLruCache来实现文件的缓存。

 val okHttpClient = OkHttpClient.Builder()
        .cache(Cache(application.cacheDir,10*1024*1024))
        .build()

在Request中我们还可以配置缓存策略,来决定Request是否采用网络还是缓存。

val cacheCOntrol = CacheControl.Builder()
        .noCache()
        .noStore()
        .onlyIfCached()
        .maxAge(2,TimeUnit.SECONDS)
        .maxStale(2,TimeUnit.SECONDS)
        .minFresh(2,TimeUnit.SECONDS)
        .build()


val request: Request = Request.Builder()
        .cacheControl(CacheControl.FORCE_CACHE)
        .cacheControl(cacheCOntrol)
        .build()    

CertificatePinner

通过在客户端内部指定host的证书,可以达到锁定证书,防止中间人攻击的风险。

# 小写主机名或通配符模式(如*.example.com)。
# SHA-256或SHA-1哈希。每个pin都是证书主题公钥信息的散列,以base64编码,前缀为sha256/或sha1/。

 val certificatePinner = CertificatePinner.Builder()
        .add("hostname","SHA1..........")
        .build()
        
val okHttpClient = OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .build()        

注意: 使用certificatePinner时,可能会存在证书过期和证书切换的问题,因为证书是写在客户端的。

SocketFactory

Okhttp中实现了SocketFactoryDefaultSocketFactory类去创建一个Socket套接字,实现HTTP的连接。

SSLSocketFactory

安全套接层Socket,用于HTTPS。

HostnameVerifier

主机名验证,在握手期间,可以通过改接口来返回是否允许该连接。

public interface HostnameVerifier {
    public boolean verify(String hostname, SSLSession session);
}

Okhttp中默认实现了一个OkHostnameVerifier类。

ConnectionPool

管理HTTP和HTTP/2连接的重用,以减少网络延迟。请求相同的地址的Request可以共用一个连接。 Okhttp中默认的最大空闲连接池为5,存活时间为5分钟。

Okhttp中内部类

包括ExchangeFinder、Exchange、ExchangeCodec、RealConnection、路由和代理等。

RealConnectionPool

默认的连接池实现。在ConnectInterceptor拦截器中创建连接时,会优先从连接池中查找是否有可用的连接,当没有可用的时,会企图创建一个连接并加入到连接池中。

RealConnection

实现了Connection接口,代表一条具体的连接。

EventListener

监听事件。

ExchangeFinder

获取一条安全可靠的连接,来携带请求。 在RetryAndFollowUpInterceptor中会负责创建ExchangeFinder对象。

 //在RetryAndFollowUpInterceptor中调用以下方法
 call.enterNetworkInterceptorExchange(request, newExchangeFinder)

 /**
   * 调用RealCall的enterNetworkInterceptorExchange方法
   * newExchangeFinder只有当不属于重试的情况下才为true
   */
 fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
    check(interceptorScopedExchange == null)
    
   ......
   
    if (newExchangeFinder) {
      this.exchangeFinder = ExchangeFinder(
          connectionPool,
          createAddress(request.url),
          this,
          eventListener
      )
    }
  }  

Exchange||ExchangeCodec

编解码接口类,编码HTTP请求和解码HTTP响应。 在ConnectInterceptor拦截器中会负责创建一个Exchange和ExchangeCodec对象。

/**
 * 调用RealCall的initExchange方法来创建一个个Exchange和ExchangeCodec对象,
 * 用来携带即将到来的请求和响应。
 */
 val exchange = realChain.call.initExchange(chain)
 
   internal fun initExchange(chain: RealInterceptorChain): Exchange {
   
   ......
    val exchangeFinder = this.exchangeFinder!!
    val codec = exchangeFinder.find(client, chain)
    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")
    return result
  }

调用RealCall的exchangeFinder对象的find方法返回一个ExchangeCodec对象的同时,会调用findHealthyConnection方法获取一个安全的连接,其实就是获取一个RealConnection对象,同时将我在OkhttpClient对象中设置的各种超时参数传递进去。

 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"
      )
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      trackFailure(e.lastConnectException)
      throw e
    } catch (e: IOException) {
      trackFailure(e)
      throw RouteException(e)
    }
  }

最后调用RealConnection的newCodec方法获取一个可用的ExchangeCodec对象。由于ExchangeCodec是一个接口,因此具体的实现是Http1ExchangeCodec和Http2ExchangeCodec。

internal fun newCodec(client: OkHttpClient, chain: RealInterceptorChain): ExchangeCodec {
    val socket = this.socket!!
    val source = this.source!!
    val sink = this.sink!!
    val http2Connection = this.http2Connection

    return if (http2Connection != null) {
      Http2ExchangeCodec(client, this, chain, http2Connection)
    } else {
      socket.soTimeout = chain.readTimeoutMillis()
      source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS)
      sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS)
      Http1ExchangeCodec(client, this, source, sink)
    }
  }