OkHttp3.0(四)-Interceptor拦截器(3)-RetryAndFollowUpInterceptor

1.概述

上一章节我们讲解了Application拦截器和Network拦截器,主要从官网和源码的角度,分析了这两个用户可传入的拦截器工作原理、使用方法,到目前为止,我们对拦截器已经并不陌生了。根据拦截器链的顺序,我们今天学习系统拦截器的第一个:RetryAndFollowUpInterceptor(重定向拦截器),顾名思义,它主要的作用,就是做OkHttp网络请求失败重连。

2.RetryAndFollowUpInterceptor中几个重要点

在讲解RetryAndFollowUpInterceptor失败重连原理之前,我们需要掌握如下几点,才能更容易的明白RetryAndFollowUpInterceptor的工作原理。

2.1.OkHttpClient设置失败重连

用户可以通过OkHttpClient设置失败重连,或者可以判断当前OkHttpClient是否支持失败重连,我们通过OkHttpClient.Builder源码看下

  public static final class Builder {
    ...
    //失败重连标记
    boolean retryOnConnectionFailure;
    ...
    public Builder() {
      ..
      //默认允许失败重连
      retryOnConnectionFailure = true;
      ...
    }
    Builder(OkHttpClient okHttpClient) {
      ...
      this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
      ...
    }
    //设置失败重连  true 允许失败重连,false 不允许失败重连
    public Builder retryOnConnectionFailure(boolean retryOnConnectionFailure) {
      this.retryOnConnectionFailure = retryOnConnectionFailure;
      return this;
    }
    public OkHttpClient build() {
      return new OkHttpClient(this);
    }
  }

所以可以通过OkHttpClient.Builder的retryOnConnectionFailure(boolean)方法设置是否支持失败重连,默认情况下是支持失败重连的。可以通过如下代码设置:

OkHttpClient client = new OkHttpClient.Builder().retryOnConnectionFailure(false).build();

boolean retryOnConnectionFailure = client.retryOnConnectionFailure();//查看当前是否允许失败重连

2.2.RouteException异常

RouteException(路由异常),继承于RuntimeException,属于运行时异常,在我们RetryAndFollowUpInterceptor中会使用到,我们看一下这个异常在哪里抛出过

是的,在RealConnection和StreamAllocation这两个类中,由于我们今天主要讲的是重定向拦截器,RealConnection我们会在后面详细讲解,StreamAllocation我们今天会大概说一下它的作用,后面也会详细分析。RouteException分别在RealConnection的connect()方法和StreamAllocation的newStream()方法中被抛出,都没有进行捕获,因为该异常最终统一会由RetryAndFollowUpInterceptor捕获处理。connect()方法是与服务器建立连接,newStream()是获取流,这两个方法在后面执行ConnectInterceptor时候会被调用,与服务器建立连接,所以在那里抛出“路由异常”也是情理之中。

2.3.StreamAllocation简单了解

StreamAllocation 按照Allocation Stream可以解释为“分配流”,分配与服务器数据传输的流。是用来建立HTTP请求所需网络设施组件的,比如说HttpCodec(OkHttp中的流)、刚才提到的RealConnection等,它还提供了调用RealConnection的connect()方法与服务器建立连接的方法,提供了断开连接的方法release(),提供了对路由的判断等等,StreamAllocation的实例是在RetryAndFollowUpInterceptor中创建的,会一直被拦截器链传递到ConnectInterceptor才会被真正的使用。我们看一下StreamAllocation·的部分代码,抓住重点,只看与今天所讲有关的,我们大概了解一下StreamAllocation中如下的几个方法即可:

//释放连接
public void release() {
    ...
    closeQuietly(socket);
    ...
}
//关闭连接
public static void closeQuietly(Socket socket) {
     ...     
     socket.close();
     ...
}
 //获取数据交换流
 public HttpCodec codec() {
    synchronized (connectionPool) {
      return codec;
    }
  }
//连接过程出现异常会调用此方法,根据不同的异常做出不同的处理
public void streamFailed(IOException e) {
      ...
      if (e instanceof StreamResetException) {
       ...
      } else if (connection != null
          && (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
       ...
    }
    //关闭连接
    closeQuietly(socket);
   ...
  }
//该链接是否还有更多路线
public boolean hasMoreRoutes() {
    return route != null
        || (routeSelection != null && routeSelection.hasNext())
        || routeSelector.hasNext();
}

3.RetryAndFollowUpInterceptor的工作原理

3.1.取消请求cancel()

不知道大家是否还记得,我们在将Call的时候,说过RealCall是Call的唯一实现类,实现的方法中,有一个cancel()方法,用来取消当前Call所持有的请求,其实RealCall的cancel()方法,调用的就是RetryAndFollowUpInterceptor的cancel()方法,可以通过代码看一下:

RealCall中的cancel()方法

 @Override public void cancel() {
    retryAndFollowUpInterceptor.cancel();
  }

RetryAndFollowUpInterceptor中的cancel()方法

 private volatile StreamAllocation streamAllocation;
 private volatile boolean canceled;
 public void cancel() {
    canceled = true;
    StreamAllocation streamAllocation = this.streamAllocation;
    if (streamAllocation != null) streamAllocation.cancel();//最终执行了StreamAllocation的cancel方法
  }

StreamAllocation的cancel()方法

 //最终会执行RealConnection的cancel()方法
 public void cancel() {
    HttpCodec codecToCancel;
    RealConnection connectionToCancel;
    ...
    if (codecToCancel != null) {
      codecToCancel.cancel();
    } else if (connectionToCancel != null) {
      connectionToCancel.cancel();
    }
  }

通过源码的继续追踪,我们会发现,取消网络请求,最终执行的,还是RealConnection的cancel()方法,我们直接看一下

RealConnection的cancel()方法

  public void cancel() {
    // Close the raw socket so we don't end up doing synchronous I/O.
    closeQuietly(rawSocket);
  }
  public static void closeQuietly(Socket socket) {
    if (socket != null) {
      try {
        socket.close();//关闭Socket连接
      } catch (AssertionError e) {
        if (!isAndroidGetsocknameError(e)) throw e;
      } catch (RuntimeException rethrown) {
        throw rethrown;
      } catch (Exception ignored) {
      }
    }
  }

可以看到,最后还是会关闭Socket,到这里,我们已经明白了,Call的取消请求的cancel()方法,实际上调用的就是重定向拦截器RetryAndFollowUpInterceptor的cancel(),最终会调用RealConnection的cancel()方法关闭Socket连接。

3.2.失败重连

RetryAndFollowUpInterceptor的主要功能,就是失败重连,我们接下来看一下它的工作原理,我给大家看的源码,是OkHttp3.11版本的,可能某些地方跟大家看的稍微有些不一样的地方。我们一起来看下RetryAndFollowUpInterceptor的intercept()方法,使用插入代码块的格式,会让我的注释变成灰色,阅读比较费劲,这段代码注释比较多,我就把它直接粘贴上来:

 

 @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();//获取请求Request
    RealInterceptorChain realChain = (RealInterceptorChain) chain;//获取拦截器链
    Call call = realChain.call();//获取当前的Call对象
    EventListener eventListener = realChain.eventListener();//获取事件监听
    //创建了StreamAllocation对象
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    //设置重连次数初始值为0
    int followUpCount = 0;
    Response priorResponse = null;//创建Response的变量,待会儿会给他赋值
    while (true) {//循环进入失败重连代码
      if (canceled) {//如果该请求已经被取消,但还是执行到了重定向拦截器,则释streamAllocation并抛出异常结束
        streamAllocation.release();
        throw new IOException("Canceled");
      }
    
      Response response;//定义网络请求结果变量Response
      boolean releaseConnection = true;//是否释放连接,默认为true
      try {
        //调用拦截器链的proceed()方法,将请求和刚才创建的StreamAllocation对象传递下去,进行网络连接,获取Response
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;//如果没有发生异常,则设置releaseConnection 为true
      } catch (RouteException e) {//后面网络请求发生RouteException异常
        // The attempt to connect via a route failed. The request will not have been sent.   //判断能否恢复连接,第三个参数传入的是false,这里可能跟某些OkHttp版本不同
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();//不能恢复连接则抛出异常结束
        }
        releaseConnection = false;//能恢复重连,则设置releaseConnection 为true,
        continue;//此处回到下一次循环,继续重连
      } catch (IOException e) {//后面网络请求发生IOException
        // An attempt to communicate with a server failed. The request may have been sent.
//是否连接中断异常,如果是 requestSendStarted为false,反之requestSendStarted为true
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        //如果不能恢复连接,则抛出异常结束,此处第三个参数是requestSendStarted,可能跟某些OkHttp版本不同。
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;//如果可以恢复连接,则设置releaseConnection为false
        continue;//此处回到下一次循环,继续重连
      } finally {此处为必然执行代码
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          //如果releaseConnection的值仍为true,则关闭流,释放streamAllocation
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
      //走到此处,说明已经顺利完成网络请求,返回了Response,响应码Code不一定为200 OK
      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        //说明前面已经完成了一次请求,需要结合前面获取的Response构建新的Response并赋值给变量response
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Request followUp;
      try {
        //对返回的response进行响应码Code的判断,如果需要失败重连,则返回一个Request对象
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        //如果判断过程中被抛出异常,那就释放掉streamAllocation,并继续抛出异常结束
        streamAllocation.release();
        throw e;
      }
      //判断响应码之后,返回的是空的Request,那就没必要重新请求,直接返回Response
      if (followUp == null) {则释放streamAllocation
          streamAllocation.release();
        }
        return response;//返回请求结果
      }
       //关闭Response的数据源
      closeQuietly(response.body());
      if (++followUpCount > MAX_FOLLOW_UPS) {
      //判断重连次数,如果超出默认最大值:MAX_FOLLOW_UPS(20次),则释放掉streamAllocation,并抛出协议异常结束
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
       //请求体被UnrepeatableRequestBody标记,则不可重试
      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();//释放streamAllocation,抛出异常结束
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }
      //判断要重连的接口与刚完成的请求的host、port、scheme是否一致
      if (!sameConnection(response, followUp.url())) {
        //不一致,则释放streamAllocation,重新建立重定向之后的StreamAllocation对象
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
      } else if (streamAllocation.codec() != null) {
        //如果前后的接口一致,可以重连,但是该请求的流还没有被关闭,则抛出异常
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }
      //将Request赋值为重定向后的Request,priorResponse 赋值为上面得到并且重新构建的response
      request = followUp;
      priorResponse = response;
      //进入下一次循环,重新连接
    }
  }

RetryAndFollowUpInterceptor的intercept()方法我们已经分析完了,接下来,我们要看一下上面代码中涉及到的问题以及使用到的方法

3.2.1.StreamAllocation的release()方法

我们前面已经说过了StreamAllocation,但是我们还是要提一嘴,因为在失败重连的过程中,我们看到代码多次调用了StreamAllocation的release()方法,这里最终会关闭Socket连接,感兴趣的同学可以去看下源码。

3.2.2.recover()方法判断是否可以恢复连接

上面的intercept()代码中我们还记的这一段

try {  ...
        response = realChain.proceed(request, streamAllocation, null, null);
      } catch (RouteException e) {
//判断能否恢复连接,第三个参数传入的是false,这里可能跟某些OkHttp版本不同
        if (!
recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();//不能恢复连接则抛出异常结束
        }
      ...
      } catch (IOException e) {//后面网络请求发生IOException
       
//是否连接中断异常,如果是 requestSendStarted为false,反之requestSendStarted为true
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        //如果不能恢复连接,则抛出异常结束,此处第三个参数是requestSendStarted,可能跟某些OkHttp版本不同。
        if (
!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;//如果可以恢复连接,则设置releaseConnection为false
        continue;//此处回到下一次循环,继续重连
      }

我们就来大概看下recover()方法中做了什么事情

//判断与服务器的连接是否为可恢复,返回false,则不可恢复,即不可重连
private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    //根据抛出的异常,做出连接、连接路线的一些处理,并且释放连接,关闭连接
    streamAllocation.streamFailed(e);
    // 如果用户不允许失败重连,则返回false
    if (!client.retryOnConnectionFailure()) return false;
    // 不是连接中断异常,并且请求体被UnrepeatableRequestBody标记,返回false
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    // 根据异常判断是否可以恢复连接
    if (!isRecoverable(e, requestSendStarted)) return false;
    //如果没有多余线路连接,则不可恢复
    if (!streamAllocation.hasMoreRoutes()) return false;
    return true;
  }
  //根据异常判断是否可以恢复连接
  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    //协议问题,不可恢复
    if (e instanceof ProtocolException) {
      return false;
    } 
    // IO连接中断
    if (e instanceof InterruptedIOException) {
      //如果线路连接超时,可以换个线路重试
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }
    // 服务证书异常
    if (e instanceof SSLHandshakeException) {
      //如果是证书问题,则不可恢复连接
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    //证书校验失败,不可恢复连接
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false;
    }
    //返回true,则可以重新连接
    return true;
  }

当网络连接发生RouteException和IOException时,会调用recover()方法来判断当前连接是否可以恢复,recover()方法,首先会根据当前发生的异常,对路由做一些处理,并且关闭流,释放StreamAllocation。接下来recover()方法会判断用户是否允许失败重连,如果用户允许,则会进一步分析连接时发生的异常,经过分析如果连接还可以恢复则返回true,否则返回false。

3.2.3.followUpRequest()方法添加身份验证Headers、重定向等操作

当执行到followUpRequest()方法,本次请求已经完成,并且返回了Response,followUpRequest()方法主要是会判断Response的Code,是否需要添加身份验证头、重定向等操作,从而决定是否需要重新向服务器端发起请求。

 /**
  *根据网络请求的响应码Code,判断当前请求是否还需要进一步添加身份验证或者重定向,如果需要则构建新        
  * 的Request返回,用于重新发送请求,否则返回null
  */
 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 (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      case HTTP_MULT_CHOICE://响应存在多种选择,需要客户端做出其中一种选择
      case HTTP_MOVED_PERM://请求的资源路径永久改变
      case HTTP_MOVED_TEMP://请求资源路径临时改变
      case HTTP_SEE_OTHER://服务端要求客户端使用GET访问另一个URI
      ......
      case HTTP_CLIENT_TIMEOUT://请求超时
      ......
      case HTTP_UNAVAILABLE://服务器临时不可用
      ......
      default:
        return null;
    }
  }

我们大概分析了下followUpRequest()方法,不必做过深入的研究。followUpRequest()对服务器返回的Response的响应码Code进行了判断,判断是否需要添加身份验证的Headers、是否需要重定向、是否需要修改Url等,如果是,则需要重新向服务器发送请求,所以构建一个新的Request返回,否则就不需要或者不可以重新向服务器发送请求,则返回null。

3.2.4.失败重连的次数不是无限的

private static final int MAX_FOLLOW_UPS = 20;

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

当失败重连的次数,超出了默认的最大次数:MAX_FOLLOW_UPS = 20次时,会释放StreamAllocation关闭Socket连接,同时抛出ProtocolException并且告诉客户,重连次数太多。

3.2.5.sameConnection()方法判断连接是否一致

在经过followUpRequest()判断之后,如果需要重连,则还会进行sameConnection()方法判断

 /**
   * 判断Response中的Url和followUp的Host、Port、scheme是否一致
   * engine.
   */
  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());
  }

sameConnection()方法,主要是比较刚刚完成的请求的Url和经过followUpRequest()构建的Request的Url,判断二者之间的HOST、POST、scheme协议是否一致。如果不一致则以followUpRequest()返回的Request为准,释放掉旧的StreamAllocation,创建新的StreamAllocation重新向服务器发送请求。

if (!sameConnection(response, followUp.url())) {
  streamAllocation.release();
  streamAllocation = new StreamAllocation(client.connectionPool(),
      createAddress(followUp.url()), call, eventListener, callStackTrace);
  this.streamAllocation = streamAllocation;
}

4.总结

我们来总结一下RetryAndFollowUpInterceptor的作用及失败重连的原理

  1. 用户调用RealCall的cancel()方法取消请求,RealCall则会调用RetryAndFollowUpInterceptor了cancel()方法,最终会关闭Socket连接;

  2. RetryAndFollowUpInterceptor重定向拦截,其主要作用就是连接失败之后做重新连接,用户可以通过调用OkHttpClient.Builder的

    retryOnConnectionFailure(boolean)方法设置是否允许失败重连;

  3. StreamAllocation由RetryAndFollowUpInterceptor创建,通过拦截器链Chain.proceed()方法,传递给后面的拦截器;

  4. 当拦截器链向服务器端发送请求,抛出RouteException或者发生IOException时,RetryAndFollowUpInterceptor会根据用户的设置以及抛出的异常进行分析,判断当前请求是否可以被恢复,如果不能则释放StreamAllocation关闭Socket连接,并且抛出异常结束。如果当前请求可以被恢复,则回到while循环开始部位,进行重连;

  5. 当向服务器发送网络请求成功,获取到了Response对象,RetryAndFollowUpInterceptor会分析当前Response的响应码,判断是否需要身份验证、重定向、修改URL等操作,如果需要则构建新的Request,进行Url校验之后,重新向服务器端发送请求。如果不需要重新发送请求则不必构建新的Request,RetryAndFollowUpInterceptor任务结束,返回刚获取到的Response。

  6. 失败重连不是无限的,最多允许重连20次。

以上就是今天要跟大家分享的RetryAndFollowUpInterceptor,欢迎各位指正批评,下次我们继续分析剩下的拦截器。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值