Okhttp

294 阅读6分钟

一、请求过程

首先来看一个最简单的Http请求是如何发送的。

   val okHttpClient = OkHttpClient()
   val request: Request = Request.Builder()
       .url("https://www.google.com/")
       .build()

   okHttpClient.newCall(request).enqueue(object :Callback{
       override fun onFailure(call: Call, e: IOException) {
       }

       override fun onResponse(call: Call, response: Response) {
       }
   })

这段代码看起来比较简单,OkHttp请求过程中最少只需要接触OkHttpClientRequestCallResponse,但是框架内部会进行大量的逻辑处理。——外观模式。

所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。 关于分发器与拦截器,这里先简单介绍下,后续会有更加详细的讲解。

  • 分发器:内部维护队列与线程池,完成请求调配;
  • 拦截器:五大默认拦截器完成整个请求过程。

对应的UML图大概是这样:

img

整个网络请求过程大致如上所示

  1. 通过建造者模式构建OKHttpClientRequest
  2. OKHttpClient通过newCall发起一个新的请求。
  3. 通过分发器维护请求队列与线程池,完成请求调配。
  4. 通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作。
  5. 得到网络请求结果。

二、分发器工作流程

发器的主要作用是:维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表, 等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求。

而这里同步请求和异步请求又略有不同。

同步请求

synchronized void executed(RealCall call) {
	runningSyncCalls.add(call);
}

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可。

异步请求

synchronized void enqueue(AsyncCall call) {
	//请求数最大不超过64,同一Host请求不能超过5个
	if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) 	  {
		runningAsyncCalls.add(call);
		executorService().execute(call);
	} else {
		readyAsyncCalls.add(call);
	}
}

当正在执行的任务未超过最大限制64,同时同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

每个任务完成后,都会调用分发器的finished方法,这里面会取出等待队列中的任务继续执行。

三、拦截器工作流程

经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了

# RealCall
  override fun execute(): Response {
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

我们再来看下RealCallexecute方法,可以看出,最后返回了getResponseWithInterceptorChain,责任链的构建与处理其实就是在这个方法里面。

internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,interceptors = interceptors,index = 0
    )
    val response = chain.proceed(originalRequest)
  }

如上所示,构建了一个OkHttp拦截器的责任链。

用户自定义应用拦截器和5大默认拦截器的责任链添加的顺序及作用如下表所示:

拦截器作用
应用拦截器拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor处理错误重试和重定向
BridgeInterceptor应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
CallServerInterceptor请求拦截器,在前置准备工作完成后,真正发起了网络请求。

我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptorintercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回,如下图所示: img

应用拦截器和网络拦截器有什么区别?

细心的同学可能发现了在getResponseWithInterceptorChain()中还有个networkInterceptors,那么它是什么呢?

okhttp除了让用户扩展自定义应用拦截器外,还提供了网络拦截器,它也是自定义拦截器,通常用于监控网络层的数据传输。

从整个责任链路来看,

  • 应用拦截器是最先执行的拦截器,由于应用拦截器在RetryAndFollowUpInterceptorCacheInterceptor之前,所以一旦发生错误重试或者网络重定向,应用拦截器永远只会触发一次。

    通常用于统计客户端的网络请求发起情况。例如日志打印、添加请求头、token校验等。

  • 网络拦截器位于ConnectInterceptorCallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。一般每次网络请求都会调用一次。但是如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。

    通常可用于统计网络链路上传输的数据,监控数据传输等。

四、缓存原理

OKHttp内部使用Okio来实现缓存文件的读写。

大致的流程如下:

  • 第一次响应根据头信息决定是否需要缓存。
  • 再次请求判断是否存在本地缓存,是否需要使用对比本地缓存。
  • 如果缓存失效或者对比本地缓存,则发出网络请求,否则使用本地缓存。

通过OkHttpClient设置缓存是全局状态的,如果我们想对某个特定的request使用或禁用缓存,可以通过CacheControl相关的API实现:

//禁用缓存
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder().noCache().build())
    .url("http://publicobject.com/helloworld.txt")
    .build();

最后需要注意的一点是,OKHttp默认只支持get请求的缓存。

# okhttp3.Cache.java
@Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    ...
    //缓存仅支持GET请求
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }
    
    //对于vary头的值为*的情况,统一不缓存
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    ...
}

五、复用连接池

private final ConnectionPool connectionPool;

里面有个复用连接池,其实就是类似于线程池。

构造方法中的参数:

最大连接数默认为5个、保活时间为5分钟

大致流程:

  • 判断当前的连接是否可以使用:流是否已经被关闭,并且已经被限制创建新的流;
  • 如果当前的连接无法使用,就从连接池中获取一个连接;
  • 连接池中也没有发现可用的连接,创建一个新的连接,并进行握手,然后将其放到连接池中。

六、Okhttp对于网络请求做了哪些优化,如何实现的?

  • 通过缓存策略减少重复的网络请求。

  • 通过复用连接池来减少请求延时(有5分钟保活的长连接)。

七、OKHttp中用到了哪些设计模式?

  1. 建造者模式:OkHttpClientRequest的构建都用到了构建者模式。
  2. 外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
  3. 责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置。