okhttp最多创建多少个连接(从网络请求过程看OkHttp拦截器)

作者: jimuzz

来源:https://www.cnblogs.com/jimuzz/p/14557234.html

前言

之前我们结合设计模式简单说了下 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) }

这些拦截器会形成一条链,组织了请求接口的所有工作。

okhttp最多创建多少个连接(从网络请求过程看OkHttp拦截器)(1)

以上为上节内容,不了解的朋友可以返回上一篇文章看看。

假如我来设计拦截器

先抛开拦截器的这些概念不谈,我们回顾下 网络通信过程 ,看看实现一个网络框架至少要有哪些功能。

请求过程 响应过程

而之前说过拦截器的基本代码格式是这样:

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 max-age=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、自定义拦截器统一处理响应数据。

有点绕,来张图瞧一瞧:

okhttp最多创建多少个连接(从网络请求过程看OkHttp拦截器)(2)

所以,拦截器的顺序也基本固定了:

  • 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)

为了方便理解,我对源码进行了修剪:scissors::

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循环。

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 Exchange

明白了,这个连接拦截器(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、新连接放入连接池,并返回

最后一步好理解吧,走到这里说明就要用这个新连接了,那么就把它存到连接池,返回这个连接。

这个拦截器确实麻烦,大家好好梳理下吧,我也再来个图:

okhttp最多创建多少个连接(从网络请求过程看OkHttp拦截器)(3)

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 在有了一个自定义拦截器的前提下又提供了一个拦截器呢?

可以发现,这个拦截器的位置是比较深的位置,处在发送数据的前一刻,以及收到数据的第一刻。

这么敏感的位置,决定了通过这个拦截器可以看到更多的信息,比如:

请求之前 请求之后

所以,这个拦截器就是用来 网络调试 的,调试比较底层、更全面的数据。

总结

最后再回顾下每个拦截器的作用:

addInterceptor(Interceptor) RetryAndFollowUpInterceptor BridgeInterceptor CacheInterceptor ConnectInterceptor networkInterceptors CallServerInterceptor

参考

https://www.jianshu.com/p/bfb13eb3a425

https://segmentfault.com/a/1190000020386580

https://www.jianshu.com/p/02db8b55aae9

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc

作者: jimuzz

来源:https://www.cnblogs.com/jimuzz/p/14557234.html

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页