美文网首页
OkHttpClient的Interceptor们之RetryA

OkHttpClient的Interceptor们之RetryA

作者: 柳岸风语 | 来源:发表于2018-07-20 17:45 被阅读0次

    OkHttpClient内部使用的是责任链模式,它里面持有多个Interceptor,每个Interceptor都会实现一到两个功能。而OkHttpClient的功能的实现其实就是这些Interceptor功能的集合。所以要想了解OkHttp,那么就必须对这些Interceptor有足够的了解。OkHttp最重要的Interceptor有5种,分别是RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorCallServerInterceptor。它们分别完成的功能是

    RetryAndFollowUpInterceptor:负责失败重试和重定向
    BridgeInterceptor:负责将用户Request转换成一个实际的网络请求Request,再调用下层的拦截器获取Response,最后再将网络Response转换成用户的Reponse
    CacheInterceptor:负责控制缓存
    ConnectInterceptor:负责进行连接主机
    CallServerInterceptor:负责真正和服务器通信,完成http请求

    大体的功能都了解了,那现在就一个一个进行解剖吧!

    RetryAndFollowUpInterceptor

    RetryAndFollowUpInterceptor主要负责的是网络访问的失败重试和重定向。Interceptor的主要功能实现是在intercept方法里面,RetryAndFollowUpInterceptor也不例外。RetryAndFollowUpInterceptorintercept()里面运行了一个的无限循环。再循环里面首先执行网络访问获取访问结果,如果访问失败,则尝试重新请求。

    try {
            response = realChain.proceed(request, streamAllocation, null, null);
            releaseConnection = false;
          } catch (RouteException e) {
            // The attempt to connect via a route failed. The request will not have been sent.
            if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
              throw e.getLastConnectException();
            }
            releaseConnection = false;
            continue;
          } catch (IOException e) {
            // An attempt to communicate with a server failed. The request may have been sent.
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
            releaseConnection = false;
            continue;
          } 
    
    • 异常捕获

    1、RouteException :这个异常发生在Request请求还没有发出去前,就是打开Socket连接失败。这个异常是OkHttp自己定义的,是一个包裹类。包裹所有建立连接过程中的异常。

    2、IOException :这个异常发生在Request发出并且读取Response响应的过程中,TCP 已经连接,或者 TLS 已经成功握手后,连接资源准备完毕

    • 判断重试

    在捕捉到上面两种异常之后,OkHttp会使用recover()方法来判断是否需要重试。
    1、不需要重试:继续抛出异常,调用 StreamAllocation 的 streamFailed 和 release 方法释放资源,结束请求。OkHttp有个黑名单机制,可以记录连接失败的Route,从而在连接发起前将失败的Route延迟到最后使用。streamFailed这个方法可以把失败的Route记录下来,放入黑名单,下次在发起请求的时候,上次失败的Route会放到最后使用,提高了响应效率。

    2、可以重试:调用proceed()方法重新发起请求。那么可不可以无限的发起请求呢?答案是不可以的。因为内部有判断routeSelection.hasNext()或routeSelector.hasNext(),当所有的Route都试完了以后,就不在重新发起请求了。

    那么具体是在什么情况下不可以重试呢?

    private boolean recover(IOException e, StreamAllocation streamAllocation,
          boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);
    
        // The application layer has forbidden retries.
        if (!client.retryOnConnectionFailure()) return false;
    
        // We can't send the request body again.
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    
        // This exception is fatal.
        if (!isRecoverable(e, requestSendStarted)) return false;
    
        // No more routes to attempt.
        if (!streamAllocation.hasMoreRoutes()) return false;
    
        // For failure recovery, use the same route selector with a new connection.
        return true;
      }
    
    1. 如果当前用户设置了连接失败不可重试。这个情况下是不会发起重试的。
    new OkHttpClient.Builder()
    .retryOnConnectionFailure(false)
    
    1. 如果当前Request已经发起请求,并且请求体是不可重复请求体,那么也是不可以重新发起请求的。这里针对的就是IOException异常了。
    2. 利用isRecoverable()排除不可以恢复的异常。不可恢复的异常有以下几种:
      • ProtocolException 协议异常
      • InterruptedIOException
        • 如果是SocketTimeoutException,即创建连接超时,此时·Request·未发出请求,就需要重试一下,试试其他的Route
        • 如果此时已经连接成功,即此时·Request·发出请求,读取响应超时,此时就不应该重试了
      • CertificateException引起的SSLHandshakeException,证书错误
      • SSLPeerUnverifiedException 访问网站的证书不在你可以信任的证书列表中
    3. 查看是否还有其他的路由可以尝试。满足下面三个条件就可以直接结束请求了:
      • route为空
      • 当前代理没有下一个可用IP
      • 没有下一个代理了和没有延迟使用的route(之前失败过的route,会在列表中延迟使用)
    public boolean hasMoreRoutes() {
        return route != nul || (routeSelection != null && routeSelection.hasNext() || routeSelector.hasNext();
    }
    

    RouteSelector封装了OkHttp选择路由进行连接的策略。而OkHttp重试的过程其实就是不断尝试请求的多个代理和IP的过程,不是只是就同一个代理和IP不断的尝试。当把所有的代理和IP都试完了,都不行,就认为当前请求失败了。
    当然,换路由尝试还有一个前提,那就是当前的路由必须为空,如果不为空的,那重试就不可避免的进入了无限循环了。因此把当前路由置为空是很重要的。那么什么时候route为空呢?在recover()的第一行,streamAllocation.streamFailed(e)

    public void streamFailed(IOException e) {
      ...
        synchronized (connectionPool) {
          if (e instanceof StreamResetException) {
            StreamResetException streamResetException = (StreamResetException) e;
            if (streamResetException.errorCode == ErrorCode.REFUSED_STREAM) {
              refusedStreamCount++;
            }
            if (streamResetException.errorCode != ErrorCode.REFUSED_STREAM || refusedStreamCount > 1) {
              noNewStreams = true;
              route = null;
            }
          } else if (connection != null && (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
            noNewStreams = true;
            if (connection.successCount == 0) {
              if (route != null && e != null) {
                routeSelector.connectFailed(route, e);
              }
              route = null;
            }
          }
          releasedConnection = connection;
          socket = deallocate(noNewStreams, false, true);
          if (connection != null || !reportedAcquired) releasedConnection = null;
        }
    ...
      }
    

    所以route为空有两种情况:

    • Http/2 在不破坏socket的情况下取消流(此时抛出StreamResetException),第一次出现ErrorCode.REFUSED_STREAM不会为空,其他都为空。
    • 连接不是HTTP/2 connection或者此时连接被中断(异常是ConnectionShutdownException),此时如果该条路由一次都没有成功过。

    在这两种情况下。route都会为空。情况下重试都会继续使用当前管道。失败重试到这里差不多就分析完了。

    如果连接成功了,并且得到了请求响应,接下来我们就需要根据响应码来判断是否需要授权认证或者重定向或者请求超时。这一部分是通过Request followUp = followUpRequest(response, streamAllocation.route())完成的。

    • 未授权
      • HTTP_PROXY_AUTH 407
        代理未授权,在请求时在请求头添加“Proxy-Authorization”;
      • HTTP_UNAUTHORIZED 401
        请求未授权,在请求时在请求头添加“Authorization”;
    • 重定向
      • 307 308
        不是GET或HEAD请求的话,不进行重定向
      • 300 301 302 303
        请求code是这些的话,首先,如果客户端设置不重定向的,那就不会重定向;如果响应头不含重定向地址,也不重定向;Http和HTTPS之间也不重定向。如果需要重定向的话,对于请求体需要有一些特殊的处理,对于GET方法,是没有请求体的,此时就需要一出一些请求头。具体的看源码。
    • 请求超时或服务不可用
      • HTTP_CLIENT_TIMEOUT 408
        请求超时。客户端设置连接失败不重试的话,不重试;请求体是不可重复请求体,不重试;上次请求超时导致的本次请求,不重试;是否配置了客户端再次尝试时间,如果有,不重试。
      • HTTP_UNAVAILABLE 503
        服务不可用。上次请求超时导致的本次请求,不重试;是否配置了客户端再次尝试时间,如果没有且时间为0,重试;否则就不重试。

    下面贴出followUpRequest源码

    private Request followUpRequest(Response userResponse, Route route) throws IOException {
        if (userResponse == null) throw new IllegalStateException();
        int responseCode = userResponse.code();
    
        final String method = userResponse.request().method();
        switch (responseCode) {
          case HTTP_PROXY_AUTH:
            Proxy selectedProxy = route != null
                ? route.proxy()
                : client.proxy();
            if (selectedProxy.type() != Proxy.Type.HTTP) {
              throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
            }
            return client.proxyAuthenticator().authenticate(route, userResponse);
    
          case HTTP_UNAUTHORIZED:
            return client.authenticator().authenticate(route, userResponse);
    
          case HTTP_PERM_REDIRECT:
          case HTTP_TEMP_REDIRECT:
            // "If the 307 or 308 status code is received in response to a request other than GET
            // or HEAD, the user agent MUST NOT automatically redirect the request"
            if (!method.equals("GET") && !method.equals("HEAD")) {
              return null;
            }
            // fall-through
          case HTTP_MULT_CHOICE:
          case HTTP_MOVED_PERM:
          case HTTP_MOVED_TEMP:
          case HTTP_SEE_OTHER:
            // Does the client allow redirects?
            if (!client.followRedirects()) return null;
    
            String location = userResponse.header("Location");
            if (location == null) return null;
            HttpUrl url = userResponse.request().url().resolve(location);
    
            // Don't follow redirects to unsupported protocols.
            if (url == null) return null;
    
            // If configured, don't follow redirects between SSL and non-SSL.
            boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
            if (!sameScheme && !client.followSslRedirects()) return null;
    
            // Most redirects don't include a request body.
            Request.Builder requestBuilder = userResponse.request().newBuilder();
            if (HttpMethod.permitsRequestBody(method)) {
              final boolean maintainBody = HttpMethod.redirectsWithBody(method);
              if (HttpMethod.redirectsToGet(method)) {
                requestBuilder.method("GET", null);
              } else {
                RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
                requestBuilder.method(method, requestBody);
              }
              if (!maintainBody) {
                requestBuilder.removeHeader("Transfer-Encoding");
                requestBuilder.removeHeader("Content-Length");
                requestBuilder.removeHeader("Content-Type");
              }
            }
    
            // When redirecting across hosts, drop all authentication headers. This
            // is potentially annoying to the application layer since they have no
            // way to retain them.
            if (!sameConnection(userResponse, url)) {
              requestBuilder.removeHeader("Authorization");
            }
    
            return requestBuilder.url(url).build();
    
          case 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.)
            if (!client.retryOnConnectionFailure()) {
              // The application layer has directed us not to retry the request.
              return null;
            }
    
            if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
              return null;
            }
    
            if (userResponse.priorResponse() != null
                && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
              // We attempted to retry and got another timeout. Give up.
              return null;
            }
    
            if (retryAfter(userResponse, 0) > 0) {
              return null;
            }
    
            return userResponse.request();
    
          case HTTP_UNAVAILABLE:
            if (userResponse.priorResponse() != null
                && userResponse.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;
    
          default:
            return null;
        }
      }
    

    相关文章

      网友评论

          本文标题:OkHttpClient的Interceptor们之RetryA

          本文链接:https://www.haomeiwen.com/subject/kstopftx.html