美文网首页
OkHttp源码学习之二 RetryAndFollowUpInt

OkHttp源码学习之二 RetryAndFollowUpInt

作者: leilifengxingmw | 来源:发表于2018-09-02 12:26 被阅读11次

    上一篇讲到在RealInterceptorChain的proceed方法中,首先由RetryAndFollowUpInterceptor处理请求

    Interceptor interceptor = interceptors.get(index);
    //调用当前拦截器的intercept方法
    Response response = interceptor.intercept(next);
     ...
    return response;
    

    RetryAndFollowUpInterceptor 重试重定向拦截器:负责从请求失败中恢复,在必要的时候进行重定向。如果call被取消的话,可能会抛出IOException。

    进入到RetryAndFollowUpInterceptor的intercept方法,代码不是很长,不到100行,稳住,不要慌。

     @Override public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Call call = realChain.call();
        EventListener eventListener = realChain.eventListener();
        // 1 首先构建一个StreamAllocation实例
        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(request.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
        // 2 重定向的次数
        int followUpCount = 0;
        // 3 本次重试或者重定向之前的响应
        Response priorResponse = null;
        // 4 while循环,重试或者进行重定向
        while (true) {
          //如果在重试或者进行重定向过程中,请求被取消了,抛出异常
          if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
          }
    
          Response response;
          boolean releaseConnection = true;
          try {
            //这里什么还没做,就交给下一个interceptor进行处理了
            response = realChain.proceed(request, streamAllocation, null, null);
            releaseConnection = false;
          } catch (RouteException e) {
            //如果是路由异常,说明尝试通过一个路由建立连接失败,请求还没有发出去
            // 5如果不能从RouteException异常中恢复,就直接抛出异常
            if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
              throw e.getFirstConnectException();
            }
             // 如果能恢复,就重新开始while循环
            releaseConnection = false;
            continue;
          } catch (IOException e) {
            // 如果是IOException异常,说明和服务器通信失败。请求可能已经被发送了
           // 6 如果不能从异常中恢复,就直接抛出异常
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
            //如果能恢复,就重新开始while循环
            releaseConnection = false;
            continue;
          } finally {
            // 7 我们抛出了一个未知的异常,释放所有的资源
            if (releaseConnection) {
              streamAllocation.streamFailed(null);
              streamAllocation.release();
            }
          }
    
          // 如果我们在重试或者重定向过程中产生了中间的响应(这样的响应没有响应体)我
         //们就把中间响应附加到response上
          if (priorResponse != null) {
            response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                        .body(null)
                        .build())
                .build();
          }
          //重定向请求
          Request followUp;
          try {
            // 8 获取重定向请求
            followUp = followUpRequest(response, streamAllocation.route());
          } catch (IOException e) {
            streamAllocation.release();
            throw e;
          }
          //如果重定向请求为null,说明不需要重定向,就可以释放连接,返回响应了
          if (followUp == null) {
            if (!forWebSocket) {
              streamAllocation.release();
            }
            return response;
          }
          // 需要重定向,关闭响应流
          closeQuietly(response.body());
          // 9 如果重定向的次数过多,就直接抛出异常
          if (++followUpCount > MAX_FOLLOW_UPS) {
            streamAllocation.release();
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
          }
          // 10 如果重定向请求的请求体类型是UnrepeatableRequestBody,就直接抛出异常
          if (followUp.body() instanceof UnrepeatableRequestBody) {
            streamAllocation.release();
            throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
          }
          // 11 如果重定向请求不能重用上次请求的连接,就重新实例化一个 StreamAllocation
          if (!sameConnection(response, followUp.url())) {
            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?");
          }
          // 12 把请求赋值为重定向请求,把priorResponse赋值为response,继续重试或者重定向
          request = followUp;
          priorResponse = response;
        }
      }
    
    1. 首先构建一个StreamAllocation实例,在拦截器的一系列操作工程中会陆续创建一些需要用到的对象,我们在创建RealInterceptorChain实例的时候,很多对象开始都是传入的null
    // 1 首先构建一个StreamAllocation实例
        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(request.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
    
    Response getResponseWithInterceptorChain() throws IOException {
     
        //构建拦截器链
        Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
            originalRequest, this, eventListener, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());
        //拦截器链处理请求
        return chain.proceed(originalRequest);
      }
    

    StreamAllocation 这个类用来协调Connections,Streams,Calls 三者之间的关系,StreamAllocation的实例能够代表call,在一个或多个connection上使用一个或多个stream,并且StreamAllocation类有API可以用来释放上述三者的所有资源。

    • connections:到远端服务器的物理socket连接。这些连接可能很慢才能建立成功,所以必须能够取消一个正在建立的连接。
    • streams:逻辑上的HTTP request/response对,依赖于connections。每个connection都有自己的分配限制,该限制定义了每个connection能同时携带多少个并发的stream。一个HTTP/1.x connection只能携带一个stream,每个HTTP/2.x connection通常携带多个stream。
      calls:逻辑上的 stream序列,通常是初始request和重定向request。我们希望一个call的所有stream共用一个connection,这样可以获得更好的性能

    释放资源的API

    • StreamAllocation#noNewStreams():禁止正在被使用的connection创建新的stream
    • StreamAllocation#streamFinished():释放当前alloction上活跃的stream。在一个指定的时间内只能有一个活跃的stream,所以在创建一个新的stream之前必须调用streamFinished
    • StreamAllocation#release():移除call对connection 的持有。注意当stream依然存在的时候,connection 不会被立即释放。这种情况发生在,请求完成了,但是response还没有完全被消费。
    1. 记录重定向的次数
    // 2 重定向的次数,用来在后续的过程中判断重定向次数是否超过了允许的最大重定向次数
    int followUpCount = 0;
    
    1. 本次重试或者重定向之前的响应
     // 3 本次重试或者重定向之前的响应
     Response priorResponse = null;
    
    1. 进入while循环,重试或者进行重定向。

    首先,如果在重试或者进行重定向过程中,请求被取消了,抛出异常,跳出while循环

     //如果在重试或者进行重定向过程中,请求被取消了,抛出异常
          if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
          }
    

    然后在try块中交给下一个拦截器进行处理request,并传递StreamAllocation实例。

    try {
           //这里什么还没做,就交给下一个interceptor进行处理了
           response = realChain.proceed(request, streamAllocation, null, null);
           releaseConnection = false;
         } 
    

    如果在后续的拦截器处理过程中出现了异常,这时候,就显现出RetryAndFollowUpInterceptor的作用了。

    如果是路由异常,说明尝试通过一个路由建立连接失败,请求还没有发出去,如果不能从RouteException异常中恢复,就直接抛出异常,如果能恢复,就重新开始while循环

    catch (RouteException e) {
            //如果是路由异常,说明尝试通过一个路由建立连接失败,请求还没有发出去
            // 5如果不能从RouteException异常中恢复,就直接抛出异常
            if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
              throw e.getFirstConnectException();
            }
             //如果能恢复,就重新开始while循环
            releaseConnection = false;
            continue;
          }
    
    1. 如果不能从RouteException异常中恢复,就直接抛出异常,进入recover方法看一看
    /**
       * 尝试从与服务器通信的失败中恢复,如果能够恢复的话,返回true,如果失败是永久性的,则返回false。
       *一个带有请求体的request只有请求体被缓存了,或者请求还没有被发出去才有可能被恢复。
       */
      private boolean recover(IOException e, StreamAllocation streamAllocation,
          boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);
    
        // 应用层拒绝重试,如果我们在配置OkHttpClient的时候拒绝重试,返回false
        if (!client.retryOnConnectionFailure()) return false;
    
        // 如果请求体是UnrepeatableRequestBody类型,返回false
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) 
        return false;
    
        // 如果是致命性的异常,一些协议问题,或者安全问题等,返回false
        if (!isRecoverable(e, requestSendStarted)) return false;
    
        // 没有更多的路由来尝试,返回false
        if (!streamAllocation.hasMoreRoutes()) return false;
    
        // 能够恢复,返回true
        return true;
      }
    

    好了,反正能从失败中恢复的话,就continue,重新while,否则就抛出异常。

    1. IOException也是一样,如果能从失败中恢复的话,就continue,重新while,否则就抛出异常。
    catch (IOException e) {
            // 如果是IOException异常,说明和服务器通信失败。请求可能已经被发送了
           // 6 如果不能从异常中恢复,就直接抛出异常
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
            //如果能恢复,就重新开始while循环
            releaseConnection = false;
            continue;
          }
    
    1. 如果抛出了一个未知的异常,像什么NullPointerException或者,IllegalStateException释放所有的资源,程序结束
    finally {
            // 7 我们抛出了一个未知的异常,释放所有的资源
            if (releaseConnection) {
              streamAllocation.streamFailed(null);
              streamAllocation.release();
            }
          }
    
    1. 获取重定向请求,如果重定向请求为null,说明不需要重定向,就可以释放连接,返回响应了
     //重定向请求
          Request followUp;
          try {
            // 8 获取重定向请求
            followUp = followUpRequest(response, streamAllocation.route());
          } catch (IOException e) {
            streamAllocation.release();
            throw e;
          }
          //如果重定向请求为null,说明不需要重定向,就可以释放连接,返回响应了
          if (followUp == null) {
            if (!forWebSocket) {
              streamAllocation.release();
            }
            return response;
          }
    

    如果重定向请求followUp不为null,那么就需要进行重定向请求了。

    1. 首先重定向次数先加1,如果重定向次数过多,大于MAX_FOLLOW_UPS的话,就直接抛出异常
     // 9 如果重定向的次数过多
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
    
     /**
       * 我们应该尝试多少次重定向和验证?Chrome会尝试21次;Firefox, curl, and   
       * wget 尝试 20次;Safari 尝试 16次; and HTTP/1.0 建议 5次。
       */
      private static final int MAX_FOLLOW_UPS = 20;
    
    1. 如果重定向请求的请求体类型是UnrepeatableRequestBody,就直接抛出异常
     // 10 如果重定向请求的请求体类型是UnrepeatableRequestBody,就直接抛出异常
          if (followUp.body() instanceof UnrepeatableRequestBody) {
            streamAllocation.release();
            throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
          }
    
    1. 如果重定向请求不能重用上次请求的连接,就重新实例化一个StreamAllocation
     // 11 如果重定向请求不能重用上次请求的连接,就重新实例化一个 StreamAllocation
          if (!sameConnection(response, followUp.url())) {
            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?");
          }
    
    1. 把请求赋值为重定向请求,把priorResponse赋值为response,继续重试或者重定向
      // 12 把请求赋值为重定向请求,把priorResponse赋值为response,继续while循环,重试或者重定向
      request = followUp;
      priorResponse = response;
    

    以上就是RetryAndFollowUpInterceptor 重试重定向拦截器大体执行流程,关于细节方面很多还是没有涉及到,在后续的学习和研究过程中,会慢慢完善。

    参考链接

    1. Interceptors
    2. okhttp源码分析(二)-RetryAndFollowUpInterceptor过滤器

    相关文章

      网友评论

          本文标题:OkHttp源码学习之二 RetryAndFollowUpInt

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