前言
之前我们结合设计模式简单说了下OkHttp
的大体流程,今天就继续说说它的核心部分——拦截器
。
因为拦截器组成的链其实是完成了网络通信的整个流程,所以我们今天就从这个角度说说各拦截器的功能。
首先,做一下简单回顾,从getResponseWithInterceptorChain
方法开始。
简单回顾(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(
interceptors = interceptors
//...
)
val response = chain.proceed(originalRequest)
}
这些拦截器会形成一条链,组织了请求接口的所有工作。
)
以上为上节内容,不了解的朋友可以返回上一篇文章看看。
假如我来设计拦截器
先抛开拦截器的这些概念不谈,我们回顾下网络通信过程
,看看实现一个网络框架至少要有哪些功能。
请求过程
:封装请求报文、建立TCP连接、向连接中发送数据响应过程
:从连接中读取数据、处理解析响应报文
而之前说过拦截器的基本代码格式是这样:
override fun intercept(chain: Interceptor.Chain): Response {
//做事情A
response = realChain.proceed(request)
//做事情B
}
也就是分为 请求前工作,请求传递,获取响应后工作 三部分。
那我们试试能不能把上面的功能分一分,设计出几个拦截器?
拦截器1
: 处理请求前的请求报文封装
,处理响应后的响应报文分析
诶,不错吧,拦截器1就用来处理 请求报文和响应报文的一些封装和解析工作。就叫它封装拦截器吧。
拦截器2
: 处理请求前的建立TCP连接
肯定需要一个拦截器用来建立TCP连接,但是响应后好像没什么需要做连接方面的工作了?那就先这样,叫它连接拦截器吧。
拦截器3
:处理请求前的数据请求(写到数据流中)
处理响应后的数据获取(从数据流拿数据)
这个拦截器就负责TCP连接后的 I/O操作,也就是从流中读取和获取数据。就叫它 数据IO拦截器 吧。
好了,三个拦截器好像足够了,我得意满满的偷看了一眼okhttp拦截器代码,7个???我去。。
那再思考思考🤔...,还有什么情况没考虑到呢?比如失败重试?返回301重定向?缓存的使用?用户自己对请求的统一处理?
所以又可以模拟出几个新的拦截器:
拦截器4
:处理响应后的失败重试和重定向功能
没错,刚才只考虑到请求成功,请求失败了要不要重试呢?响应码为301、302时候的重定向处理?这都属于要重新请求的部分,肯定不能丢给用户,需要网络框架自己给处理好。就叫它 重试和重定向拦截器吧。
拦截器5
:处理响应前的缓存复用
,处理响应后的缓存响应数据
。
还有一个网络请求有可能的需求就是关于缓存,这个缓存的概念可能有些朋友了解的不多,其实它多用于浏览器中。
浏览器缓存一般分为两部分:强制缓存和协商缓存
。
强制缓存
就是服务器会告诉客户端该怎么缓存,例如 cache-Control
字段,随便举几个例子:
private
:所有内容只有客户端可以缓存,Cache-Control的默认取值max-age=xxx
:表示缓存内容将在xxx秒后失效no-cache
:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定no-store
:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
协商缓存
就是需要客户端和服务器进行协商后再决定是否使用缓存,比如强制缓存过期失效了,就要再次请求服务器,并带上缓存标志,例如Etag。
客户端再次进行请求的时候,请求头带上If-None-Match
,也就是之前服务器返回的Etag值。
Etag值就是文件的唯一标示,服务器通过某个算法对资源进行计算,取得一串值(类似于文件的md5值),之后将该值通过etag返回给客户端
然后服务器就会将Etag
值和服务器本身文件的Etag
值进行比较,如果一样则数据没改变,就返回304
,代表你要请求的数据没改变,你直接用就行啦。
如果不一致,就返回新的数据,这时候的响应码就是正常的200
。
这个拦截器就是用于处理这些情况,我们就叫它 缓存拦截器
吧。
- 拦截器6: 自定义拦截器
最后就是自定义的拦截器了,要给开发者一个可以自定义的拦截器,用于统一处理请求或响应数据。
这下好像齐了,至于之前说的7个拦截器还有1个,留个悬念最后再说。
最后再给他们排个序吧:
- 1、自定义拦截器的公共参数处理。
- 2、封装拦截器封装请求报文
- 3、缓存拦截器的缓存复用。
- 4、连接拦截器建立TCP连接。
- 5、IO拦截器的数据写入。
- 6、IO拦截器的数据读取。
- 7、缓存拦截器保存响应数据缓存。
- 8、封装拦截器分析响应报文
- 9、重试和重定向拦截器处理重试和重定向情况。
- 10、自定义拦截器统一处理响应数据。
有点绕,来张图瞧一瞧:
所以,拦截器的顺序也基本固定了:
- 1、自定义拦截器
- 2、重试和重定向拦截器
- 3、封装拦截器
- 4、缓存拦截器
- 5、连接拦截器
- 6、IO拦截器
下面具体看看吧。
自定义拦截器
在请求之前,我们一般创建自己的自定义拦截器,用于添加一些接口公共参数,比如把token
加到Header中。
class MyInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
request = request.newBuilder()
.addHeader("token", "token")
.url(url)
.build()
return chain.proceed(request)
}
要注意的是,别忘了调用chain.proceed
,否则这条链就无法继续下去了。
在获取响应之后,我们一般用拦截器进行结果打印,比如常用的HttpLoggingInterceptor
。
addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
重试和重定向拦截器(RetryAndFollowUpInterceptor)
为了方便理解,我对源码进行了修剪✂️:
class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
while (true) {
try {
try {
response = realChain.proceed(request)
} catch (e: RouteException) {
//路由错误
continue
} catch (e: IOException) {
// 请求错误
continue
}
//获取响应码判断是否需要重定向
val followUp = followUpRequest(response, exchange)
if (followUp == null) {
//没有重定向
return response
}
//赋予重定向请求,再次进入下一次循环
request = followUp
}
}
}
}
这样代码就很清晰了,重试和重定向的处理都是需要重新请求,所以这里用到了while循环。
- 当发生请求过程中错误的时候,就需要重试,也就是通过continue进入下一次循环,重新走到
realChain.proceed
方法进行网络请求。 - 当请求结果需要
重定向
的时候,就赋予新的请求,并进入下一次循环,重新请求网络。 - 当请求结果没有重定向,那么就直接返回
response
响应结果。
封装拦截器(BridgeInterceptor)
class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//添加头部信息
requestBuilder.header("Content-Type", contentType.toString())
requestBuilder.header("Host", userRequest.url.toHostHeader())
requestBuilder.header("Connection", "Keep-Alive")
requestBuilder.header("Accept-Encoding", "gzip")
requestBuilder.header("Cookie", cookieHeader(cookies))
requestBuilder.header("User-Agent", userAgent)
val networkResponse = chain.proceed(requestBuilder.build())
//解压
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())
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
}
}
return responseBuilder.build()
}
请求前的代码很简单,就是添加了一些必要的头部信息,包括Content-Type、Host、Cookie
等等,封装成一个完整的请求报文,然后交给下一个拦截器。
而获取响应后的代码就有点不是很明白了,gzip
是啥?GzipSource
又是什么类?
gzip压缩是基于deflate中的算法进行压缩的,gzip会产生自己的数据格式,gzip压缩对于所需要压缩的文件,首先使用LZ77算法进行压缩,再对得到的结果进行huffman编码,根据实际情况判断是要用动态huffman编码还是静态huffman编码,最后生成相应的gz压缩文件。
简单的说,gzip
就是一种压缩方式,可以将数据进行压缩,在添加头部信息的时候就添加了这样一个头部:
requestBuilder.header("Accept-Encoding", "gzip")
这一句其实就是在告诉服务器,客户端所能接受的文件的压缩格式,这里设置了gzip
之后,服务器看到了就能把响应报文数据进行gzip
压缩再传输,提高传输效率,节省流量。
所以请求之后的这段关于gzip
的处理其实就是客户端对压缩数据进行解压缩,而GzipSource
是okio库里面一个进行解压缩读取数据的类。
缓存拦截器(CacheInterceptor)
继续看缓存拦截器—CacheInterceptor
。
class CacheInterceptor(internal val cache: Cache?) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//取缓存
val cacheCandidate = cache?.get(chain.request())
//缓存策略类
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
// 如果不允许使用网络,并且缓存数据为空
if (networkRequest == null && cacheResponse == null) {
return Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(HTTP_GATEWAY_TIMEOUT)//504
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build().also {
listener.satisfactionFailure(call, it)
}
}
// 如果不允许使用网络,但是有缓存
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build().also {
listener.cacheHit(call, it)
}
}
networkResponse = chain.proceed(networkRequest)
// 如果缓存不为空
if (cacheResponse != null) {
//304,表示数据未修改
if (networkResponse?.code == HTTP_NOT_MODIFIED) {
cache.update(cacheResponse, response)
return response
}
}
//如果开发者设置了缓存,则将响应数据缓存
if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
//缓存header
val cacheRequest = cache.put(response)
//缓存body
return cacheWritingResponse(cacheRequest, response)
}
}
return response
}
}
还是分两部分看:
请求之前
,通过request获取了缓存,然后判断缓存为空,就直接返回code为504的结果。如果有缓存并且缓存可用,则直接返回缓存。请求之后
,如果返回304
代表服务器数据没修改,则直接返回缓存。如果cache
不为空,那么就把response
缓存下来。
这样看是不是和上面我们说过的缓存机制对应上了?请求之前就是处理强制缓存
的情况,请求之后就会处理协商缓存
的情况。
但是还是有几个问题需要弄懂:
1、缓存是怎么存储和获取的?
2、每次请求都会去存储和获取缓存吗?
3、缓存策略(CacheStrategy)到底是怎么处理网络和缓存的?networkRequest什么时候为空?
首先,看看缓存哪里取的:
val cacheCandidate = cache?.get(chain.request())
internal fun get(request: Request): Response? {
val key = key(request.url)
val snapshot: DiskLruCache.Snapshot = try {
cache[key] ?: return null
}
val entry: Entry = try {
Entry(snapshot.getSource(ENTRY_METADATA))
}
val response = entry.response(snapshot)
if (!entry.matches(request, response)) {
response.body?.closeQuietly()
return null
}
return response
}
通过cache.get
方法获取了response缓存,get方法中主要是用到了请求Request的url
来作为获取缓存的标志。
所以我们可以推断,缓存的获取是通过请求的url作为key来获取的。
那么cache
又是哪里来的呢?
val cache: Cache? = builder.cache
interceptors += CacheInterceptor(client.cache)
class CacheInterceptor(internal val cache: Cache?) : Interceptor
没错,就是实例化CacheInterceptor
的时候传进去的,所以这个cache是需要我们创建OkHttpClient
的时候设置的,比如这样:
val okHttpClient =
OkHttpClient().newBuilder()
.cache(Cache(cacheDir, 10 * 1024 * 1024))
.build()
这样设置之后,okhttp
就知道cache
存在哪里,大小为多少,然后就可以进行服务器响应的缓存处理了。
所以第二个问题也解决了,并不是每次请求都会去处理缓存,而是开发者需要去设置缓存的存储目录和大小,才会针对缓存进行这一系列的处理操作。
最后再看看缓存策略方法 CacheStrategy.Factory().compute()
class CacheStrategy internal constructor(
val networkRequest: Request?,
val cacheResponse: Response?
)
fun compute(): CacheStrategy {
val candidate = computeCandidate()
return candidate
}
private fun computeCandidate(): CacheStrategy {
//没有缓存情况下,返回空缓存
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
//...
//缓存控制不是 no-cache,且未过期
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
val builder = cacheResponse.newBuilder()
return CacheStrategy(null, builder.build())
}
return CacheStrategy(conditionalRequest, cacheResponse)
}
在这个缓存策略生存的过程中,只有一种情况下会返回缓存,也就是缓存控制不是no-cache
,并且缓存没过期情况下,就返回缓存,然后设置networkRequest为空。
所以也就对应上一开始缓存拦截器中的获取缓存后的判断:
// 如果不允许使用网络,但是有缓存,则直接返回缓存
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build().also {
listener.cacheHit(call, it)
}
}
连接拦截器(ConnectInterceptor)
继续,连接拦截器,之前说了是关于TCP连接
的。
object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
val exchange = realChain.call.initExchange(chain)
val connectedChain = realChain.copy(exchange = exchange)
return connectedChain.proceed(realChain.request)
}
}
代码看着倒是挺少的,但其实这里面很复杂很复杂,不着急,我们慢慢说。
这段代码就执行了一个方法就是initExchange
方法:
internal fun initExchange(chain: RealInterceptorChain): Exchange {
val codec = exchangeFinder.find(client, chain)
val result = Exchange(this, eventListener, exchangeFinder, codec)
return result
}
fun find(
client: OkHttpClient,
chain: RealInterceptorChain
): ExchangeCodec {
try {
val resultConnection = findHealthyConnection(
connectTimeout = chain.connectTimeoutMillis,
readTimeout = chain.readTimeoutMillis,
writeTimeout = chain.writeTimeoutMillis,
pingIntervalMillis = client.pingIntervalMillis,
connectionRetryEnabled = client.retryOnConnectionFailure,
doExtensiveHealthChecks = chain.request.method != "GET"
)
return resultConnection.newCodec(client, chain)
}
}
好像有一点眉目了,找到一个ExchangeCodec类,并封装成一个Exchange类。
ExchangeCodec
:是一个连接所用的编码解码器,用于编码HTTP请求和解码HTTP响应。Exchange
:封装这个编码解码器的一个工具类,用于管理ExchangeCodec,处理实际的 I/O。
明白了,这个连接拦截器(ConnectInterceptor)就是找到一个可用连接呗,也就是TCP连接,这个连接就是用于HTTP请求和响应的。
你可以把它可以理解为一个管道
,有了这个管道,才能把数据丢进去,也才可以从管道里面取数据。
而这个ExchangeCodec
,编码解码器就是用来读取和输送到这个管道的一个工具,相当于把你的数据封装成这个连接(管道)需要的格式。
我咋知道的?我贴一段ExchangeCodec代码你就明白了:
//Http1ExchangeCodec.java
fun writeRequest(headers: Headers, requestLine: String) {
check(state == STATE_IDLE) { "state: $state" }
sink.writeUtf8(requestLine).writeUtf8("\r\n")
for (i in 0 until headers.size) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n")
}
sink.writeUtf8("\r\n")
state = STATE_OPEN_REQUEST_BODY
}
这里贴的是Http1ExchangeCodec
的write代码,也就是Http1的编码解码器。
很明显,就是将Header信息一行一行写到sink中,然后再由sink交给输出流,具体就不分析了。只要知道这个编码解码器就是用来处理连接中进行输送的数据即可。
然后就是这个拦截器的关键了,连接到底是怎么获取的呢?继续看看:
private fun findConnection(): RealConnection {
// 1、复用当前连接
val callConnection = call.connection
if (callConnection != null) {
//检查这个连接是否可用和可复用
if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
toClose = call.releaseConnectionNoEvents()
}
return callConnection
}
//2、从连接池中获取可用连接
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
//3、从连接池中获取可用连接(通过一组路由routes)
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
return result
}
route = localRouteSelection.next()
// 4、创建新连接
val newConnection = RealConnection(connectionPool, route)
newConnection.connect
// 5、再获取一次连接,防止在新建连接过程中有其他竞争连接被创建了
if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
return result
}
//6、还是要使用创建的新连接,放入连接池,并返回
connectionPool.put(newConnection)
return newConnection
}
获取连接的过程很复杂,为了方便看懂,我简化了代码,分成了6步。
- 1、检查当前连接是否可用。
怎么判断可用的?主要做了两个判断
1)判断是否不再接受新的连接
2)判断和当前请求有相同的主机名和端口号。
这倒是很好理解,要这个连接是连接的同一个地方才能复用是吧,同一个地方怎么判断?就是判断主机名和端口号
。
还有个问题就是为什么有当前连接??明明还没开始连接也没有获取连接啊,怎么连接就被赋值了?
还记得重试和重定向
拦截器吗?对了,就是当请求失败需要重试的时候或者重定向的时候,这时候连接还在呢,是可以直接进行复用的。
- 2和3、从连接池中获取可用连接
第2步和第3步都是从连接池获取连接,有什么不一样吗?
connectionPool.callAcquirePooledConnection(address, call, null, false)
connectionPool.callAcquirePooledConnection(address, call, routes, false)
好像多了一个routes
字段?
这里涉及到HTTP/2的一个技术,叫做 HTTP/2 CONNECTION COALESCING
(连接合并),什么意思呢?
假设有两个域名,可以解析为相同的IP地址,并且是可以用相同的TLS证书(比如通配符证书),那么客户端可以重用相同的TCP连接
从这两个域名中获取资源。
再看回我们的连接池,这个routes
就是当前域名(主机名)可以被解析的ip地址
集合,这两个方法的区别也就是一个传了路由地址,一个没有传。
继续看callAcquirePooledConnection
代码:
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
if (address.url.host == this.route().address.url.host) {
return true
}
//HTTP/2 CONNECTION COALESCING
if (http2Connection == null) return false
if (routes == null || !routeMatchesAny(routes)) return false
if (address.hostnameVerifier !== OkHostnameVerifier) return false
return true
}
1)判断主机名、端口号等,如果请求完全相同就直接返回这个连接。
2)如果主机名不同,还可以判断是不是HTTP/2
请求,如果是就继续判断路由地址,证书,如果都能匹配上,那么这个连接也是可用的。
- 4、创建新连接
如果没有从连接池中获取到新连接,那么就创建一个新连接,这里就不多说了,其实就是调用到socket.connect
进行TCP连接。
- 5、再从连接池获取一次连接,防止在新建连接过程中有其他竞争连接被创建了
创建了新连接,为什么还要去连接池获取一次连接呢?
因为在这个过程中,有可能有其他的请求和你一起创建了新连接,所以我们需要再去取一次连接,如果有可以用的,就直接用它,防止资源浪费。
其实这里又涉及到HTTP2的一个知识点:多路复用
。
简单的说,就是不需要当前连接的上一个请求结束之后再去进行下一次请求,只要有连接就可以直接用。
HTTP/2引入二进制数据帧和流的概念,其中帧对数据进行顺序标识,这样在收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据,这就是流所做的事情。
所以在HTTP/2
中可以保证在同一个域名只建立一路连接,并且可以并发进行请求。
- 6、新连接放入连接池,并返回
最后一步好理解吧,走到这里说明就要用这个新连接了,那么就把它存到连接池,返回这个连接。
这个拦截器确实麻烦,大家好好梳理下吧,我也再来个图:
IO拦截器(CallServerInterceptor)
连接拿到了,编码解码器有了,剩下的就是发数据,读数据了,也就是跟I/O
相关的工作。
class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//写header数据
exchange.writeRequestHeaders(request)
//写body数据
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
requestBody.writeTo(bufferedRequestBody)
} else {
exchange.noRequestBody()
}
//结束请求
if (requestBody == null || !requestBody.isDuplex()) {
exchange.finishRequest()
}
//获取响应数据
var response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.build()
var code = response.code
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
return response
}
}
这个拦截器 倒是没干什么活,之前的拦截器兄弟们都把准备工作干完了,它就调用下exchange
类的各种方法,写入header,body
,拿到code,response
。
这活可干的真轻松啊。
被遗漏的自定义拦截器(networkInterceptors)
好了,最后补上这个拦截器networkInterceptors
,它也是一个自定义拦截器,位于CallServerInterceptor
之前,属于倒数第二个拦截器。
那为什么OkHttp
在有了一个自定义拦截器的前提下又提供了一个拦截器呢?
可以发现,这个拦截器的位置是比较深的位置,处在发送数据的前一刻,以及收到数据的第一刻。
这么敏感的位置,决定了通过这个拦截器可以看到更多的信息,比如:
请求之前
,OkHttp处理之后的请求报文数据,比如增加了各种header之后的数据。请求之后
,OkHttp处理之前的响应报文数据,比如解压缩之前的数据。
所以,这个拦截器就是用来网络调试
的,调试比较底层、更全面的数据。
总结
最后再回顾下每个拦截器的作用:
addInterceptor(Interceptor)
,这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。RetryAndFollowUpInterceptor
,这里会对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作。BridgeInterceptor
,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。CacheInterceptor
,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。ConnectInterceptor
,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec。networkInterceptors
,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。CallServerInterceptor
,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。
参考
https://www.jianshu.com/p/bfb...
https://segmentfault.com/a/11...
https://www.jianshu.com/p/02d...
https://kaiwu.lagou.com/cours...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。