Http协议&OkHttp3 知识点汇总

108 阅读6分钟

1. Http2.0 相比Http1.1有哪些改进

  1. 多路复用,同一个TCP连接可以发送多个http请求。
  2. 相比http1.1使用文本传输,Http2.0采用了二进制格式,文本的种类繁多要做到全面覆盖足够健壮较复杂,使用二进制格式解析方便且高效。同时二进制数据在一次交互中有的概念,流有唯一ID,一个流上有一个或多个帧,帧可以乱序发送,在接收端可以重组数据。

例如:流 1 - HTML

  • 流标识符:1
  • 帧 1(HEADERS 帧):包含请求的头部信息
  • 帧 2(DATA 帧):包含 HTML 内容的一部分
  • 帧 3(DATA 帧):包含 HTML 内容的另一部分
  1. Header压缩,由于Header传输的数据key很多都是重复的,没必要每次都传输,在两端通过建立静态表保存固定的字段,动态表保存动态变更的字段,两端传输数据时使用编码器将key转化成表格索引号来执行传输,收到数据的一端再用解码器转换成实际的header key。
  2. 支持push,服务器主动向客户端发送消息。

2. Http3.0 相比Http2.0有哪些改进

主要是改用UDP来传输数据,解决TCP传输效率低下的问题。 具体参考www.cnblogs.com/wiesslibrar…

3. OKhttp3有哪些默认拦截器

3.1 RetryAndFollowUpInterceptor重试重定向拦截器

主要功能:通过recover()方法来判断是否有必要进行重试,若是一些不可更改的错误,则直接抛出异常,终止此次请求,否则执行下一次循环。源码中有一些无法重试的错误包括tcp错误,非超时连接的断连IO错误等等。

private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
  // If there was a protocol problem, don't recover.
  if (e is ProtocolException) {
    return false
  }

  // If there was an interruption don't recover, but if there was a timeout connecting to a route
  // we should try the next route (if there is one).
  if (e is InterruptedIOException) {
    return e is SocketTimeoutException && !requestSendStarted
  }

  // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
  // again with a different route.
  if (e is SSLHandshakeException) {
    // If the problem was a CertificateException from the X509TrustManager,
    // do not retry.
    if (e.cause is CertificateException) {
      return false
    }
  }
  if (e is SSLPeerUnverifiedException) {
    // e.g. a certificate pinning error.
    return false
  }
  // An example of one we might want to retry with a different route is a problem connecting to a
  // proxy and would manifest as a standard IOException. Unless it is one we know we should not
  // retry, we return true and try a new route.
  return true
}

重定向逻辑是在拦截器后半段

override fun intercept(chain: Interceptor.Chain): Response {
      val exchange = call.interceptorScopedExchange
      //followUpRequest() 通过获得响应来判断是否需要进行重定向及具体操作
      val followUp = followUpRequest(response, exchange)

      if (followUp == null) {
        // ...
      }

      val followUpBody = followUp.body
      if (followUpBody != null && followUpBody.isOneShot()) {
        closeActiveExchange = false
        return response
      }

      response.body?.closeQuietly()
// 如果超过了重定向的最大重试次数,则抛出错误
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw ProtocolException("Too many follow-up requests: $followUpCount")
      }
// 重定向的新的request
      request = followUp
      priorResponse = response
    } finally {
      call.exitNetworkInterceptorExchange(closeActiveExchange)
    }
  }
}

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

  val method = userResponse.request.method
  when (responseCode) {
    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)
    }

    HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
// 如果是308,307,300,301,302,303这些状态码,则重建request
    HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
      return buildRedirectRequest(userResponse, method)
    }
    // ...
}

3.2 BridgeInterceptor

BridgeInterceptor主要的作用是将用户配置的头部信息转换成http请求的头部信息,比如Content-Type,Content-Length,Transfer-Encoding,Host,Connection。同时默认添加了Gzip压缩的Accept-Encoding,再返回的response后也解压出具体的数据

override fun intercept(chain: Interceptor.Chain): Response {
  val userRequest = chain.request()
  val requestBuilder = userRequest.newBuilder()
  val body = userRequest.body
  if (body != null) {
    val contentType = body.contentType()
    if (contentType != null) {
      requestBuilder.header("Content-Type", contentType.toString())
    }

    val contentLength = body.contentLength()
    if (contentLength != -1L) {
      requestBuilder.header("Content-Length", contentLength.toString())
      requestBuilder.removeHeader("Transfer-Encoding")
    } else {
      requestBuilder.header("Transfer-Encoding", "chunked")
      requestBuilder.removeHeader("Content-Length")
    }
  }

  if (userRequest.header("Host") == null) {
    requestBuilder.header("Host", userRequest.url.toHostHeader())
  }
  if (userRequest.header("Connection") == null) {
    requestBuilder.header("Connection", "Keep-Alive")
  }

  // 未配置Accept-Encoding主动配置成Gzip,并负责解压
  var transparentGzip = false
  if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true
    requestBuilder.header("Accept-Encoding", "gzip")
  }
// ...

  val responseBuilder = networkResponse.newBuilder()
      .request(userRequest)

  if (transparentGzip &&
      "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
      networkResponse.promisesBody()) {
    val responseBody = networkResponse.body
    if (responseBody != null) {
      val gzipSource = GzipSource(responseBody.source())
      val strippedHeaders = networkResponse.headers.newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build()
      responseBuilder.headers(strippedHeaders)
      val contentType = networkResponse.header("Content-Type")
      // content-Length一般是分块传输或者压缩传输时无法确定原始长度是传递-1
      responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
    }
  }

  return responseBuilder.build()
}

3.3 CacheInterceptor

源码解读可以参考这篇文章: OKHTTP核心之五大内置拦截器源码分析 基本是按照Http缓存策略来处理的,根据头部的配置来读写缓存。

3.3.1 Cache-Control强制缓存策略

  1. no-store,针对变化频率很高的资源,不允许客户端缓存,每次必须访问服务器。
  2. no-cache,(很容易误解)实际是允许使用缓存,但是使用前必须和服务器验证,如果返回403表示缓存可用,就可以使用缓存。
  3. must-recalidate,表示允许缓存,并且如果缓存不过期的话,先使用缓存,如果缓存过期的话,再去服务器端进行验证
  4. max-age=xx,表示缓存过期计时时间,这个是服务端响应报文创建时间为节点,由于网络传输还有一定时间间隔,客户端能实际使用的缓存时间会短一点。

如果同时配置了上述策略,以上述1234优先级顺序来执行策略,比如设置了no-cache和max-age,即使在缓存过期时间内也会去访问服务器确定缓存的有效性。

3.3.2 协商缓存策略:Last-Modified/if-Modified-Since

如果服务器返回了Last-Modified: xxx,表示资源上一次修改的时间,客户端可以记录该时间,再下一次的请求头中添加if-Modified-Since,服务器会根据这个值在对比资源上次修改时间确定返回403还是新资源。

3.3.3 协商缓存策略:ETag/If-None-Match

如果服务器返回了ETag:id,表示返回了上一次资源的唯一ID,客户端可以记录该ID,在下一次请求头中添加If-None-Match:id,服务器会对比资源ID确认缓存有效性。如果同时返回了If-None-Match和if-Modified-Since,优先查看资源ID,因为资源可能改变后又变回来了,修改时间产生了变化但是资源本身其实未变。

3.4 ConnectInterceptor

也参考OKHTTP核心之五大内置拦截器源码分析

4. 网络拦截器VS应用拦截器

应用拦截器

  • 不需要考虑重定向和重试等中间响应。
  • 仅调用一次,即使 http 响应是从缓存中获取的结果。
  • 主要关注应用程序的原始意图,不关心 okhttp 注入的头,如 If-None-Match ...
  • 允许短路操作,即 不调用 Chain.proceed()。
  • 允许重试并多次调用 Chain.proceed()
  • 可以使用 withConnectTimeout, withReadTimeout, withWriteTimeout 来调整 Call 超时时间。

网络拦截器

  • 能够操作中间处理过程,如重定向和重试。
  • 不关注 cache 层拦截的短路操作
  • 关注网络层数据传输
  • 执行 Connection 请求

image.png

参考官网:square.github.io/okhttp/feat…

5. OkHttp拦截器和自定义拦截器执行顺序

直接上源码:RealCall.kt 可以看出用户添加的拦截器会被首先添加,默认拦截器后再后续依次添加。再依据拦截器内部实现是在获取response前执行部分代码,获取之后再执行部分代码,所以基本是一个U型结构,先处理request的后处理response,跟View的拦截器处理模式类似。

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(
      //...
  )

  var calledNoMoreExchanges = false
  try {
    val response = chain.proceed(originalRequest)
    // ...

盗图盗图:

image.png

参考

  1. OKHTTP核心之五大内置拦截器源码分析
  2. http3.0改进优势