源码分析三:OkHttp—RetryAndFollowUpInterceptor

引言

上一篇博客笼统的讲了各个拦截器:

从这边博客开始单独分析每个拦截器的源码。首先是RetryAndFollowUpInterceptor。

这个拦截器它的作用主要是负责请求的重定向操作,用于处理网络请求中,请求失败后的重试机制。

核心功能

  • 连接失败重试(Retry)

    在发生 RouteException 或者 IOException 后,会捕获建联或者读取的一些异常,根据一定的策略判断是否是可恢复的,如果可恢复会重新创建 StreamAllocation 开始新的一轮请求

  • 继续发起请求(Follow up)

    主要有这几种类型

    • 3xx 重定向
    • 401,407 未授权,调用 Authenticator 进行授权后继续发起新的请求
    • 408 客户端请求超时,如果 Request 的请求体没有被 UnrepeatableRequestBody 标记,会继续发起新的请求

其中 Follow up 的次数受到MAX_FOLLOW_UP 约束,在 OkHttp 中为 20 次,这样可以防止重定向死循环。

源码分析

这里详细分析重定向机制,关于RealCall的构造方法,源代码如下所示

RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
  final EventListener.Factory eventListenerFactory = client.eventListenerFactory();

  this.client = client;
  this.originalRequest = originalRequest;
  this.forWebSocket = forWebSocket;
  this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);

  // TODO(jwilson): this is unsafe publication and not threadsafe.
  this.eventListener = eventListenerFactory.create(this);
}复制代码

根据之前拦截器的介绍,会执行RetryAndFollowUpInterceptor的intercept方法。

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

  streamAllocation = new StreamAllocation(
      client.connectionPool(), createAddress(request.url()), callStackTrace);
   // (1)“创建StreamAllocation”
  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {
    if (canceled) {
      streamAllocation.release();
      throw new IOException("Canceled");
    }
    //(2)进入循环,检查请求是否已被取消


    Response response = null;
    boolean releaseConnection = true;
    try {
      response = ((RealInterceptorChain) chain).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(), 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, requestSendStarted, request)) throw e;
      releaseConnection = false;
      continue;
    } finally {
      
      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }
    //(3)“执行RealInterceptorChain.procced,并捕获异常来判断是否可以重定向”

    // 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();
    }
    
    Request followUp = followUpRequest(response);
    //(4)“是否需要进行重定向”

    if (followUp == null) {
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }
    //(5)“不需要重定向,直接返回response,结束。否则到6”
    closeQuietly(response.body());

    if (++followUpCount > MAX_FOLLOW_UPS) {
      streamAllocation.release();
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }
    
    if (followUp.body() instanceof UnrepeatableRequestBody) {
      streamAllocation.release();
      throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
    }
    // (6)“重定向安全检查”

    if (!sameConnection(response, followUp.url())) {
      streamAllocation.release();
      streamAllocation = new StreamAllocation(
          client.connectionPool(), createAddress(followUp.url()), callStackTrace);
    //(7)“根据新的请求创建连接”
    } else if (streamAllocation.codec() != null) {
      throw new IllegalStateException("Closing the body of " + response
          + " didn't close its backing stream. Bad interceptor?");
    }

    request = followUp;
    priorResponse = response;
  }
}复制代码

可以看出主要拦截处理过程是个while循环,如果没有重定向则返回response,有则按循环内处理。

主要有以下5步:

  1. 创建StreamAllocation
  2. 进入循环,检查请求是否已被取消
  3. 执行RealInterceptorChain.procced,并捕获异常来判断是否可以重定向
  4. 是否需要进行重定向
  5. 不需要重定向,直接返回response,结束。否则到6
  6. 重定向安全检查
  7. 根据新的请求创建连接

下面逐个分析:

1.创建StreamAllocation

Request request = chain.request();

streamAllocation = new StreamAllocation(
    client.connectionPool(), createAddress(request.url()), callStackTrace);复制代码

它主要用于管理客户端与服务器之间的连接,同时管理连接池,以及请求成功后的连接释放等操作。

在执行StreamAllocation创建时,可以看到根据客户端请求的地址url,还调用了createAddress方法。进入该方法可以看出,这里返回了一个创建成功的Address,实际上Address就是将客户端请求的网络地址,以及服务器的相关信息,进行了统一的包装,也就是将客户端请求的数据,转换为OkHttp框架中所定义的服务器规范,这样一来,OkHttp框架就可以根据这个规范来与服务器之间进行请求分发了。

private Address createAddress(HttpUrl url) {
  SSLSocketFactory sslSocketFactory = null;
  HostnameVerifier hostnameVerifier = null;
  CertificatePinner certificatePinner = null;
  if (url.isHttps()) {
    sslSocketFactory = client.sslSocketFactory();
    hostnameVerifier = client.hostnameVerifier();
    certificatePinner = client.certificatePinner();
  }

  return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
      sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
      client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
}复制代码

2.检查该请求是否已被取消

这里会进入循环,首先会进行一个安全检查操作,检查当前请求是否被取消,如果这时请求被取消了,则会通过StreamAllocation释放连接,并抛出异常,源代码如下所示

if (canceled) {
  streamAllocation.release();
  throw new IOException("Canceled");
}复制代码

3.捕获异常来判断是否可以重定向

3.1 捕获异常

如果这时没有发生2,接下来会通过RealInterceptorChain的proceed方法处理请求,在请求过程中,只要发生异常,releaseConnection就会为true,一旦变为true,就会将StreamAllocation释放掉。 

根据之前的博文,RealInterceptorChain的proceed会调用后面的拦截器的拦截方法,如果有问题,会抛2种异常:

  • RouteException

    这个异常发生在 Request 请求还没有发出去前,就是打开 Socket 连接失败。这个异常是 OkHttp 自定义的异常,是一个包裹类,包裹住了建联失败中发生的各种 Exception

    主要发生 ConnectInterceptor 建立连接环节

    比如连接超时抛出的 SocketTimeoutException,包裹在 RouteException 中

  • IOException

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

    主要发生在 CallServerInterceptor 中,通过建立好的通道,发送请求并且读取响应的环节

    比如读取超时抛出的 SocketTimeoutException

3.2 重试判断

在捕获到这两种异常后,OkHttp 会使用 recover 方法来判断是否是不可以重试的。然后有两种处理方式:

  • 不可重试的

    会把继续把异常抛出,调用 StreamAllocation 的 streamFailedrelease 方法释放资源,结束请求。OkHttp 有个黑名单机制,用来记录发起失败的 Route,从而在连接发起前将之前失败的 Route 延迟到最后再使用,streamFailed 方法可以将这个出问题的 route 记录下来,放到黑名单(RouteDatabase)。所以下一次发起新请求的时候,上次失败的 Route 会延迟到最后再使用,提高了响应成功率

  • 可以重试的

    则继续使用 StreamAllocation 开始新的 proceed 。是不是可以无限重试下去?并不是,每一次重试,都会调用 RouteSelector 的 next 方法获取新的 Route,当没有可用的 Route 后就不会再重试了

什么情况下的失败是不可以恢复的呢?

继续看recover方法:

private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
  streamAllocation.streamFailed(e);

  // 需要OkHttpClient配置可以重试
  if (!client.retryOnConnectionFailure()) return false;

  // 使用 isRecoverable 方法过滤掉不可恢复异常
  if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

  // 使用 isRecoverable 方法过滤掉不可恢复异常
  if (!isRecoverable(e, requestSendStarted)) return false;

  // 已经没有其他的路由可以使用
  if (!streamAllocation.hasMoreRoutes()) return false;

  // For failure recovery, use the same route selector with a new connection.
  return true;
}复制代码

简单分析 recover 的代码,首先会调用 StreamAllocation 的 streamFailed 方法释放资源。然后通过以下4种策略来判断这些类型是否可以重试:

   (1)OkHttpClient是否配置可以重试

OkHttpClient okHttpClient = new OkHttpClient.Builder()
   ...
   .retryOnConnectionFailure(true)
   ...
   .build();复制代码

    (2)不是被 RouteException 包裹的异常,并且请求的内容被 UnrepeatableRequestBody 标记       

       也就是不是建立连接阶段发生的异常,比如发起请求和获取响应的时候发生的IOException,同时请求的内容不可重复发起,就不能重试

    (3) 使用 isRecoverable 方法过滤掉不可恢复异常

    通过的话会继续进入第 4 步。

    不可重试的异常有:

  • ProtocolException

    协议异常,主要发生在 RealConnection 中创建 HTTPS 通过 HTTP 代理进行连接重试超过 21 次。不可以重试

    private void buildTunneledConnection(int connectTimeout, int readTimeout, int writeTimeout,
    ConnectionSpecSelector connectionSpecSelector) throws IOException {
         ...
         int attemptedConnections = 0;
         int maxAttempts = 21;
         while (true) {
           if (++attemptedConnections > maxAttempts) {
             throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);
           }
          ...
          }
      ...
    }12345678910111213复制代码
  • InterruptedIOException

    如果是建立连接时 SocketTimeoutException,即创建 TCP 连接超时,会走到第4步

    如果是连接已经建立,在读取响应的超时的 SocketTimeoutException,不可恢复

  • CertificateException 引起的 SSLHandshakeException

    证书错误导致的异常,比如证书制作错误

  • SSLPeerUnverifiedException

    访问网站的证书不在你可以信任的证书列表中

    (4) 已经没有其他的路由可以使用

    前面三步条件都通过的,还需要最后一步检验,就是获取可用的 Route。

   public boolean hasMoreRoutes() {
       return route != null || routeSelector.hasNext();
   }  123复制代码

所以满足下面两个条件就可以结束请求,释放 StreamAllocation 的资源了

  • StreamAllocation 的 route 为空
  • RouteSelector 没有可选择的路由了
    • 没有下一个 IP
    • 没有下一个代理
    • 没有下一个延迟使用的 Route(之前有失败过的路由,会在这个列表中延迟使用)

RouteSelector 封装了选择可用路由进行连接的策略。重试一个重要作用,就是这个请求存在多个代理,多个 IP 情况下,OkHttp 帮我们在连接失败后换了个代理和 IP,而不是同一个代理和 IP 反复重试。所以,理所当然的,如果没有其他 IP 了,那么就会停止。比如 DNS 对域名解析后会返回多个 IP。比如有三个 IP,IP1,IP2 和 IP3,第一个连接超时了,会换成第二个;第二个又超时了,换成第三个;第三个还是不给力,那么请求就结束了

但是 OkHttp 在执行以上策略前,也就是 RouteSelector 内部的策略前,还有一个判断,就是该 StreamAllocation 的当前 route 是否为空,如果不为空话会继续使用该 route 而没有走入到 RouteSelector 的策略中。

4.是否需要进行重定向

我们知道,是否需要进行请求重定向,是根据http请求的响应码来决定的,因此,在followUpRequest方法中,将会根据响应userResponse,获取到响应码,并从连接池StreamAllocation中获取连接,然后根据当前连接,得到路由配置参数Route。观察以下代码可以看出,这里通过userResponse得到了响应码responseCode。 

private Request followUpRequest(Response userResponse) throws IOException {
  if (userResponse == null) throw new IllegalStateException();
  Connection connection = streamAllocation.connection();
  Route route = connection != null
      ? connection.route()
      : null;
  int responseCode = userResponse.code();复制代码

接下来将会根据响应码responseCode来检查当前是否需要进行请求重定向,我们知道,在Http响应码中,处于3XX的,都需要进行请求重定向处理。因此,接下来该方法会通过switch…case…来进行不同的响应码处理操作。
从这段代码开始,都是3xx的响应码处理,这里就开始进行请求重定向的处理操作。 

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:复制代码

执行重定向请求构建前,首先根据OkHttpClient,执行了followRedirects方法,检查客户端是否允许进行重定向请求。如果这时客户端未允许重定向,则会返回null。
接下来根据响应获取到位置location,然后根据location,得到重定向的url,代码如下所示。 

if (!client.followRedirects()) return null;

String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);复制代码

接下来这一大段代码就是重新构建Request的过程,根据重定向得到的url,重新构建Request请求,并对请求中的header和body分别进行处理,最后可以看到,通过build模式,将新构建的Request请求进行返回。

// 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();复制代码

5.不需要重定向,直接返回response,结束。否则到6

执行followUpRequest方法,来检查是否需要进行重定向操作。当不需要进行重新定向操作时,就会直接返回Response,如下所示

if (followUp == null) {
  if (!forWebSocket) {
    streamAllocation.release();
  }
  return response;
}复制代码

6.重定向安全检查

1.重定向次数<20

if (++followUpCount > MAX_FOLLOW_UPS) {
  streamAllocation.release();
  throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}复制代码

2.然后根据重定向请求followUp,与当前的响应进行对比,检查是否同一个连接。

通常,当发生请求重定向时,url地址将会有所不同,也就是说,请求的资源在这时已经被分配了新的url。因此,接下来的!sameConnection这个判断将会符合,该这个方法就是用来检查重定向请求,和当前的请求,是否为同一个连接。一般来说,客户端进行重定向请求时,需要与新的url建立连接,而原先的连接,则需要进行销毁。

判断是否为同一个连接:

private boolean sameConnection(Response response, HttpUrl followUp) {
  HttpUrl url = response.request().url();
  return url.host().equals(followUp.host())
      && url.port() == followUp.port()
      && url.scheme().equals(followUp.scheme());
}复制代码

不是同一个连接,则重新建立新连接,并销毁原来的连接:

if (!sameConnection(response, followUp.url())) {
  streamAllocation.release();
  streamAllocation = new StreamAllocation(
      client.connectionPool(), createAddress(followUp.url()), callStackTrace);
} else if (streamAllocation.codec() != null) {
  throw new IllegalStateException("Closing the body of " + response
      + " didn't close its backing stream. Bad interceptor?");
}复制代码


至此,RetryAndFollowUpInterceptor就分析完毕了。


转载于:https://juejin.im/post/5aa23fa46fb9a028cb2d3a96

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值