探索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 {
...
}
这里也就是将OkHttpClient
的cookieJar
属性作为构造参数传给了BridgeInterceptor
,我们查看OkHttpClient
的cookieJar
属性
val cookieJar: CookieJar = builder.cookieJar
可以看出与OkHttpClient.Builder
相关,OkHttpClient.Builder
的cookieJar
属性如下
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
使用的cookieJar
是CookieJar.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)
}
默认情况下,由于cookieJar
是CookieJar.NO_COOKIES
,所以这里会直接return
,实际上并没有进行cookie
的解析与保存。
当然,用户可以在构造OkHttpClient
的时候,在OkHttpClient.Builder
中传入自定义的cookieJar
,从而实现cookie
的存储与获取的功能。
总结
我们对BridgeInterceptor
的intercept
方法的执行流程做一个总结
-
根据原来的
userRequest
,构建一个Request.Builder
,往该Builder
中添加或移除Header
,完成请求的转换 -
在请求转换的过程中,若发现用户原来的请求没有添加
Accept-Encoding
和Range
,就往Request.Builder
里面添加Accept-Encoding : gzip
这个Header
,用于告诉服务端,客户端希望服务端以gzip
编码方式将内容压缩后传输过来 -
在请求转换的过程中,可能会往
Request.Builder
添加Cookie
的Header
-
调用
chain.proceed
方法将转换后的请求交给下一个拦截器,同时获取其返回的Response
-
若返回的
Response
中携带Cookie
,则尝试进行解析保存 -
若返回的
Response
需要gzip
解压,则进行gzip
解压缩
注意的点
-
当用户手动设置
Accept-Encoding
头信息时,拿到Response
后不进行gzip
解压缩(可定位到transparentGzip
变量) -
在
gzip
解压缩、对响应进行转换的时候,转换后的Response
移除了Content-Length
字段,并且转换后的Response
的body
的contentLength
属性值被设置为-1,因此上层代码在获取其body
的contentLength
的值的时候,可能会获取到-1