美文网首页Android技术知识Android开发
OkHttp源码之RetryAndFollowUpInterce

OkHttp源码之RetryAndFollowUpInterce

作者: 低情商的大仙 | 来源:发表于2018-10-20 19:09 被阅读5次

之前在上一篇文章okhttp源码之责任链模式中有提到过,okhttp的所有功能都是通过拦截器来实现的,就是我们今天要分析的是第一个核心功能的拦截器:RetryAndFollowUpInterceptor,该拦截器主要功能是负责处理各种重新请求。

首先明确要弄明白什么

所谓的重新请求包括:1.重定向,就是请求一个地址时,服务端返回特殊状态码和新的请求地址,客户端再重新去请求这个新的地址; 2. 一些特殊情况需要重新请求 。那么想要完成这个重新请求,我们必须搞明白以下几点:

  • 重新请求有没有次数限制
  • 哪些情况需要重新请求
  • 是否每次都是新地址,若是,地址从哪里获取
  • 重新请求和第一次请求有区别吗

整体结构

首先我们整体看下intercept方法:

@Override public Response intercept(Chain chain) throws IOException {
     //省略一些代码
    while (true) {
        //获取请求结果
        response = realChain.proceed(request, streamAllocation, null, null)
     //省略其他代码
      Request followUp;
      try {
        //检查这一次请求是否需要重定向到其他地址
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }
      //本次请求无需重定向,直接说明此次请求获得了真正结果
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }
      closeQuietly(response.body());
      //本次请求依然需要重定向到新的请求,检查是否达到最大重定向次数
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //省略部分代码
      request = followUp;
      priorResponse = response;
    }
  }

从上面的代码注释中可以看到,整个流程其实很简单,一个死循环在那里执行,每次请求得到结果后,利用followUpRequest()方法看会不会返回一个新的Request,若返回需要重定向,也就是拿新的Request去获取Response,当然这里有一个最大重定向次数限制:

/**
   * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
   * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
   */
  private static final int MAX_FOLLOW_UPS = 20;

这里我们解答了一个问题,那就是重定向次数限制,那么,其他问题答案很明显只能在followUpRequest()方法中寻找:

private Request followUpRequest(Response userResponse, Route route) throws IOException {
  //省略代码
    switch (responseCode) {
      case HTTP_PROXY_AUTH:
       //省略代码,处理代理情况
      case HTTP_UNAUTHORIZED:
      //省略代码,处理授权情况
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
      //省略代码,做一些检查
        // fall-through
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        //此处就是构建新的重定向请求的部分
      case HTTP_CLIENT_TIMEOUT:
       //省略代码
      case HTTP_UNAVAILABLE:
      //省略代码
      default:
        return null;
    }
  }

此处我省略了大部分代码,只呈现出结构,这里其实就是匹配返回的状态码,如果返回的是某些特殊的状态码,我们就重新构建一个请求,我将这些状态码分成4种,这四种情况都会构建一个Request,然后重新请求,不单单重定向这一种情况。

哪些要重新请求

  1. HTTP_PROXY_AUTH(407)或 HTTP_UNAUTHORIZE(401)
    这两个状态码差不多,只不过一个是代理服务器要验证信息,一个是真正服务器要验证信息,我们以407为例:
 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);

首先要检查我们有没有设置代理信息,如果没有直接抛异常,client中的proxyAuthenticator()其实获取的是默认设置的一个proxyAuthenticator,在builder中默认的:

proxyAuthenticator = Authenticator.NONE;

这个NONE长这样:

public interface Authenticator {
  /** An authenticator that knows no credentials and makes no attempt to authenticate. */
  Authenticator NONE = new Authenticator() {
    @Override public Request authenticate(Route route, Response response) {
      return null;
    }
  };

  /**
   * Returns a request that includes a credential to satisfy an authentication challenge in {@code
   * response}. Returns null if the challenge cannot be satisfied.
   */
  @Nullable Request authenticate(Route route, Response response) throws IOException;
}

也就是说默认的只会返回null,那么默认情况下自然不会重新请求了,想要做到接收407状态码后自动重新请求,需要我们手动设置一个proxyAuthenticator,也就是覆写authenticate方法。401状态码道理一样,默认也是NONE,如果有需要,得自己手动添加授权。

  1. HTTP_CLIENT_TIMEOUT (408)
    408状态码比较特殊,okhttp直接说了这个是rare的,遇到情况很少
// 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();

408表示请求超时,对于408,是可以拿原来的Request重新请求,但某些情况下应该放弃:
(1)上传的request的信息不可重复,比如从流中读取的
(2) 之前已经遇到过408,这一次还是408,直接放弃
(3) 服务端明确告知重新尝试时间大于0的
这种情况要说明下,收到408后,某些情况下服务端会通过在Response中加入“Retry-After"字段告知客户端什么时候重新尝试,所以这里取了这个字段判断:

  if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

这里如果大于0,说明要延后请求,那么当前就不应该重新请求,所以返回null。
除了上述情况外,都会直接返回上次请求的Request然后重新请求。

  1. HTTP_UNAVAILABLE(503)
    503表示服务器错误:
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;

通常来说遇到503不应该重新请求,但若服务端返回Retry-After字段且为0,则这里会重新请求,但连续两次都收到503则会放弃。

  1. 重定向
    这里包括以下状态码:HTTP_PERM_REDIRECT(308)、 HTTP_TEMP_REDIRECT(307)、 HTTP_MULT_CHOICE(300)、 HTTP_MOVED_PERM(301)、 HTTP_MOVED_TEMP(302)、 HTTP_SEE_OTHER(303)
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();

这段代码看似很长,实际上只是找到Response中的Location字段携带的新的地址,然后重新构建一个Request而已。

其他

经过上面的分析,整个RetryAndFollowUpInterceptor应该是非常清楚了,当然,这里我们省略了一些代码,比如:

@Override public Response intercept(Chain chain) throws IOException {
   //省略其他代码
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
 //省略其他代码
}

这个StreamAllocation是真正负责socket连接的,这里又没有涉及到真正的socket,为什么在这里实例化?这是因为RetryAndFollowUpInterceptor 里面一些情况下是需要释放连接的,而它又是第一个核心功能拦截器,所以必须在这里实例化,这是为了确保这里能释放。

总结

现在,我们可以完整的回答之前提出的问题了

  • 重新请求有没有次数限制
    有,最多20次
  • 哪些情况需要重新请求
    (1) 401,407:未授权情况下且自定义了授权的Authenticator
    (2) 408:第一次出现,且返回的Response中Retry-After为0
    (3) 503:第一次出现,且返回的Response中Retry-After为0
    (4) 300,301,302,303,307,308: 自己没有禁止okhttp重定向功能,且返回的Response重的Location字段携带有效的url地址的
  • 是否每次都是新地址,若是,地址从哪里获取
    只有重定向情况会向新地址发起请求,新地址是从上一次返回的Response的header中的Location字段携带的,其他情况都是向原地址重新请求
  • 重新请求和第一次请求有区别吗
    401,407情况下,新的请求Request会多出授权信息;
    重定向情况下会删掉一些多余的Header中的字段

相关文章

网友评论

    本文标题:OkHttp源码之RetryAndFollowUpInterce

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