Android Okhttp3《二》RetryAndFollowUpInterceptor

前言

前一篇文章简单介绍了OkHttp 以及创建一个请求与请求执行过程,本编继续接着向下介绍

一、责任链

  private Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

我们进入RealInterceptorChain 的proceed

//代码有删减
  public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream,
      Connection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpStream, connection, index + 1, request);

    Interceptor interceptor = interceptors.get(index);
         //调用interceptor的intercept 方法
    Response response = interceptor.intercept(next);
    

    return response;
  }

一方面RealInterceptorChain 可以 看成是一个拦截器的装饰类,被装饰的拦截器就是在拦截器列表interceptors内下标为index的拦截器。
在这里插入图片描述
另一方面表示拦截器链表,但是你也可以将RealInterceptorChain
在这里插入图片描述
我们可以看到,当调用被装饰的拦截器的时候会传入另一个RealInterceptorChain ,这个RealInterceptorChain 可以看成是下一个拦截器,因此当当前interceptor 执行完任务之后可以通过next调用下一个拦截的代码,如此逐级调用,形成了一个链表

二、RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 是OKHttp内部第一个拦截器,主要用来重定向与网络访问失败重试。

重定向:
  1 当求情的资源url发生了变换,Http协议会返回301,,302,303,307,308中的其中一种,此时需要根据返回的资源新的url求获取资源。
  2 有点url可能会对应多个ip地址,当某一个ip地址不能用的时候,此时会尝试链接另一个ip,这一点跟HTTP协议没有关系。

重式:

  1 当请求服务的时候缺少验证信息,此时根据HTTP协议此时会返回401,要求用户输入账号,密码之后再次请求。这一部分跟HTTP协议有关系

  2 网络环境是复杂的,可能由于某些原因会导致某一瞬间连接服务器失败,此时可以在第一次失败之后再次尝试链接,这一部分与HTTP协议没有关系,是实现HTTP的软件需要考虑的情况。

了解了上面的情况之后我们再来接着阅读源码部分

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

    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()));

    int followUpCount = 0;
    Response priorResponse = null;
    //这里维持这一个循环
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true;
      try {
      //proceed 内部会建立连接访问网络。
        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.
        //ConnectInterceptor 以及之后的拦截器
        //在连接的过程中发生的IO异常最终都会别包装成RouteException,
        //最终抛到这里处理。
        //当用户没有禁止重试而且有多个ip地址的时候此时revover 返回true
        if (!recover(e.getLastConnectException(), true, request)) throw e.getLastConnectException();
        releaseConnection = false;
        continue;
      } catch (IOException e) {
      BridgeInterceptor以及CacheInterceptor 发生的IO 异常会走到这里
        if (!recover(e, false, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // 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);

    // 如果不需要重定向,返回结果
      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);
      }

     // 如果body内容只能发送一次,释放连接
      if (followUp.body() instanceof UnrepeatableRequestBody) {
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }

     //到了这里表示因为重试或者重定向需要重新访问网络,那么需要判断
     // 重试的host等是否发生改变,如果是需要重新实例化StreamAllocation
      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(followUp.url()));
      } else if (streamAllocation.stream() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }
      // 更新下一次的网络请求对象
      request = followUp;
      // 保存上一次的请求结果
      priorResponse = response;
    }
  }

1 两种异常
  RouteException 与IOException, 实际上RouteException 就是对IOException 的装饰,就是说RouteException 就是IOException ,区别在于发生的时机是不一样。当在底层使用sokect连接服务器的时候,可能会因为tcp的握手失败或者是https 握手失败等等情形造成IOException ,此时的IOException 会抛到ConnectInterceptor,而ConnectInterceptor 会将所有的IOException 包装成RouteException 。
在这里插入图片描述

在访问网络之前也即是ConnectInterceptor 之前的拦截器发生的IO异常会正常的抛到RetryAndFollowUpInterceptor。ConnectInterceptor 之前的拦截器也就是BridgeInterceptor与CacheInterceptor。
在这里插入图片描述

2 正常返回
  如果没有发生异常,此时就会根据服务器返回的状态码做进一步的处理,判断是不是需要重定向等等,这一部分主要是followUpRequest

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://407
      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");
      }
 //客户端必须代理服务器上进行身份验证。
        //代理服务器必须返回一个 Proxy-Authenticate 用以进行身份询问。
        //客户端可以返回一个 Proxy-Authorization 信息头用以验证
      return client.proxyAuthenticator().authenticate(route, userResponse);

    case HTTP_UNAUTHORIZED://401
//当前请求需要用户验证。就是要求输入账号密码
      return client.authenticator().authenticate(route, userResponse);

    case HTTP_PERM_REDIRECT://307
    case HTTP_TEMP_REDIRECT://308
      // "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"
        //如果这不是一个GET 或者 HEAD 请求,那么浏览器禁止自动进行重定向
      if (!method.equals("GET") && !method.equals("HEAD")) {
//post禁止直接重定向,浏览器一般要求是弹出对话框,这里直接返回null,
//intercept的处理,是直接将response返回给客户端,
//客户自己去判断状态码,获取location,决定是不是再次发起请求。
        return null;
      }
 // 走到这一般是get与head请求返回的307,308,由于没有return与break所以
//走300,301,302,303的处理流程,也就是判断用户的是否允许重定向。
      // fall-through
    case HTTP_MULT_CHOICE:	//300
    case HTTP_MOVED_PERM:	//301
    case HTTP_MOVED_TEMP:	//302
    case HTTP_SEE_OTHER:	//303
      // Does the client allow redirects?
  //客户端是否允许重定向
      if (!client.followRedirects()) return null;
 // 新的临时性的URI 应当在响应的 Location 域中返回
//location 也就是重定向的地址。
      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());
  //是否scheme相同,客户端是否遵循SSL重定向
      if (!sameScheme && !client.followSslRedirects()) return null;

      // Most redirects don't include a request body.
  //重新构造request
      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();

    case HTTP_CLIENT_TIMEOUT:	//408  超时
      // 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;
      }
//服务器有没有返回多久以后重试,
//返回值大于0直接放弃自动重试。
      if (retryAfter(userResponse, 0) > 0) {
        return null;
      }
// 重试一次
      return userResponse.request();

    case HTTP_UNAVAILABLE://503服务不可用
      if (userResponse.priorResponse() != null
          && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
        // We attempted to retry and got another timeout. Give up.
//同样503也只允许重试一次。
        return null;
      }

      if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
        // specifically received an instruction to retry without delay
//服务器指定我们立即重试,也就是返回的重试时间是0。
//这个时候直接重试一次。
        return userResponse.request();
      }
  //放弃重试
      return null;

    default:
      return null;
  }
}

这里主要是根据响应码(code)和响应头(header),查看是否需要重定向,并重新设置请求,当然,如果是正常响应则直接返回Response停止循环,代码注释的已经非常清楚了,

服务器要求多久以后再次重试:
private int retryAfter(Response userResponse, int defaultDelay) {
  String header = userResponse.header("Retry-After");

  if (header == null) {
    return defaultDelay;
  }

  // https://tools.ietf.org/html/rfc7231#section-7.1.3
  // currently ignores a HTTP-date, and assumes any non int 0 is a delay
  if (header.matches("\\d+")) {
    return Integer.valueOf(header);
  }

  return Integer.MAX_VALUE;
}

结束

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OkHttp3是一个用于Android应用程序的网络请求框架,它提供了一个简单而强大的API,可以用来发送和接收HTTP请求。为了更好地使用OkHttp3来进行网络请求,我们可以创建一个OkHttp3工具类。 首先,我们需要在项目中添加OkHttp3的依赖。可以通过在build.gradle文件中引入以下依赖来实现: ``` implementation 'com.squareup.okhttp3:okhttp:4.9.2' ``` 然后,我们可以创建一个OkHttp3工具类,该工具类封装了发送和接收网络请求的相关方法。下面是一个简单的示例: ```java import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import java.io.IOException; public class OkHttp3Utils { private OkHttpClient client; public OkHttp3Utils() { client = new OkHttpClient(); } public void sendRequest(String url, Callback callback) { Request request = new Request.Builder() .url(url) .build(); client.newCall(request).enqueue(callback); } } ``` 在这个示例中,我们创建了一个OkHttpClient实例,并提供了一个sendRequest方法来发送网络请求。我们可以将此类实例化并调用sendRequest方法来发送GET或POST请求,并在回调中处理服务器的响应结果。 使用这样的工具类,我们可以更方便地管理和处理网络请求,从而提高Android应用程序的性能和可维护性。当然,还可以根据实际项目的需求来扩展和完善这个工具类,以满足更复杂的网络请求操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值