探索OkHttp系列 (三) 请求与响应的转换

前言

上一篇文章,我们介绍了RetryAndFollowUpInterceptor拦截器,该拦截器的下一个拦截器就是BridgeInterceptor,本篇文章我们就对BridgeInterceptor进行一个介绍。

BridgeInterceptor的名字可以看出,它起的是一个桥接的作用:

  • 将用户构造的请求转换为发送给服务器的请求,请求转换的过程就是添加一些服务器端需要的header信息
  • 将服务器的响应转换为对用户友好的响应,响应转换的过程就是进行gzip解压

BridgeInterceptor

intercept

BridgeInterceptor::intercept如下

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val userRequest = chain.request()  
    val requestBuilder = userRequest.newBuilder()     
      
    /* 开始进行请求转换 */  
      
    val body = userRequest.body
    // 如果有Request Body,将body的一些属性设置到Request当中  
    if (body != null) {
      val contentType = body.contentType()
      // 设置Content-Type  
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString())
      }

      // Request Body有指明长度,则在Request中标明 Content-Length 这个Header。
      // Request Body没有指明长度,则在Request中标明 Transfer-Encoding:chunked 这个Header,
      // 表示要使用分块传输编码。
      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")
    }

    // 表示服务器传输过来的内容是否使用gzip的编码方式进行压缩  
    var transparentGzip = false
    // 如果用户的Request没有使用 Accept-Encoding 指明服务器传输过来的内容的编码方式,并且
    // Request中没有使用 Range 这个Header,那么就将transparentGzip设置为true,并且在Request
    // 中添加 Accept-Encoding : gzip 这个Header,表明客户端希望服务端传输的内容使用gzip进行
    // 编码。
    // 如果在这里添加了 Accept-Encoding: gzip 这个Header,那么该方法在接收到下一个拦截器
    // 的Response之后,也会负责对服务端传输过来的内容进行解压  
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true
      requestBuilder.header("Accept-Encoding", "gzip")
    }

    // cookieJar是BridgeInterceptor的构造方法要求传入的,在ReallCall的
    // getResponseWithInterceptorChain方法会构造BridgeInterceptor,传入的是OkHttpClient
    // 的cookieJar属性,默认情况下,该cookieJar并没有包含cookie。
    // 这一行语句的意思是,从cookieJar从查找与url相关的cookie列表  
    val cookies = cookieJar.loadForRequest(userRequest.url)
    // 若cookie含有元素,那么就在Request上面设置 Cookie 的 Header  
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }
      
	/* 请求转换完成 */
      
    // 调用 chain.proceed 方法将请求交给下一个拦截器处理,同时获得下一个拦截器返回的Response  
    val networkResponse = chain.proceed(requestBuilder.build())

    // 若Response中携带Cookie,则对Cookie进行解析并保存  
    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)

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

    /* 开始进行响应的转换 */  
      
    // 如果前面指定了gzip的压缩方式,并且返回的Response中带有 Content-Encoding: gzip 的 Header,
    // 那么就进行gzip解压  
    if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        networkResponse.promisesBody()) {
      val responseBody = networkResponse.body
      if (responseBody != null) {
        val gzipSource = GzipSource(responseBody.source())
        // 移除Content-Encoding、Content-Length这两个 Header
        val strippedHeaders = networkResponse.headers.newBuilder()
            .removeAll("Content-Encoding")
            .removeAll("Content-Length")
            .build()
        // 设置新的Header
        responseBuilder.headers(strippedHeaders)
        // 获取Content-Type  
        val contentType = networkResponse.header("Content-Type")
        // 设置Response Body,注意这里将第二个参数contentLength设置为-1  
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }

    /* 响应转换完成 */  
      
    // 返回转换后的响应  
    return responseBuilder.build()
  }

上面涉及到的Header的部分说明如下

  • 请求首部字段

    • Accept-Encoding:该首部字段用来告知服务器用户代理支持的内容编码及内容编码的优先级顺序。可一次性指定多种内容编码。
    • Range:对于只需获取部分资源的范围请求,包含首部字段Range即可告知服务器资源的指定范围。
    • User-Agent:用于传达浏览器的种类。
  • 实体首部字段

    • Content-Encoding:该首部字段会告知客户端服务器对实体的主体部分选用的内容编码方式,如Content-Encoding: gzip
    • Content-Length:该首部字段表明了实体主体部分的大小(单位是字节)。对实体主体进行内容编码传输时,不能再使用Content-Length首部字段。

cookie的逻辑

上面涉及到了cookie的相关逻辑,这里梳理一下。

ReallCall::getResponseWithInterceptorChain中,构建了BridgeInterceptor,构建代码如下

  internal fun getResponseWithInterceptorChain(): Response {
  	...
    interceptors += BridgeInterceptor(client.cookieJar)  
    ...  
  }

BridgeInterceptor的构造函数如下

class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
	...
}

这里也就是将OkHttpClientcookieJar属性作为构造参数传给了BridgeInterceptor,我们查看OkHttpClientcookieJar属性

val cookieJar: CookieJar = builder.cookieJar

可以看出与OkHttpClient.Builder相关,OkHttpClient.BuildercookieJar属性如下

internal var cookieJar: CookieJar = CookieJar.NO_COOKIES

可以看到它的默认值是CookieJar.NO_COOKIES,我们查看CookieJar这个接口

interface CookieJar {
  // 保存cookies,该cookies与传入的url相关联,具体的存储方式自己实现  
  fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
  // 为url返回对应的cookie列表,返回的列表也可能没有包含元素
  // 该方法的简单实现可以是返回,已接受的、匹配该url的、仍未过期的cookies  
  fun loadForRequest(url: HttpUrl): List<Cookie>

  companion object {
    /** A cookie jar that never accepts any cookies. */
    
    // 一个已经定义好的CookieJar的实现类  
    @JvmField
    val NO_COOKIES: CookieJar = NoCookies()
    private class NoCookies : CookieJar {
      // 不存储任何的cookies  
      override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
      }

      // 返回一个空的cookie列表  
      override fun loadForRequest(url: HttpUrl): List<Cookie> {
        return emptyList()
      }
    }
  }
}

那么,默认情况下,BridgeInterceptor使用的cookieJarCookieJar.NO_COOKIES,在intercept方法进行请求转换的时候,有下面的这段代码

    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }

默认情况下,获取到的cookies是一个空的cookie列表。

在获取到下一个拦截器返回的Response后,intercept方法有这样的一段代码

    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)

调用了CookieJar的扩展函数

fun CookieJar.receiveHeaders(url: HttpUrl, headers: Headers) {
  if (this === CookieJar.NO_COOKIES) return

  val cookies = Cookie.parseAll(url, headers)
  if (cookies.isEmpty()) return

  saveFromResponse(url, cookies)
}

默认情况下,由于cookieJarCookieJar.NO_COOKIES,所以这里会直接return,实际上并没有进行cookie的解析与保存。

当然,用户可以在构造OkHttpClient的时候,在OkHttpClient.Builder传入自定义的cookieJar,从而实现cookie的存储与获取的功能。

总结

我们对BridgeInterceptorintercept方法的执行流程做一个总结

  1. 根据原来的userRequest,构建一个Request.Builder,往该Builder中添加或移除Header,完成请求的转换

  2. 在请求转换的过程中,若发现用户原来的请求没有添加Accept-EncodingRange,就往Request.Builder里面添加Accept-Encoding : gzip这个Header,用于告诉服务端,客户端希望服务端以gzip编码方式将内容压缩后传输过来

  3. 在请求转换的过程中,可能会往Request.Builder添加CookieHeader

  4. 调用chain.proceed方法将转换后的请求交给下一个拦截器,同时获取其返回的Response

  5. 若返回的Response中携带Cookie,则尝试进行解析保存

  6. 若返回的Response需要gzip解压,则进行gzip解压缩

注意的点

  • 当用户手动设置Accept-Encoding头信息时,拿到Response后不进行gzip解压缩(可定位到transparentGzip变量)

  • gzip解压缩、对响应进行转换的时候,转换后的Response移除了Content-Length字段,并且转换后的ResponsebodycontentLength属性值被设置为-1,因此上层代码在获取其bodycontentLength的值的时候,可能会获取到-1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值