OkHttp原理第三篇—RetryAndFollowUpInterceptor

OkHttp原理第三篇—RetryAndFollowUpInterceptor_第1张图片

作者简介:00后,22年刚刚毕业,一枚在鹅厂搬砖的程序员。

前置任务:在阅读本篇文章之前希望读者已经阅读了上篇文章OkHttp原理第二篇—OkHttp的责任链模式,本篇文章详细对RetryAndFollowUpInterceptor进行解析,也希望读者在阅读的时候自己边研究源码边思考本篇文章,硬看是很难看懂的若有错误也希望大家指正。

学习目标:弄懂RetryAndFollowUpInterceptor如何处理重试和重定向。

创作初衷:学习OkHttp的原理,阅读Kotlin框架源码,提高自己对Kotlin代码的阅读能力。为了读代码而读代码,笔者知道这是不对的,但作为应届生,提高阅读源码的能力笔者认为还是很重要的。


文章目录

  • OkHttp原理第三篇—RetryAndFollowUpInterceptor
    • 重试
    • 重定向
    • 代理
    • 服务端鉴权
    • HTTP协议相关知识总结
    • 总结

OkHttp原理第三篇—RetryAndFollowUpInterceptor

主要处理请求的重试和重定向

下面进行验证:

RetryAndFollowUpInterceptor#intercept

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    var request = chain.request
    val call = realChain.call
    var followUpCount = 0
    var priorResponse: Response? = null // 保存上次重定向的响应
    var newExchangeFinder = true
    var recoveredFailures = listOf<IOException>() // 此次请求失败的异常集合
    while (true) {
        // 创建ExchangeFinder,此类用于创建和寻找Exchange,Exchange主要处理此次请求中的IO的连接管理和事件请,会在后续的拦截器中进行分析
        call.enterNetworkInterceptorExchange(request, newExchangeFinder)

        var response: Response
        var closeActiveExchange = true
        try {
            // 若call取消则扔出IO异常
            if (call.isCanceled()) {
                throw IOException("Canceled")
            }
            try {
                // 将Request交给给下个节点,此处的下个节点为BridgeInterceptor,若下层节点处理过程中扔出错误会被下面的catch捕获
                response = realChain.proceed(request)
                newExchangeFinder = true
            } catch (e: RouteException) {
                // 重点方法,决定能否重试的关键方法,此方法返回为true才可重试,看重试小节中的分析
                // The attempt to connect via a route failed. The request will not have been sent.
                // 检测路由异常是否能重新连接
                if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
                    throw e.firstConnectException.withSuppressed(recoveredFailures)
                } else {
                    recoveredFailures += e.firstConnectException
                }
                newExchangeFinder = false
                continue
            } catch (e: IOException) {
                // 只有HTTP2才会抛出ConnectionShutdownException,此篇文章不对HTTP2的实现进行分析,在IO异常进入下述方法第四个参数为true
                // An attempt to communicate with a server failed. The request may have been sent.
                // 检测该IO异常是否能重新连接
                if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
                    throw e.withSuppressed(recoveredFailures)
                } else {
                    recoveredFailures += e
                }
                newExchangeFinder = false
                continue
            }
			// 走到这里,则一定拿到了服务器的响应
			// 若循环中的代码完整执行过,则priorResponse不为空,也就意味着需要重定向(不太严谨的说法),类似链表的结构保存下来整个响应路径
            if (priorResponse != null) {
                response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                               .body(null) //清除body
                               .build())
                .build()
            }
			// exchange为Exchange类,主要处理此次网络连接的IO操作
            val exchange = call.interceptorScopedExchange
            // 处理重定向的重点方法,此方法会根据返回的response和连接管理器判断是否需要重定向,看下重定向小节
            val followUp = followUpRequest(response, exchange)
			// 若为null,则意味着此次请求结束了
            if (followUp == null) {
                // 此if与websocket相关,此处不分析
                if (exchange != null && exchange.isDuplex) {
                    call.timeoutEarlyExit()
                }
                closeActiveExchange = false
                return response
            }
			// 拿到新请求的请求体
            val followUpBody = followUp.body
            // 请求体只可能是原始请求的请求体,若原始请求的请求体不允许重试则直接返回
            if (followUpBody != null && followUpBody.isOneShot()) {
                closeActiveExchange = false
                return response
            }
			// 需要继续请求,关闭此次响应的body
            response.body?.closeQuietly()
			// 若重定向次数超过20,则抛出协议一场
            if (++followUpCount > MAX_FOLLOW_UPS) {
                throw ProtocolException("Too many follow-up requests: $followUpCount")
            }
			// 继续下一次请求
            request = followUp
            priorResponse = response
        } finally {
            // webSocket相关,不予分析
            call.exitNetworkInterceptorExchange(closeActiveExchange)
        }
    }
}

重试

1.RetryAndFollowUpInterceptor#recover

此方法返回为false则不可重试

//This interceptor recovers from failures and follows redirects as necessary. It may throw an IOException if the call was canceled.
private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
): Boolean {
    // 在配置OkhttpClient时设置了不允许重试(默认允许),则一旦发生请求失败就不再重试,在构建Client时可以指定。
    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure) return false

    // 如果是RouteException或者HTTP2且异常的具体类型为ConnectionShutdownException的情况下,此if不会命中,因requestSendStarted为false
	// 若请求时使用了请求体,且请求体继承自RequestBody并重写isOneShot()返回为true,则不允许重试,看下2.RetryAndFollowUpInterceptor#requestIsOneShot分析
    // We can't send the request body again.
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
	
    // 下述方法若返回false则也不允许重试,看下3分析
    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false
	
    // 没有路线可以尝试了,也不允许重试
    if (!call.retryAfterFailure()) return false

    // For failure recovery, use the same route selector with a new connection.
    return true
}

2.RetryAndFollowUpInterceptor#requestIsOneShot

private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
  val requestBody = userRequest.body
  return (requestBody != null && requestBody.isOneShot()) ||  //isOneShot()是需要程序员重写RequestBody的方法,不重写的情况下默认返回为false
      e is FileNotFoundException  //若下层节点处理过程中抛出FileNotFoundException也不允许重试
}

3.RetryAndFollowUpInterceptor#isRecoverable

private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
    // 协议异常则不许重试,举一个协议异常的例子,没有使用代理却返回响应码407需要代理验证,此类与协议规范冲突的错误大多数为ProtocolException
    if (e is ProtocolException) {
        return false
    }
	// 若产生中断异常,Socket超时或者在非IO异常(路由异常)时可能可以重试(requestSendStarted是catch中捕获的最初的异常决定的)
    // 若捕获异常为RouteException,requestSendStarted为false
    // 若捕获异常为IOException,且异常的具体类型为ConnectionShutdownException时,requestSendStarted也为false
    if (e is InterruptedIOException) {
        return e is SocketTimeoutException && !requestSendStarted
    }
	
    //ssl握手异常,且具体错误属于是证书异常,则不允许重试,因为重试也肯定失败
    if (e is SSLHandshakeException) {
        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.
    // 翻译:如果产生连接代理出现问题,且是抛出IO异常,此时要返回true,尝试新的路由路线
    return true
}

总结上述中不可重试的情况,分为两种情况一种是程序员不希望重试一种是下层抛出异常

程序员控制

  • 配置OkhttpClient时设置retryOnConnectionFailurefalse,所有请求将不可重试。
  • 使用请求体请求时,Body继承自RequestBody并重写isOneShot()返回为true,可以做到过滤某些请求。

下层节点抛出异常

  • ProtocolException协议异常不允许重试,协议异常在近期不可修复,即使重试也无济于事。
  • SSLHandshakeException握手异常且异常的具体原因是CertificateException证书异常时不允许重试,原因于上一条一样,近期无法修复。
  • SocketTimeoutException时若捕获的异常最初的为IO异常,且此异常的真实类型不为ConnectionShutdownException,也不允许重试,因此大多数IO异常都不允许重试。
  • SSLPeerUnverifiedException异常证书校验失败也不允许重试。
  • 没有更多的路线重试。

这里提出一个问题,重试不会死循环吗?

不会,首先重试的条件是苛刻的,绝大多数重试情况都只是网络波动,且在判断是否可重试时,有非常关键的一个条件,有没有可以重试的路线,一直重试必然会导致路线全部使用完,此时也一定会跳出循环。

回到上述RetryAndFollowUpInterceptor#intercept()方法,继续往下分析响应成功的情况。

重定向

重定向简单理解则是客户端请求服务端时,服务端让客户端请求别的网站,服务端会在响应中添加新的请求地址,客户端去请求这个新的地址。

在学习客户端如何处理重定向的之前,先学习HTTP中协议的一些知识。

在响应码为308,307,300,301,302,303时需要重定向,响应码作用如下:

  • 300 多项选择 表示该请求拥有多种可能的响应。用户代理或者用户自身应该从中选择一个。
  • 301 永久重定向 被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的URI之一,和直接请求一个新的网站区别不大。
  • 302 状态码表示目标资源临时移动到了另一个 URI 上。由于重定向是临时发生的,所以客户端在之后的请求中还应该使用原本的 URI。
  • 303302作用类似,301302在最初的协议规范中不允许请求方法的改变,比如get请求,重定向后应仍为get,但是由于历史原因,客户端在实现时会改变方法,因此拓展出303响应码,代表302可改变方法。
  • 307 的定义实际上和 302 是一致的,唯一的区别在于,307 状态码不会改变请求方法。
  • 308 的定义实际上和 301 是一致的,唯一的区别在于,308 状态码不会改变请求方法。

由于协议的一些规定,需要重建Request,下述方法则根据协议规则创建新的Request

1.RetryAndFollowUpInterceptor#followUpRequest

//Figures out the HTTP request to make in response to receiving userResponse. This will either add authentication headers, follow redirects or handle a client request timeout. If a follow-up is either unnecessary or not applicable, this returns null.
//翻译如下:计算出响应接收 userResponse 的 HTTP 请求。这将添加身份验证标头、遵循重定向或处理客户端请求超时。如果后续行动不必要或不适用,则返回 null,个人理解:说白了就是根据上次相应的响应码,判断是不是需要发起新的请求。
@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
	// 拿到此次请求的Route,其内部有此次请求的代理,地址信息
    val route = exchange?.connection?.route()
    // 原始请求的响应码
    val responseCode = userResponse.code
	// 原始请求的方法get,post ...
    val method = userResponse.request.method
    when (responseCode) {
        // 如果响应码为407,代理需要验证客户端身份
        HTTP_PROXY_AUTH -> {
            // 获取此次响应的代理
            val selectedProxy = route!!.proxy
            // 没有设置代理但是却返回407,需要抛出协议异常
            if (selectedProxy.type() != Proxy.Type.HTTP) {
                throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
            }
            // 回调下面方法获取到新的Request,此Request则一定包括了验证代理服务器的账号密码信息,看代理小节
            return client.proxyAuthenticator.authenticate(route, userResponse)
        }
		// 响应码401,服务器本身需要验证客户端身份,看下面服务器鉴权小节
        HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
		// 响应码308,307,300,301,302,303需要直接重定向
        HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
            // 根据响应获取新的Request,本节下2.RetryAndFollowUpInterceptor#buildRedirectRequest分析
            return buildRedirectRequest(userResponse, method)
        }
		// 响应码408,请求超时,服务器觉得客户端太慢了,此处应该归类于重试环节而不是重定向,此处的重试是收到了服务器的响应408,在重试小节中的重试是没有收到服务器的响应
        HTTP_CLIENT_TIMEOUT -> {
            // 408's are rare in practice, but some servers like HAProxy use this response code. The
            // spec says that we may repeat the request without modifications. Modern browsers also
            // repeat the request (even non-idempotent ones.)
            //上述翻译:408 在实践中很少见,但一些服务器(如 HAProxy)使用此响应代码。规范说我们可以不加修改地重复请求。现代浏览器也会重复请求(甚至是非幂等的)。
            // 在重试小节对此标志位进行过分析,表示是否可重试,false则直接返回null,返回null意味着结束此次请求,返回Response,最终业务层拿到的是响应码为408的Response
            if (!client.retryOnConnectionFailure) {
                // The application layer has directed us not to retry the request.
                return null
            }
			
            val requestBody = userResponse.request.body
            // 在重试小节也分析过,若请求体实现isOneShot()方法返回false,表示此请求不可重试
            if (requestBody != null && requestBody.isOneShot()) {
                return null
            }
           	
            val priorResponse = userResponse.priorResponse // 之前的重试和重定向记录
            // 若之前重试过且响应码为408,则不允许重试,
            // 场景举例:客户端发起请求且响应为408
            // 第一次请求时返回408,priorResponse为null,重试此次请求,执行下次循环会把408的这次响应赋值给priorResponse,再次走到这时priorResponse不为null,此时则会返回null,结束此次请求,因此上述只说priorResponse和重定向相关是不准确的
            // 之所以这么做笔者理解应该是priorResponse本意是保存重定向路径,若命中408则意味着路径中会保存重试的408响应导致路径不准确,不如直接结束此次请求,直接返回
            if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
                // We attempted to retry and got another timeout. Give up.
                return null
            }
			// 判断响应头中的Retry-After字段,若还在限制时间内则不允许重试
            if (retryAfter(userResponse, 0) > 0) {
                return null
            }
			// 原样返回请求,对此次请求进行重试
            return userResponse.request
        }
		// 503 服务器错误
        HTTP_UNAVAILABLE -> {
            val priorResponse = userResponse.priorResponse
            // 若上次返回码也为503,则结束此次请求
            if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
                // We attempted to retry and got another timeout. Give up.
                return null
            }
			// 若到达限制时间,则可以重试
            if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
                // specifically received an instruction to retry without delay
                return userResponse.request
            }
			// 其他情况结束此次请求
            return null
        }
		//421请求码,只会在HTTP2中发生,此处不做讨论
        HTTP_MISDIRECTED_REQUEST -> {
            // OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
            // RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
            // we can retry on a different connection.
            // 翻译:即使域名不同,OkHttp 也可以合并 HTTP2 连接。请参阅 RealConnection.isEligible()。如果我们尝试了这个并且服务器返回了 HTTP 421,那么我们可以在不同的连接上重试。
            val requestBody = userResponse.request.body
            if (requestBody != null && requestBody.isOneShot()) {
                return null
            }

            if (exchange == null || !exchange.isCoalescedConnection) {
                return null
            }

            exchange.connection.noCoalescedConnections()
            return userResponse.request
        }

        else -> return null
    }
}

2.RetryAndFollowUpInterceptor#buildRedirectRequest

private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
    // 在创建client时是否设置可以重定向,若不允许重定向则返回null
    if (!client.followRedirects) return null
    // 获取上次返回头中的Location字段,此字段记录重定向的地址
    val location = userResponse.header("Location") ?: return null
    // 根据Location字段解析新的URL地址
    val url = userResponse.request.url.resolve(location) ?: return null

    // scheme为http or https,此处记录重定向的协议是否发生改变
    val sameScheme = url.scheme == userResponse.request.url.scheme
    // 若协议发生改变且client.followSslRedirects为false,则不允许重定向,此标志位默认为true在创建client时可以设置
    if (!sameScheme && !client.followSslRedirects) return null

    // 将上次的Request进行copy
    val requestBuilder = userResponse.request.newBuilder()
    // 若上次请求方法不为get和head则Request需要进一步封装处理
    if (HttpMethod.permitsRequestBody(method)) {
        val responseCode = userResponse.code
        // 此标志位在在请求方法为PropFind,或响应码为307,308时为true
        val maintainBody = HttpMethod.redirectsWithBody(method) ||
        responseCode == HTTP_PERM_REDIRECT ||
        responseCode == HTTP_TEMP_REDIRECT
        // 原先的请求方法不为PROPFIND,且响应码非307,308,重定向后一律改为get
        if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
            requestBuilder.method("GET", null)
        } else {
            // 若maintainBody为true 则沿用上次请求的请求体
            val requestBody = if (maintainBody) userResponse.request.body else null
            // 设置新的request的方法和请求体
            requestBuilder.method(method, requestBody)
        }
        // 若不维持请求体则将下述三个字段移除
        if (!maintainBody) { 
            requestBuilder.removeHeader("Transfer-Encoding") //请求体传输编码
            requestBuilder.removeHeader("Content-Length")    //请求体长度
            requestBuilder.removeHeader("Content-Type")		 //请求体传输类型
        }	
    }
    // 跨主机重定向时去除身份验证信息
    if (!userResponse.request.url.canReuseConnectionFor(url)) {
        requestBuilder.removeHeader("Authorization")
    }

    return requestBuilder.url(url).build()
}

Request的构建是比较复杂的,整体流程如下:

  1. 是否可以改变协议进行重定向,由clientfollowSslRedirects决定,此标志位在创建Client时可以设置,若此标志位为false则不允许改变协议进行重定向。
  2. 若请求方法为get或者head则不需要继续处理。
  3. 若方法不为get或者head则需要进一步处理,请求方法为PropFind,或响应码为307308时会保留请求体,若请求方法为PROPFIND,且响应码非307308,重定向后请求方法一律改为getget请求不需要请求体则去掉相关的头信息Transfer-EncodingContent-LengthContent-Type
  4. 最后一步如果跨主机重定向去除鉴权信息,Authorization

是否需要改变请求方法为get,请求体是否有效,(√,×)表示改变请求方法,请求体无效,表格如下:

请求方法\响应码 300,301,302,303 307,308
get,head (×,×) (×,×)
非get,非head,非propfind (√,×) (×,√)
propfind (×,√) (×,√)

gethead请求时不需要使用请求体则请求体一定是无效的。

代理

代理分为两种:

  • 正向代理:保护客户端,局域网中所有客户端的出口,使得外界不能找到具体的客户端,VPN算正向代理的一种。
OkHttp原理第三篇—RetryAndFollowUpInterceptor_第2张图片
  • 反向代理:保护服务端,所有的请求都交给此代理服务器,由代理服务器再次分发,主要用于负载均衡,降低服务器的通信量,还有一种用途则是防火墙,过滤请求。

    OkHttp原理第三篇—RetryAndFollowUpInterceptor_第3张图片

客户端设置的代理为正向代理。

OkHttp设置代理的方式如下:

val okHttpClient = OkHttpClient().newBuilder()
    .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("192.168.0.124", 8853))) //设置代理
    .proxyAuthenticator { route, response ->
        val info = Credentials.basic("zjm", "ljn")
        response.request.newBuilder()
                .header("Proxy-Authorization", info)
                .build()
     }
    .build()

回到重定向小节继续分析其他情况。

服务端鉴权

响应码401需要服务端直接鉴权,和407的区别是407是代理服务端需要鉴权。

OkHttp设置服务端鉴权的方式如下:

val okHttpClient = OkHttpClient().newBuilder()
    .authenticator { route, response ->
        val info = Credentials.basic("zjm", "ljn")
        response.request.newBuilder()
            .header("Authorization", info)
            .build()
    }
    .build()

和响应码407的区别是请求头的字段名不同

回到重定向小节继续分析其他情况。

HTTP协议相关知识总结

每个拦截器都是HTTP协议的部分实现,内部有大量的协议内容,此小节对其进行总结

响应码

RetryAndFollowUpInterceptor共涉及407,401,308,307,300,301,302,303,408,503,421共11个响应码。

308,307,300,301,302,303在重定向小节中已经叙述,此小节不再叙述。

  • 407代理服务器需要验证客户端身份
  • 401服务器需要验证客户端身份
  • 408请求超时,服务器认为客户端太慢了
  • 503服务器不存在访问的资源
  • 421 ``HTTP2情况下才会返回,一般是当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围

响应头字段

  • Location 重定向服务端返回的新的地址
  • Retry-After 标志此资源在多长时间内不可被重试

请求头字段

  • Proxy-Authorization 代理服务器鉴权,包括客户端的身份信息
  • Authorization 服务器鉴权,包括客户端的身份信息
  • Transfer-Encoding希望的传输方式,比如chunked,整个报文就会分块传输,与Content-Length冲突
  • Content-Length请求的内容长度
  • Content-Type请求的与实体对应的MIME信息

请求方法

  • GET 大家都知道就不过多赘述了
  • POST 大家都知道就不过多赘述了
  • HEAD GET类似,不同时HEAD只获取头信息,不需要响应正文
  • PROPFIND WebDavHTTP1.1的拓展)中的方法,用于获取某个属性的值

一篇关于HTTP协议版本的区别的好文,点击这里

总结

RetryAndFollowUpInterceptor主要处理重试和重定向,在上述代码分析中已经得到验证。

重试OkHttp对网络请求的优化,重试的条件是苛刻的,大多数情况原因是网络波动。

重定向HTTP协议规范的内容,协议细节比较多,重点为30x的响应码处理,以及OkHttp实现重定向的思路。

原创不易,还希望各位大佬支持一下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下

点赞,你的认可是我创作的动力! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!

⭐️ 收藏,你的青睐是我努力的方向! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!

✏️ 评论,你的意见是我进步的财富! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!

下篇预告:分析第二个拦截器-BridgeInterceptor ,分析其如何处理请求头

下篇文章已更新OkHttp原理第四篇-BridgeInterceptor

你可能感兴趣的:(计算机网络,android,http,kotlin)