OkHttp原理解析(二)

前言

上一篇我们学习了OKHttp的请求执行流程,知道了最终请求流程都会交给getResponseWithInterceptorChain方法来执行,接下来我们就详细分析执行getResponseWithInterceptorChain方法所涉及的设计模式以及各个拦截器的用途!

OkHttp原理解析(一)

责任链模式

OKHttp最核心的工作是在getResponseWithInterceptorChain中执行,因为此方法的实现就是利用责任链模式完成一步步请求,所以我们先来了解一下什么是责任链模式;

责任链模式是一种行为模式,为请求创建一个接收者的对象链.这样就避免,一个请求链接多个接收者的情况.进行外部解耦.类似于单向链表结构。

打个比方:
现在有个需求来了,首先是实习生拿到这个需求。
如果实习生能够实现,直接实现。如果不行,他把这个需求交给初级工程师。
如果初级工程师能够实现,直接实现。如果不行,交给中级工程师。
如果中级工程师能够实现,直接实现。如果不行,交给高级工程师。
如果高级工程师能够实现,直接实现。如果不行,交给 CTO。
如果 CTO能够实现,直接实现。如果不行,直接跟产品说,需求不做。

优点

  1. 降低耦合度。它将请求的发送者和接收者解耦;
  2. 简化了对象。使得对象不需要知道链的结构;
  3. 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任;
  4. 增加新的请求处理类很方便;

缺点

  1. 不能保证请求一定被接收;
  2. 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用;
  3. 可能不容易观察运行时的特征,有碍于除错;

在责任链模式中,每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。

具体责任链模式可参考:责任链模式详解

拦截器流程

OKHttp中的getResponseWithInterceptorChain流程如下:
getResponseWithInterceptorChain请求流程
请求会被交给责任链中的一个个拦截器。如果没有新增自定义拦截器的话,默认情况下有五大拦截器:

1.RetryAndFollowUpInterceptor:重试重定向拦截器,第一个接触到请求,最后接触到响应,负责判断是否需要重新发起整个请求;
2.BridgeInterceptor:桥接拦截器,补全请求信息,并对响应进行额外处理;
3.CacheInterceptor:缓存拦截器,请求前查询缓存,获取响应后判断是否需要进行缓存;
4.ConnectionInterceptor:连接拦截器,与服务器完成TCP连接;
5.CallServerInterceptor:发起请求拦截器,与服务器通信;封装请求数据与解析响应数据【如:HTTP报文】

拦截器详情

重试以及重定向拦截器 RetryAndFollowUpInterceptor

主要就是完成两件事:重试重定向

重试

请求阶段发生RouteException或者IOException会进行判断是否重新发起请求。

### RouteException
catch (e: RouteException) {
          // 路由异常,连接没有成功,请求还没发出去
          if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
            throw e.firstConnectException.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e.firstConnectException
          }
          newExchangeFinder = false
          continue
        }

### IOException
catch (e: IOException) {
          //请求发出去了,但是和服务器通信失败【socket流正在读写数据的时候断开连接】
          //HTTP2才会抛出ConnecttionShutdownException,对于HTTP1 requestSendStarted一定是true
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newExchangeFinder = false
          continue
        }

两个异常都是根据recover方法判断是否能够进行重试,如果返回true,则表示允许重试。

### recover方法
  private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    //1.在配置OkHttpClient时设置了不允许重试【默认允许】,则一旦发生请求失败就不再重试
    if (!client.retryOnConnectionFailure) return false

    // 2.requestIsOneShot校验的是RequestBody的isOneShot是否是true, isOneShot默认是false。说明一个请求正文可以多次请求(多次请求的情况如408 客户端超时;401和407 权限异常可以通过头部进行满足;503 服务端异常,但是头部的retry-After为0可以进行重试)。
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    // isRecoverable 校验当前的异常是否是可恢复的异常。ProtocolException 协议异常返回false;InterruptedIOException io读写异常同时是socket链接超时异常可以重试;SSLHandshakeException https握手时候的异常同时是校验异常CertificateException会返回false; SSLPeerUnverifiedException证书校验异常则返回false。
    if (!isRecoverable(e, requestSendStarted)) return false

    // 没有更多可以重试的路由
    if (!call.retryAfterFailure()) return false

    // For failure recovery, use the same route selector with a new connection.
    return true
  }

因此当使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在isRecoverable中进行判断:

 private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
 	//异常是协议异常,不能重试
    if (e is ProtocolException) {
      return false
    }
	//如果不是超时异常,不能重试
    if (e is InterruptedIOException) {
      return e is SocketTimeoutException && !requestSendStarted
    }
	//SSL握手异常中,证书出现问题,不能重试
    if (e is SSLHandshakeException) {
      if (e.cause is CertificateException) {
        return false
      }
    }
    //SSL握手未授权异常,不能重试
    if (e is SSLPeerUnverifiedException) {
      return false
    }
    return true
  }

1.协议异常,如果是这样直接判定不能重试;【你的请求或者服务器的响应本身就存在问题,没有按照http协议来定义数据,再重试也没用】
2.超时异常,可能由于网络波动造成socket连接的超时,可以使用不同路线重试;
3.SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根没有证书,或者证书数据不正确;

经过异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比如DNS对域名解析后可能会返回多个IP,在一个IP失败后,尝试另一个IP进行重试。

重定向

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步判断是否需要重定向的判断。重定向的判断位于followUpRequest方法中

  @Throws(IOException::class)
  private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
    val route = exchange?.connection?.route()
    val responseCode = userResponse.code

    val method = userResponse.request.method
    when (responseCode) {
    // 407 客户端使用了HTTP代理服务器,在请求头中添加"Proxy-Authorization",让代理服务器授权
      HTTP_PROXY_AUTH -> {
        val selectedProxy = route!!.proxy
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
        }
        return client.proxyAuthenticator.authenticate(route, userResponse)
      }
	 //401 需要身份验证,有些服务器接口需要验证使用者身份,在请求中添加"Authorization"
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
   	状态码30X 系列一般是发生了资源变动处理的行为。如重定向跳转等。
	//300 是指有多种选择。请求的资源包含多个位置
	//301 请求的资源已经永久移动了 会自动重定向
	//302 临时移动,资源是临时转移了,客户端可以沿用原来的url
	//303 查看其他地址,可301类似
	//307 临时重定向,GET请求的重定向
	//308 和307类似也是临时重定向
      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }
	// 状态码408 服务器等待客户端发送请求超时处理
	//1.当前okhttp是否允许重试
    //2.请求体是否允许重复发送
    //3.是否已经重试了,且重试的状态是否还是408
    //4.通过retryAfter获取响应头部信息Retry-After(头部存在该key,则设置为key的内容否则设置为0.不存在该key设置为INT的最大数值)。拿到重试时间后判断是否大于0,大于0说明此时返回一个空的请求对象,Okhttp将不会处理抛给业务层自己处理。

      HTTP_CLIENT_TIMEOUT -> {
        if (!client.retryOnConnectionFailure) {
          return null
        }
        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null
        }
        if (retryAfter(userResponse, 0) > 0) {
          return null
        }

        return userResponse.request
      }
	// 状态码 503 由于服务器的异常导致无法完成客户端的请求,如果上一次的请求已经是503了,就没必要重复请求了。且如果Retry-After 设置为0,说明需要立即重复请求,才会重新请求,其他情况下只会放弃请求。
      HTTP_UNAVAILABLE -> {
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request
        }

        return null
      }
	//状态码421 超出了服务器最大连接数,需要重新请求
      HTTP_MISDIRECTED_REQUEST -> {
        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }

        if (exchange == null || !exchange.isCoalescedConnection) {
          return null
        }

        exchange.connection.noCoalescedConnections()
        return userResponse.request
      }

      else -> return null
    }
  }

整个是否需要重定向的判断内容很多,如果此方法返回null,那就表示不需要再重定向了,直接返回响应;如果返回非空,那就要重新请求返回的Request,但需要注意的是,我们的followup在拦截器定义的最大次数为20次;

小结

重试重定向拦截器是整个责任链的第一个,这意味着它是第一个接触到Request与最后接收到Respose的角色,在这个拦截器中主要功能就是判断是否需要重试与重定向。

重试的前提是出现了RouteException或者IOException,一旦在后续的拦截器执行过程中出现这两个异常,就会通过recover方法进行判断是否进行连接重试。

重定向发生在重试的判断之后,如果不满足重试的条件,还需要进一步调用followUpRequest根据Response的响应码【如果直接请求失败,Response都不存在就会抛出异常】,followup最大发生20次。

桥接拦截器 BridgeInterceptor

连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。

补全请求头

请求头说明
Content-Type请求体类型,如: application/x-www-form-urlencoded
Content-Length/Transfer-Encoding请求体解析方式
Host请求的主机站点
Connection: Keep-Alive保持长连接
Accept-Encoding: gzip接受响应支持gzip压缩
Cookiecookie身份辨别
User-Agent请求的用户信息,如:操作系统,浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:
1.保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的CookieJar不提供实现;
2.如果使用gzip返回的数据,则使用GzipSource包装便于解析。

小结

桥接拦截器的执行逻辑主要就是以下几点:
对用户构建的Request进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的Request,将符合网络请求规范的Request交给下一个拦截器处理,并获取Response如果响应体经过了GZIP压缩,那就需要解压,再构建成用户可用的Response并返回。

缓存拦截器 CacheInterceptor

在发出请求前,判断是否命中缓存,如果命中则可以不请求,直接使用缓存的响应。【只会存在Get请求的缓存】

步骤如下:
1.从缓存中获取对应请求的响应缓存;
2.创建CacheStrategy,创建时会判断是否能够使用缓存,在CacheStrategy中存在两个成员:networkRequestcacheResponse。它们的组合如下:

networkRequestcacheResponse说明
NullNot Null直接使用缓存
Not NullNull向服务器发起请求
NullNull直接返回504
Not NullNot Null发起请求,若得到响应为304【无修改】,则更新缓存响应并返回

3.交给下一个责任链继续处理;
4.后续工作,返回304则用缓存的响应,否则使用网络响应并缓存本次响应【只缓存Get请求的响应】

缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多,在缓存拦截器中判断是否可以使用缓存,或是请求服务器都是通过CacheStrategy判断。

缓存策略

CacheStrategy,首先需要认识几个请求头与响应头;

响应头说明例子
Date消息发送的时间Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires资源过期的时间Expires: Sat, 18 Nov 2028 06:17:41 GMT
LastModified资源最后修改时间Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT
ETag资源在服务器的唯一标识ETag: “16df0-5383097a03d40”
Age服务器用缓存响应请求,该缓存从产生到现在经过多长时间(秒)Age: 3825683
CacheControl--
请求头说明例子
If-Modified-Since服务器没有在指定的时间后修改请求对应资源,返回304(无修改)If-Modified-Since: Fri, 22 Jul 2016 02:17:41 GMT
If-None-Match服务器将其与请求对应资源的 Etag 值进行比较,匹配返回304If-None-Match: “16df0-5383097a03d40”
CacheControl--
其中CacheControl可以在请求头存在,也能在响应头存在,对应的value可以设置多种组合;
  1. max-age=[秒] :资源最大有效时间;
  2. public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
  3. private :表明该资源只能被单个用户缓存,默认是private。
  4. no-store :资源不允许被缓存
  5. no-cache :(请求)不使用缓存
  6. immutable :(响应)资源不会改变
  7. min-fresh=[秒] :(请求)缓存最小新鲜度(用户认为这个缓存有效的时长)
  8. must-revalidate :(响应)不允许使用过期缓存
  9. max-stale=[秒] :(请求)缓存过期后多久内仍然有效

假设存在max-age =100,min-fresh =20,这代表了用户认为这个缓存的响应,从服务器创建响应到能够缓存使用的时间为100-20 =80s。但是如果max-stale=100,这代表了缓存有效时间80s过后,仍然允许使用100s,可以看成缓存有效时长为180s。

OkHttp缓存查找流程

详细流程

如果从缓存中获得了本次请求URL对应的Response,首先会从响应中获取以上数据备用;

 class Factory(
    private val nowMillis: Long,
    internal val request: Request,
    private val cacheResponse: Response?
  ) {
  	...
    init {
      if (cacheResponse != null) {
      	//对应响应的请求出发的本地时间和接收到响应的本地时间
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        val headers = cacheResponse.headers
        for (i in 0 until headers.size) {
          val fieldName = headers.name(i)
          val value = headers.value(i)
          when {
            fieldName.equals("Date", ignoreCase = true) -> {
              servedDate = value.toHttpDateOrNull()
              servedDateString = value
            }
            fieldName.equals("Expires", ignoreCase = true) -> {
              expires = value.toHttpDateOrNull()
            }
            fieldName.equals("Last-Modified", ignoreCase = true) -> {
              lastModified = value.toHttpDateOrNull()
              lastModifiedString = value
            }
            fieldName.equals("ETag", ignoreCase = true) -> {
              etag = value
            }
            fieldName.equals("Age", ignoreCase = true) -> {
              ageSeconds = value.toNonNegativeInt(-1)
            }
          }
        }
      }
    }

判断缓存的命中会调用compute()方法

fun compute(): CacheStrategy {
      val candidate = computeCandidate()
		// 如果可以使用缓存,那networkRequest必定为null;指定了只使用缓存但是networkRequest又不为null,冲突,那就返回504
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }
      return candidate
    }

方法中调用computeCandidate()方法来完成真正的缓存判断。

1.缓存是否存在

      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }

cacheResponse是从缓存中找到的响应,如果为null,那就表示没有找到对应的缓存,创建的CacheStrategy实例对象只存在 networkRequest,这代表了需要发起网络请求。

2.https请求的缓存
继续往下走意味着cacheResponse必定存在,但是它不一定能用,后续进行有效性的一系列判断

 if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

如果本次请求是HTTPS,但是缓存中没有对应的握手信息,那么缓存无效。

3.响应码以及响应头

    if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }

主要关注下isCacheable方法:

 fun isCacheable(response: Response, request: Request): Boolean {
      // Always go to network for uncacheable response codes (RFC 7231 section 6.1), This
      // implementation doesn't support caching partial content.
      when (response.code) {
        HTTP_OK,
        HTTP_NOT_AUTHORITATIVE,
        HTTP_NO_CONTENT,
        HTTP_MULT_CHOICE,
        HTTP_MOVED_PERM,
        HTTP_NOT_FOUND,
        HTTP_BAD_METHOD,
        HTTP_GONE,
        HTTP_REQ_TOO_LONG,
        HTTP_NOT_IMPLEMENTED,
        StatusLine.HTTP_PERM_REDIRECT -> {
          // These codes can be cached unless headers forbid it.
        }

        HTTP_MOVED_TEMP,
        StatusLine.HTTP_TEMP_REDIRECT -> {
          // These codes can only be cached with the right response headers.
          // http://tools.ietf.org/html/rfc7234#section-3
          // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
          if (response.header("Expires") == null &&
              response.cacheControl.maxAgeSeconds == -1 &&
              !response.cacheControl.isPublic &&
              !response.cacheControl.isPrivate) {
            return false
          }
        }

        else -> {
          // All other codes cannot be cached.
          return false
        }
      }

      // A 'no-store' directive on request or response prevents the response from being cached.
      return !response.cacheControl.noStore && !request.cacheControl.noStore
    }

缓存响应中的响应码为 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308 的情况下,只判断服务器是不是给了Cache-Control: no-store (资源不能被缓存),所以如果服务器给到了这个响应头,那就和前面两个判定一致【缓存不可用】。否则继续进一步判断缓存是否可用。

而如果响应码是302/307(重定向),则需要进一步判断是不是存在一些允许缓存的响应头。如果存在Expires或者Cache-Control的值为:

  1. max-age=[秒] :资源最大有效时间;
  2. public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
  3. private :表明该资源只能被单个用户缓存,默认是private。

同时不存在 Cache-Control: no-store ,那就可以继续进一步判断缓存是否可用。

所以综合来看判定优先级如下:
1、响应码不为 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308,302,307 缓存不可用;
2、当响应码为302或者307时,未包含某些响应头,则缓存不可用;
3、当存在 Cache-Control: no-store 响应头则缓存不可用。

如果响应缓存可用,进一步再判断缓存有效性;

4.用户的请求配置

val requestCaching = request.cacheControl
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }

    private fun hasConditions(request: Request): Boolean =
        request.header("If-Modified-Since") != null || request.header("If-None-Match") != null
  }

到这里,OkHttp需要先对用户本次发起的Request进行判定,如果用户指定了Cache-Control:no-cache(不使用缓存)的请求头或者请求头包含If-Modified-SinceIf-None-Match(请求验证)。那么就不允许使用缓存。

请求头说明
Cache-Control: no- cache忽略缓存
If-Modified-Since: 时间值一般为 Data 或 lastModified ,服务器没有在指定的时间后修改请求对应资源,返回304(无修改)
If-None-Match:标记值一般为 Etag ,将其与请求对应资源的 Etag 值进行比较;如果匹配,返回304

这意味着如果用户请求头中包含了这些内容,那就必须向服务器发起请求。但是需要注意的是,OkHttp并不会缓存304的响应,如果是此种情况,即用户主动要求与服务器发起请求,服务器返回的304(无响应体),则直接把304的响应返回给用户:“既然你主动要求,我就只告知你本次请求结果”。而如果不包含这些请求头,那继续判定缓存有效性。

5.响应的缓存有效期
这里进一步根据缓存响应中的一些信息判定缓存是否处理有效期内,如果满足:
缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长
代表可以使用缓存。几种新鲜度可以理解为有效时间,而这里的缓存新鲜度 - 缓存最小新鲜度就代表了缓存真正有效的时间。

### CacheStrategy.computeCandidate
	//5.1获取缓存的响应从创建到现在的时间
 	  val ageMillis = cacheResponseAge()
 	  //5.2获取这个响应有效缓存的时长
      var freshMillis = computeFreshnessLifetime()
      if (requestCaching.maxAgeSeconds != -1) {
      // 如果请求中指定了max-age表示指定了能拿的缓存有效时长,就需要综合响应有效时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }
	// 5.3 请求包含 Cache-Control:min-fresh=[秒] 能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }
     // 5.4
	//5.4.1 Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认 
	//5.4.2 Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长 如果未指定多少秒,则表示无论过期 多长时间都可以;如果指定了,		   则只要是指定时间内就能使用缓存 
	// 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
      var maxStaleMillis: Long = 0
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }
		//5.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以 使用的时间
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
        // 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        // 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
        val oneDayMillis = 24 * 60 * 60 * 1000L
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
        }
        return CacheStrategy(null, builder.build())
      }

5.1、缓存到现在存活的时间:ageMillis
首先 cacheResponseAge() 方法获得了响应大概存在了多久:

   private fun cacheResponseAge(): Long {
      val servedDate = this.servedDate
      val apparentReceivedAge = if (servedDate != null) {
        maxOf(0, receivedResponseMillis - servedDate.time)
      } else {
        0
      }

      val receivedAge = if (ageSeconds != -1) {
        maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong()))
      } else {
        apparentReceivedAge
      }

      val responseDuration = receivedResponseMillis - sentRequestMillis
      val residentDuration = nowMillis - receivedResponseMillis
      return receivedAge + responseDuration + residentDuration
    }

1、 apparentReceivedAge 代表了客户端收到响应到服务器发出响应的一个时间差
seredData 是从缓存中获得的 Date 响应头对应的时间(服务器发出本响应的时间);
receivedResponseMillis 为本次响应对应的客户端发出请求的时间
2、 receivedAge 是代表了客户端的缓存,在收到时就已经存在多久了
ageSeconds 是从缓存中获得的 Age 响应头对应的秒数 (本地缓存的响应是由服务器的缓存返回,这个缓存在
服务器存在的时间) ageSeconds 与上一步计算结果 apparentReceivedAge 的最大值为收到响应时,这个响应数据已经存在多
久。
假设我们发出请求时,服务器存在一个缓存,其中 Data: 0点 。 此时,客户端在1小时候发起请求,此时由
服务器在缓存中插入 Age: 1小时 并返回给客户端,此时客户端计算的 receivedAge 就是1小时,这就代表了
客户端的缓存在收到时就已经存在多久了。(不代表到本次请求时存在多久了)
3、 responseDuration 是缓存对应的请求,在发送请求与接收请求之间的时间差
4、 residentDuration 是这个缓存接收到的时间到现在的一个时间差
receivedAge + responseDuration + residentDuration 所代表的意义就是:
缓存在客户端收到时就已经存在的时间 + 请求过程中花费的时间 + 本次请求距离缓存获得的时间,就是缓存真正存
在了多久。

5.2、缓存新鲜度(有效时间):freshMillis

  private fun computeFreshnessLifetime(): Long {
      val responseCaching = cacheResponse!!.cacheControl
      if (responseCaching.maxAgeSeconds != -1) {
        return SECONDS.toMillis(responseCaching.maxAgeSeconds.toLong())
      }

      val expires = this.expires
      if (expires != null) {
        val servedMillis = servedDate?.time ?: receivedResponseMillis
        val delta = expires.time - servedMillis
        return if (delta > 0L) delta else 0L
      }

      if (lastModified != null && cacheResponse.request.url.query == null) {
        // As recommended by the HTTP RFC and implemented in Firefox, the max age of a document
        // should be defaulted to 10% of the document's age at the time it was served. Default
        // expiration dates aren't used for URIs containing a query.
        val servedMillis = servedDate?.time ?: sentRequestMillis
        val delta = servedMillis - lastModified!!.time
        return if (delta > 0L) delta / 10 else 0L
      }

      return 0L
    }

缓存新鲜度(有效时长)的判定会有几种情况,按优先级排列如下:
1、缓存响应包含 Cache-Control: max-age=[秒] 资源最大有效时间;
2、缓存响应包含 Expires: 时间 ,则通过 Date 或接收该响应时间计算资源有效时间;
3、缓存响应包含 Last-Modified: 时间 ,则通过 Date 或发送该响应对应请求的时间计算资源有效时间;并且根据
建议以及在Firefox浏览器的实现,使用得到结果的10%来作为资源的有效时间。

5.3、缓存最小新鲜度:minFreshMillis

  var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }

如果用户的请求头中包含 Cache-Control: min-fresh=[秒] ,代表用户认为这个缓存有效的时长。假设本身缓存新
鲜度为: 100毫秒,而缓存最小新鲜度为:10毫秒,那么缓存真正有效时间为90ms;

5.4、缓存过期后仍有效时长:maxStaleMillis

   var maxStaleMillis: Long = 0
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }

这个判断中第一个条件为缓存的响应中没有包含 Cache-Control: must-revalidate (不可用过期资源),获得用户请
求头中包含 Cache-Control: max-stale=[秒] 缓存过期后仍有效的时长。

5.5、判定缓存是否有效

  if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
        }
        return CacheStrategy(null, builder.build())
      }

最后利用上4步产生的值,只要缓存的响应未指定 no-cache 忽略缓存,如果:缓存存活时间+缓存最小新鲜度 < 缓存新鲜度+过期后继续使用时长,代表可以使用缓存。假设 缓存到现在存活了:100 毫秒; 用户认为缓存有效时间(缓存最小新鲜度)为:10 毫秒; 缓存新鲜度为: 100毫秒; 缓存过期后仍能使用: 0 毫秒; 这些条件下,首先缓存的真实有效时间为: 90毫秒,而缓存已经过了这个
时间,所以无法使用缓存。不等式可以转换为: 缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长,即 存活时间 < 缓存有效时间 + 过期后继续使用时间;
总体来说,只要不忽略缓存并且缓存未过期,则使用缓存。

6.缓存过期处理

val conditionName: String
      val conditionValue: String?
      when {
        etag != null -> {
          conditionName = "If-None-Match"
          conditionValue = etag
        }

        lastModified != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = lastModifiedString
        }

        servedDate != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = servedDateString
        }
		//意味着无法与服务器发起比较,只能重新请求
        else -> return CacheStrategy(request, null) 
      }
      //添加请求头
      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      return CacheStrategy(conditionalRequest, cacheResponse)

如果继续执行,表示缓存已经过期无法使用。此时我们判定缓存的响应中如果存在 Etag ,则使用 If-None-Match
交给服务器进行验证;如果存在Last-Modified或者 Data ,则使用If-Modified-Since交给服务器验证。服务器
如果无修改则会返回304,这时候注意:
由于是缓存过期而发起的请求(与第4个判断用户的主动设置不同),如果服务器返回304,那框架会自动更新缓存,
所以此时 CacheStrategy 既包含 networkRequest 也包含 cacheResponse

7.收尾
至此,缓存的判定结束,拦截器中只需要判断 CacheStrategy 中 networkRequest 与 cacheResponse 的不同组合就
能够判断是否允许使用缓存。
但是需要注意的是,如果用户在创建请求时,配置了 onlyIfCached 这意味着用户这次希望这个请求只从缓存获
得,不需要发起请求。那如果生成的 CacheStrategy 存在 networkRequest 这意味着肯定会发起请求,此时出现冲
突!那会直接给到拦截器一个既没有 networkRequest 又没有 cacheResponse 的对象。拦截器直接返回用户 504 ;

小结

1.如果从缓存获取的Response是null,那就需要使用网络请求获取响应;
2.如果是Https请求,但是又丢失了握手信息,那也不能使用缓存,需要进行网络请求;
3.如果判断响应码不能缓存且响应头有no-store标识,那就需要进行网络请求;
4.如果请求头有no-cache标识或者有If-Modified-Since/If-None-Match ,那么需要进行网络请求;
5.如果响应头没有 no-cache 标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行网络请求;
6.如果缓存过期了,判断响应头是否设置Etag/Last-Modified/Date,没有那就直接使用网络请求否则需要考虑服务器返回304;

并且,只要需要进行网络请求,请求头中就不能包含·only-if-cached ,否则框架直接返回504;
缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,如果确定需要发起网络请求,则
下一个拦截器为 ConnectInterceptor

连接拦截器 ConnectInterceptor

打开与目标服务器的连接,并执行下一个拦截器。对应代码如下:

### ConnectInterceptor
object ConnectInterceptor : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    // 初始化Exchange
    val exchange = realChain.call.initExchange(chain)
    // 将Exchange交给Chain,此时下面的拦截器就可以通过调用Exchange的方法间接操作输入输出流
    val connectedChain = realChain.copy(exchange = exchange)
    return connectedChain.proceed(realChain.request)
  }
}

代码量很少,主要工作在realChain.call.initExchange(chain)中;

  internal fun initExchange(chain: RealInterceptorChain): Exchange {
    synchronized(this) {
      check(expectMoreExchanges) { "released" }
      check(!responseBodyOpen)
      check(!requestBodyOpen)
    }
	// exchangeFinder在RetryAndFollowUpInterceptor中创建
    val exchangeFinder = this.exchangeFinder!!
    // 返回一个解析器
    val codec = exchangeFinder.find(client, chain)
     // 将解析器再次包装为Exchange
    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
  }

执行完initExchange()方法Exchange初始化完毕,将持有解析器,相当于间接持有网络输出输入流,回到最初的地方。

小结

这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。

请求服务器拦截器 CallServerInterceptor

利用HttpCodec发出请求到服务器并且解析生成 Response
首先调用exchange.writeRequestHeaders(request);将请求头写入到缓存中(直到调用 flushRequest() 才真正发
送给服务器)。然后马上进行第一个逻辑判断

 override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val exchange = realChain.exchange!!
    val request = realChain.request
    val requestBody = request.body
    val sentRequestMillis = System.currentTimeMillis()

    exchange.writeRequestHeaders(request)

    var invokeStartEvent = true
    var responseBuilder: Response.Builder? = null
    if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
        exchange.flushRequest()
        responseBuilder = exchange.readResponseHeaders(expectContinue = true)
        exchange.responseHeadersStart()
        invokeStartEvent = false
      }
      if (responseBuilder == null) {
        if (requestBody.isDuplex()) {
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest()
          val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
          requestBody.writeTo(bufferedRequestBody)
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
          requestBody.writeTo(bufferedRequestBody)
          bufferedRequestBody.close()
        }
      } else {
        exchange.noRequestBody()
        if (!exchange.connection.isMultiplexed) {
          // HTTP2多路复用,不需要关闭socket,不管!
          exchange.noNewExchangesOnConnection()
        }
      }
    } else {
      exchange.noRequestBody()
    }

    if (requestBody == null || !requestBody.isDuplex()) {
      exchange.finishRequest()
    }
   
  }

整个if都和一个请求头有关: Expect: 100-continue 。这个请求头代表了在发送请求体之前需要和服务器确定是
否愿意接受客户端发送的请求体。所以 permitsRequestBody 判断为是否会携带请求体的方式(POST),如果命中
if,则会先给服务器发起一次查询是否愿意接收请求体,这时候如果服务器愿意会响应100(没有响应体,
responseBuilder 即为nul)。这时候才能够继续发送剩余请求数据。

但是如果服务器不同意接受请求体,那么我们就需要标记该连接不能再被复用,调用 noNewExchangesOnConnection() 关闭相关的Socket

接下来:

 if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
      if (invokeStartEvent) {
        exchange.responseHeadersStart()
        invokeStartEvent = false
      }
    }
    var response = responseBuilder
        .request(request)
        .handshake(exchange.connection.handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build()

这时 responseBuilder 的情况即为:
1、POST方式请求,请求头中包含 Expect ,服务器允许接受请求体,并且已经发出了请求体, responseBuilder 为null;
2、POST方式请求,请求头中包含 Expect ,服务器不允许接受请求体, responseBuilder 不为null
3、POST方式请求,未包含 Expect ,直接发出请求体, responseBuilder 为null;
4、POST方式请求,没有请求体, responseBuilder 为null;
5、GET方式请求, responseBuilder 为null;
对应上面的5种情况,读取响应头并且组成响应 Response ,注意:此 Response 没有响应体。同时需要注意的是,
如果服务器接受 Expect: 100-continue 这是不是意味着我们发起了两次 Request ?那此时的响应头是第一次查询
服务器是否支持接受请求体的,而不是真正的请求对应的结果响应。所以紧接着:

var code = response.code
    if (code == 100) {
      // Server sent a 100-continue even though we did not request one. Try again to read the actual
      // response status.
      responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
      if (invokeStartEvent) {
        exchange.responseHeadersStart()
      }
      response = responseBuilder
          .request(request)
          .handshake(exchange.connection.handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
      code = response.code
    }

如果响应是100,这代表了是请求 Expect: 100-continue 成功的响应,需要马上再次读取一份响应头,这才是真正
的请求对应结果响应头。

然后收尾

 exchange.responseHeadersEnd(response)
    response = if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response.newBuilder()
          .body(EMPTY_RESPONSE)
          .build()
    } else {
      response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build()
    }
    if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
        "close".equals(response.header("Connection"), ignoreCase = true)) {
      exchange.noNewExchangesOnConnection()
    }
    if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
      throw ProtocolException(
          "HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
    }
    return response
  }

forWebSocket 代表websocket的请求,我们直接进入else,这里就是读取响应体数据。然后判断请求和服务器是
不是都希望长连接,一旦有一方指明 close ,那么就需要关闭 socket 。而如果服务器返回204/205,一般情况而
言不会存在这些返回码,但是一旦出现这意味着没有响应体,但是解析到的响应头中包含 Content-Length 且不为
0,这表响应体的数据字节长度。此时出现了冲突,直接抛出协议异常!

小结

在这个拦截器中就是完成HTTP协议报文的封装与解析。

总结

整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。
这五个拦截器分别为:
重试重定向拦截器
桥接拦截器
缓存拦截器
连接拦截器
请求服务拦截器

每一个拦截器负责的工作不一样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。
但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结
果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。

当用户发起一个请求后,会由任务分发起 Dispatcher 将请求包装并交给重试拦截器处理。

1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码
判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。

2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP
压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。

4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处
理。

5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。
在经过了这一系列的流程后,就完成了一次HTTP请求。

附录

OkHttp原理8连问

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OkHttp 是一个开源的 HTTP 客户端,用于 Android 平台和 Java 应用。它建立在 Java 的 HttpURLConnection 类之上,并提供了更简洁、更强大的 API。 OkHttp 的工作原理主要涉及以下几个关键组件: 1. `OkHttpClient`:这是 OkHttp 的核心类,负责配置和创建请求、设置拦截器、管理连接池等。你可以通过构建 OkHttpClient 实例来自定义请求的行为和参数。 2. `Request`:表示一个 HTTP 请求,包括 URL、请求方法(如 GET、POST)、请求体、请求头等信息。你可以通过 Request.Builder 构建一个 Request 实例。 3. `Response`:表示一个 HTTP 响应,包括响应码、响应体、响应头等信息。OkHttp 会将服务器返回的数据解析成 Response 对象。 4. `Interceptor`:拦截器用于在发送请求和接收响应之前进行一些额外的处理。OkHttp 提供了很多内置的拦截器,如重试拦截器、缓存拦截器等,同时也支持自定义拦截器。 5. `Dispatcher`:调度器负责管理请求的调度和执行。它可以控制同时并发执行的请求数量,还可以设置请求超时时间等。 6. `ConnectionPool`:连接池用于管理 HTTP 连接的复用和回收。OkHttp 会自动复用连接以减少网络延迟,提高性能。 7. `Cache`:缓存可以保存服务器返回的响应,以便在后续的请求中复用。OkHttp 支持对响应进行缓存,并提供了灵活的配置选项。 当你使用 OkHttp 发起一个网络请求时,它会通过 OkHttpClient 来创建一个 Request 对象,并通过 Dispatcher 来执行这个请求。在执行过程中,OkHttp 会根据设置的拦截器进行一系列的处理,如添加请求头、重试、缓存等。最终,OkHttp 将返回一个 Response 对象,你可以从中获取到服务器返回的数据。 总体来说,OkHttp 的工作原理是通过封装底层的 HttpURLConnection,提供了简洁易用的 API,并通过拦截器和连接池等机制优化了网络请求的性能和可定制性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值