【Android】OkHttp系列(二):重试/重定向拦截器RetryAndFollowUpInterceptor

该系列OkHttp源码分析基于OkHttp3.14.0版本

概述

用于对连接失败时重新连接以及对需要重定向的响应进行重定向。

源码分析

对于所有的拦截器而言,关键逻辑都在其intercept()方法中。

重试

@Override public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Transmitter transmitter = realChain.transmitter();

  int followUpCount = 0;//重定向次数
  Response priorResponse = null;
  while (true) {
    ...省略部分代码

    Response response;
    boolean success = false;
    try {
        //调用后续的拦截器
      response = realChain.proceed(request, transmitter, null);
      success = true;
    } catch (RouteException e) {
      // The attempt to connect via a route failed. The request will not have been sent.
      // 尝试通过路由连接失败。 该请求将不会被发送。
        //这里调用了recover()进行判断
      if (!recover(e.getLastConnectException(), transmitter, false, request)) {
        throw e.getFirstConnectException();
      }
      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, transmitter, requestSendStarted, request)) throw e;
      continue;//重试
    } finally {
      // The network call threw an exception. Release any resources.
      // 网络通话引发异常。 释放所有资源。
      if (!success) {
        transmitter.exchangeDoneDueToException();
      }
    }

   ...省略部分重定向的代码
  }
}

根据上面的代码我们可以看到,调用后续拦截器的代码chain.proced()是在一个while(true)循环中的。如果在发生了一些异常的情况下,将会继续该循环。

那么哪些情况下才会进行重试呢,主要关注的是下面的几个方法:

  1. recover

  2. isRecoverable

recover

/**
 * Report and attempt to recover from a failure to communicate with a server. Returns true if
 * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
 * be recovered if the body is buffered or if the failure occurred before the request has been
 * sent.
 * 报告并尝试从与服务器通信的故障中恢复。
 * 如果{@code e}是可恢复的,则返回true;如果失败是永久的,则返回false。
 * 仅在缓冲正文或在发送请求之前发生故障时,才能恢复带有正文的请求。
 * Dong:
 * 该方法用于指示是否继续重试
 */
private boolean recover(IOException e, Transmitter transmitter,
    boolean requestSendStarted, Request userRequest) {
  // The application layer has forbidden retries.
  // 1.client不允许重试
  if (!client.retryOnConnectionFailure()) return false;

  // We can't send the request body again.
  // 2.请求已经开始并且发生FileNotFindException,不允许重试
  // 3.请求已经开始并且请求体不为空且请求体只允许读写一次,不允许重试
  if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

  // This exception is fatal.
  // 4.ProtocolException 不允许重试
  // 5.InterruptedIOException但是不为SocketTimeoutException,不允许重试
  // 6.CertificateException 不允许重试
  // 7.SSLPeerUnverifiedException 不允许重试
  if (!isRecoverable(e, requestSendStarted)) return false;

  // No more routes to attempt.
  // 8.没有更多的路由尝试时,不允许重试
  if (!transmitter.canRetry()) return false;

  // For failure recovery, use the same route selector with a new connection.
  // 为了进行故障恢复,请使用具有新连接的相同路由选择器。
  return true;
}

isRecoverable

/**
 * 判断该异常是否允许重试
 * @param e 异常
 * @param requestSendStarted 请求是否已经开始
 * @return true/false
 */
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
  // If there was a protocol problem, don't recover.
  // 如果存在协议问题,请不要恢复。
  if (e instanceof ProtocolException) {
    return false;
  }

  // If there was an interruption don't recover, but if there was a timeout connecting to a route
  // we should try the next route (if there is one).
  // 如果发生中断,则无法恢复,但是如果连接到一条路由的超时,我们应该尝试下一条路由(如果有一条路由)。
  if (e instanceof InterruptedIOException) {
    return e instanceof SocketTimeoutException && !requestSendStarted;
  }

  // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
  // again with a different route.
  // 查找不太可能通过使用其他路由重试的已知客户端或协商错误。
  if (e instanceof SSLHandshakeException) {
    // If the problem was a CertificateException from the X509TrustManager,
    // do not retry.
    // 如果问题是来自X509TrustManager的CertificateException,不要重试。
    if (e.getCause() instanceof CertificateException) {
      return false;
    }
  }
  if (e instanceof 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.
  // 我们可能想使用其他路由重试的一个示例是连接到代理时出现问题,并表现为标准IOException。
  // 除非它是一个我们不应该重试的方法,否则我们返回true并尝试一条新路线。
  return true;
}

总结所有不允许进行重试的情况

1.配置OkHttpClient时不允许重试

2.请求已经开始并且发生FileNotFindException

3.请求已经开始并且请求体不为空且请求体只允许读写一次

4.发生ProtocolException异常

5.发生InterruptedIOException但是不为SocketTimeoutException

6.发生CertificateException异常

7.发生SSLPeerUnverifiedException异常

8.没有更多的路由(线路)

重定向

由于代码比较多,因此我删除了部分和重定向无关的代码。

@Override public Response intercept(Chain chain) throws IOException {
  ...省略部分代码

  int followUpCount = 0;//重定向次数
  Response priorResponse = null;
  while (true) {
    transmitter.prepareToConnect(request);

    if (transmitter.isCanceled()) {
      throw new IOException("Canceled");
    }

    Response response;
    boolean success = false;
    try {
      response = realChain.proceed(request, transmitter, null);
      success = true;
    }
      ...省略了部分重试的代码

    // Attach the prior response if it exists. Such responses never have a body.
    // 附加先前的响应(如果存在)。 这样的响应从来没有身体。
    if (priorResponse != null) {
      response = response.newBuilder()
          .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
          .build();
    }

    Exchange exchange = Internal.instance.exchange(response);
    Route route = exchange != null ? exchange.connection().route() : null;
    Request followUp = followUpRequest(response, route);//这里进行重定向相关的操作

    if (followUp == null) {
      //重定向结束
      if (exchange != null && exchange.isDuplex()) {
        transmitter.timeoutEarlyExit();
      }
      return response;
    }

    RequestBody followUpBody = followUp.body();
    if (followUpBody != null && followUpBody.isOneShot()) {
      return response;
    }

    closeQuietly(response.body());
    if (transmitter.hasExchange()) {
      exchange.detachWithViolence();
    }

    //最多允许重定向20次
    if (++followUpCount > MAX_FOLLOW_UPS) {
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    request = followUp;
    priorResponse = response;
  }
}

根据源码可以看到,重定向的操作是在一个死循环while(true)中的,而退出这个死循环即完成重定向的条件只有下面几个:

  1. followUp为null

  2. followUpBody不为null且followUpBody只允许传输一次

  3. 超过最大允许重定向的次数MAX_FOLLOW_UPS,20次

条件有了,那么我们就来具体分析一下,什么时候才能满足这些条件,由于超过最大重定向次数会抛出异常,因此按照正常业务逻辑,我们比较关注的是前两个条件。

followUp什么时候为null

从代码中可以看到,followUp是从followUpRequest()这个方法返回的,那么我们进入这个方法看看里面有什么。

根据源码可以看到,整体逻辑主要是根据之前的响应的状态码进行不同的逻辑。主要涉及到这几个状态码:

  1. 407(HTTP_PROXY_AUTH)
  2. 401(HTTP_UNAUTHORIZED)
  3. 308(HTTP_PERM_REDIRECT)
  4. 307(HTTP_TEMP_REDIRECT)
  5. 300(HTTP_MULT_CHOICE)
  6. 301(HTTP_MOVED_PERM)
  7. 302(HTTP_MOVED_TEMP)
  8. 303(HTTP_SEE_OTHER)
  9. 408(HTTP_CLIENT_TIMEOUT)
  10. 503(HTTP_UNAVAILABLE)

比较特殊的是407和408这两个状态码,407代表了需要进行代理服务器的认证,408代表了需要进行认证。这两个的认证完成都需要我们自己进行处理,否则的话将会返回null。因为根据我们前面提到的OkHttpClient.Builder的默认参数中可以看到,authenticatorproxyAuthenticator默认都是返回null的。

然后就是3xx系列的状态码了,熟悉http协议的可能知道,3xx系列的状态码就是和重定向相关的,不同的状态码表示不同的重定向要求以及时效。关于它们之间的区别,可以去看看这篇文章《HTTP中的301、302、303、307、308》

根据源码可以看到,当状态码为308\308时,如果请求Method不为”GET“也不为"HEAD",将直接返回null。

case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
  // "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"
  // 如果响应GET或HEAD以外的请求而接收到307或308状态代码,则用户代理务不必自动重定向该请求
  if (!method.equals("GET") && !method.equals("HEAD")) {
    return null;
  }

其他的3xx系列状态码,会先进行一些必要的参数校验。

// Does the client allow redirects?
// 判断当前客户端是否允许重定向 默认为true
if (!client.followRedirects()) return null;

String location = userResponse.header("Location");
if (location == null) return null;//响应头中没有声明重定向的地址
HttpUrl url = userResponse.request().url().resolve(location);//验证url是否合法

// Don't follow redirects to unsupported protocols.
// 不要遵循重定向到不支持的协议
if (url == null) return null;

// If configured, don't follow redirects between SSL and non-SSL.
// 如果已配置,请不要遵循SSL和非SSL之间的重定向。即http和https之间重定向
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());

/**如果重定向的协议和之前请求的协议不一致并且client配置时不允许https重定向,则返回null*/
if (!sameScheme && !client.followSslRedirects()) return null;

然后就是构建一个新的Request并返回了。

对于状态码408来说比较特殊,因为它表示请求超时了,超时一般来说其实不应该叫重定向了,而应该是重试才对。不太清楚为何OkHttp会将408放到重定向里面来。既然是重试,那么这里的检测逻辑和重试其实是有挺相似的,都是检测配置OkHttpClient时是否允许重试、RequestBody是否不为空且只允许传输一次。跟重试不同的是,这里会检查上一次响应的状态码是否也是408,如果是的话那么将不会再进行重试了。另外如果响应头中声明了"Retry-After",并且大于0的话,也不再进行重定向了,也返回null。

case HTTP_CLIENT_TIMEOUT://408
  // 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这样的某些服务器使用此响应代码。
  // 规范说,我们可以重复请求而无需进行修改。
  // 现代浏览器还会重复请求(甚至是非幂等的请求)。
  if (!client.retryOnConnectionFailure()) {
    // The application layer has directed us not to retry the request.
    // 应用层不允许进行重试
    return null;
  }

  RequestBody requestBody = userResponse.request().body();
  if (requestBody != null && requestBody.isOneShot()) {
    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) {
    //如果要求延迟一段时间,即响应头中的"Retry-After">0,返回null
    return null;
  }

  return userResponse.request();

最后是状态码503,这个状态码的逻辑就很简单了,除非服务端要求立即重试,即响应头中的"Retry-After"为0,否则的话结束重定向,返回null。

followUpBody什么时候不为null

根据前面的分析,我们可以知道,followUpBody不为null的情况只有在状态码为3xx系列的时候。

case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
  ...
  // fall-through
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
  ...
  // Most redirects don't include a request body.
  // 大多数重定向不包含请求正文。
  Request.Builder requestBuilder = userResponse.request().newBuilder();
  if (HttpMethod.permitsRequestBody(method)) {//排除"GET"和"HEAD"请求
    final boolean maintainBody = HttpMethod.redirectsWithBody(method);//是否是"PROPFIND"请求
    if (HttpMethod.redirectsToGet(method)) {
      //method不是"PROPFIND"
      requestBuilder.method("GET", null);
    } else {
      //method是"PROPFIND",这里设置了requestBody
      RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
      requestBuilder.method(method, requestBody);
    }

根据源码中可以看到,如果为3xx系列请求,并且请求Method为"PROPFIND"时,followUpBody是不为null的。

发布了24 篇原创文章 · 获赞 7 · 访问量 8685

猜你喜欢

转载自blog.csdn.net/d745282469/article/details/104296259