OKHTTP--RetryAndFollowUpInterceptor重连拦截器

780 阅读5分钟

1.RetryAndFollowUpInterceptor

作为OKHTTP内置的第一个拦截器, 其功能主要有以下几点:

  • 1、创建StreamAllocation
  • 2、调用RealInterceptorChain.proceed()进行网络请求
  • 3、根据异常结果或者响应结果判断是否要进行重新请求 结合代码, 重连机制是通过while(true)实现

重试机制分两个层面的重试机制:

  • 1、请求过程中抛出的异常(连接建议、HTTP请求)
  • 2、请求成功之后获取到状态码, 根据状态码判断是否需要重试

第二个重试有一个最大次数限制, 但是第一种情况的重试则很可能会出现死循环的情况. 如何定位这种问题, OKHTTP提供的了自定义拦截器可以实现这个需求.

如果Request能够正常执行, 则所有拦截器都是递归的方式依次顺序执行完成, 但是如果请求的链路上发生了异常, 如果抛出了RouteException或者是IOExcept, 则可能会被RetryAndFollowUpInterceptor捕获, 然后判断这种异常是否可以重新连接, 如果可以重新连接, 则继续从BridgeInterceptor拦截器开始进行处理, 此时如果只通过自定义的NormalInterceptors拦截器, 是无法感知这种重连的, 可以通过自定义的NetworkInterceptor可以感知这种重试情况.

@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;

    int followUpCount = 0;
    Response priorResponse = null;
    // 2.通过while(true)实现重连机制
    while (true) {
        // 3.如果显示调用cancel方法取消该请求, 重连机制取消
        if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
        }
        Response response;
        boolean releaseConnection = true;
        try {
            // 4.将请求交给RealInterceptorChain, 然后对请求链的异常进行捕获与分析
            response = realChain.proceed(request, streamAllocation, null, null);
            releaseConnection = false;
        } catch (RouteException e) {
            // 5.如果发生了路由异常, 再分析ConnectInterceptor拦截器时了解到路由异常发生在TCP连接阶段
            //   针对该阶段抛出的异常, 通过recover判断该异常是否需要抛出还是可以继续重连
            if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                throw e.getFirstConnectException();
            }
            releaseConnection = false;
            continue;
        } catch (IOException e) {
            // 6.捕获到IO异常, 判断该异常是否需要抛出还是可以继续重连.
            //   与5都是通过recover进行判断, 区别就是第三个参数, 5传的是false, 6传的是requestSendStarted,
            //   之所以这样, 是因为这个阶段可能存在两种情况, (1)HTTP请求还未发送到服务器就出现了异常,
            //   (2) 请求发送到服务器, 但是由于服务器原因, 连接被迫断开, 导致IO异常
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
            releaseConnection = false;
            continue;
        } finally {
            if (releaseConnection) {
                streamAllocation.streamFailed(null);
                streamAllocation.release();
            }
        }
        // 7.执行到这里, 说明请求已经发送到Server, 并且收到了Server端的响应, 但是响应码不一定为200
        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;
        }
        if (followUp == null) {
            streamAllocation.release();
            return response;
        }
        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());
        }
        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?");
        }
        request = followUp;
        priorResponse = response;
    }
}

2.RouteException

当抛出RouteException异常时, 通过以下几步来确定是否需要重连;

  • 1、通过recover方法检测该RouteException是否能重新连接
  • 2、如果不能重连, 抛出该异常, 如果能重连, 将标识位releaseConnection置为false, 表示不释放连接
  • 3、continue进入到下一次循环, 进行网络请求
2.1 RouteException场景下recover

结合上图, RouteException是在RealConnection.connect与StreamAllocation.newStream中抛出, 这个阶段是建立TCP连接的过程, 此过程请求还没有开始, 同时注意RouteException异常时, recover第三个参数为false, 表示请求还没有发出

@return true:可以重连, false:不支持重连
private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);
    // 1.如果应用层不支持重连(对外提供接口, 提高优先级, SDK开发某些功能是否可用可以参考这里的设计)
    if (!client.retryOnConnectionFailure()) return false;
    // 2.请求体不支持重试机制时
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    // 3.判断该异常是否支持重连
    if (!isRecoverable(e, requestSendStarted)) return false;
    // 4.如果没有更多的路由, 也不支持重连
    if (!streamAllocation.hasMoreRoutes()) return false;
    // 5.剩余的场景可以重连
    return true;
}
2.2 isRecoverable
@return true: 允许该异常的情况下进行重连, false: 不允许该异常的情况下进行重连
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // 1.如果协议异常
    if (e instanceof ProtocolException) {
        return false;
    }
    // 2.如果请求还未发出的情况下发生了超时异常, 也允许这种情况下进行重新连接
    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.
    // 3.如果是证书校验相关的异常, 也不允许进行重连
    if (e instanceof SSLHandshakeException) {
        if (e.getCause() instanceof CertificateException) {
            return false;
        }
    }
    if (e instanceof SSLPeerUnverifiedException) {
        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.
    // 4.注释解释的很清楚了, 除非这个异常是我们明确知道的不能进行重连的, 否则我们尽量尝试进行下一次的重连, 重连时切换到
    //   下一个路由(这里的下一个路由包括同一个Proxy下的路由, 或者是切换到下一个Proxy然后遍历其所有的Route)
    return true;
}