OkHttp原理解析之重试及重定向拦截器

本文介绍OkHttp中的RetryAndFollowUpInterceptor拦截器如何处理重试与重定向逻辑。针对RouteException和IOException异常,通过recover方法判断是否重试;对于特定响应码,如301、302等,通过followUpRequest方法实现重定向。
摘要由CSDN通过智能技术生成

一、重试及重定向拦截器

第一个拦截器:RetryAndFollowUpInterceptor,主要就是完成两件事情:重试与重定向。

重试

在这里插入图片描述
请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。

RouteException:

catch (RouteException e) {
	//todo 路由异常,连接未成功,请求还没发出去
    if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
    	throw e.getLastConnectException();
    }
    releaseConnection = false;
    continue;
} 

IOException:

catch (IOException e) {
	//todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
    // ConnectionShutdownException只对HTTP2存在。假定它就是false
	boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
	if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
		releaseConnection = false;
		continue;
} 

两个异常都是根据recover 方法判断是否能够进行重试,如果返回true,则表示允许重试。

private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
	streamAllocation.streamFailed(e);
	//todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
	if (!client.retryOnConnectionFailure()) return false;
	//todo 2、由于requestSendStarted只在http2的io异常中为true,http1中一定是false,先不管http2
	if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
		return false;

	//todo 3、判断是不是属于可重试的异常
	if (!isRecoverable(e, requestSendStarted)) return false;

	//todo 4、判断是否存在更多的路由,如果域名可以解析出多个ip或者配置了多个http代理,那么hasMoreRoutes就为true
	if (!streamAllocation.hasMoreRoutes()) return false;

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

所以首先使用者在不禁止重试(可通过retryOnConnectionFailure(false)方法设置)的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在isRecoverable中进行判断:

/**
 * 判断是不是属于可重试的异常
 * @param e
 * @param requestSendStarted
 * @return
 */
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
	// 出现协议异常,不能重试
    if (e instanceof ProtocolException) {
      return false;
    }

	// requestSendStarted认为它一直为false(不管http2),异常属于socket超时异常,直接判定可以重试
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    // SSL握手异常中,证书出现问题,不能重试
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    // SSL握手未授权异常 不能重试
    if (e instanceof SSLPeerUnverifiedException) {
      return false;
    }
    return true;
}

1、协议异常,如果是那么直接判定不能重试;(协议异常是指你的请求或者服务器的响应本身就存在问题,没有按照http协议来定义数据,再重试也没用)
比如在发起Http请求时,没有携带Host:xxxxx,这就是一个协议异常。
2、超时异常,可能由于网络波动造成了Socket管道的超时,那么肯定重试,因为本次超时异常很有可能是由于短暂网络波动导致的超时异常。(后续还会涉及到路由)

3、SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确,那么不能重试。

经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有更多的可用路由路线来进行连接。简单来说,比如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。

重定向

在这里插入图片描述

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重定向,如果需要则发起新的请求。重定向的判断位于followUpRequest方法

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

    final String method = userResponse.request().method();
    switch (responseCode) {
      // 407 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,给代理服务器授权
      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);
      // 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” 
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向 
      // 307 临时重定向
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303 
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 如果用户不允许重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 从响应头取出location 
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 根据location 配置新的请求 url
        HttpUrl url = userResponse.request().url().resolve(location);
        // 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
        if (url == null) return null;
        // 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        Request.Builder requestBuilder = userResponse.request().newBuilder();
		/**
		 *  重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,
		 *  即只有 PROPFIND 请求才能有请求体
		 */
		//请求不是get与head
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
           // 除了 PROPFIND 请求之外都改成GET请求
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          // 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // 在跨主机重定向时,删除身份验证请求头
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      // 408 客户端请求超时 
      case HTTP_CLIENT_TIMEOUT:
        // 408 算是连接失败了,所以判断用户是不是允许重试
       	if (!client.retryOnConnectionFailure()) {
			return null;
		}
		// UnrepeatableRequestBody实际并没发现有其他地方用到
		if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
			return null;
		}
		// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求了
		if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
			return null;
		}
		// 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
		if (retryAfter(userResponse, 0) > 0) {
			return null;
		}
		return userResponse.request();
	   // 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
 	   case HTTP_UNAVAILABLE:
		if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
         	return null;
         }

         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
         	return userResponse.request();
         }

         return null;
      default:
        return null;
    }
}

整个是否需要重定向的判断内容很多,记不住,这很正常,关键在于理解他们的意思。如果此方法返回空,那就表示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的Request,但是需要注意的是,我们的followup在拦截器中定义的最大次数为20次。

总结

本拦截器是整个责任链中的第一个,这意味着它会是首次接触到Request与最后接收到Response的角色,在这个拦截器中主要功能就是判断是否需要重试与重定向。

重试的前提是出现了RouteException或者IOException。一但在后续的拦截器执行过程中出现这两个异常,就会通过recover方法进行判断是否进行连接重试。

重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用followUpRequest根据Response 的响应码(当然,如果直接请求失败,Response都不存在就会抛出异常)。followup最大发生20次。

好的,针对你的问题,我可以给出以下的基于 OkHttp重试拦截器实现: ```java public class RetryInterceptor implements Interceptor { private final int maxRetryCount; private final long retryInterval; public RetryInterceptor(int maxRetryCount, long retryInterval) { this.maxRetryCount = maxRetryCount; this.retryInterval = retryInterval; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = null; IOException exception = null; int tryCount = 0; while (tryCount <= maxRetryCount) { try { response = chain.proceed(request); if (response.isSuccessful()) { return response; } } catch (IOException e) { exception = e; } tryCount++; if (tryCount <= maxRetryCount) { try { Thread.sleep(retryInterval); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } } if (exception != null) { throw exception; } throw new IOException("Request failed after " + maxRetryCount + " attempts."); } } ``` 在这个拦截器中,我们可以通过传入最大重试次数和重试间隔时间来配置拦截器。当请求失败时,会在一定时间后重新尝试请求,直到达到最大重试次数或请求成功为止。如果最大重试次数用完后仍然失败,则抛出异常。 使用这个拦截器需要在 OkHttpClient 中添加: ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new RetryInterceptor(3, 1000)) .build(); ``` 这样就可以在 OkHttp 的请求中添加重试功能了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值