OKHTTP系列(四)--拦截器之RetryAndFollowUpInterceptor重定向拦截器

在介绍RetryAndFollowUpInterceptor拦截器之前,先了解一下重定向是什么!!!

一、http协议中的重定向简单了解

原理:

在 HTTP 协议中,重定向操作由服务器通过发送特殊的响应(即 redirects)而触发。HTTP 协议的重定向响应的状态码为 3xx 。浏览器在接收到重定向响应的时候,会采用该响应提供的新的 URL ,并立即进行加载;大多数情况下,除了会有一小部分性能损失之外,重定向操作对于用户来说是不可见的。

  • client:向server发送一个请求,要求获取一个资源
  • server:接收到这个请求后,发现请求的这个资源实际存放在另一个位置于是server在返回的response header的Location字段中写入那个请求资源的正确的URL,并设置reponse的状态码为30x
  • client:接收到这个response后,发现状态码为重定向的状态吗,就会去解析到新的URL,根据新的URL重新发起请求

不同类型的重定向映射可以划分为三个类别:永久重定向、临时重定向和特殊重定向。

1.1、永久重定向

这种重定向操作是永久性的。它表示原 URL 不应再被使用,而应该优先选用新的 URL。搜索引擎机器人会在遇到该状态码时触发更新操作,在其索引库中修改与该资源相关的 URL 。

编码含义处理方法典型应用场景
301Moved PermanentlyGET 方法不会发生变更,其他方法有可能会变更为 GET 方法。该规范无意使方法发生改变,但在实际应用中用户代理会这么做。 308 状态码被创建用来消除在使用非 GET 方法时的歧义行为。网站重构。
308Permanent Redirect方法和消息主体都不发生变化。网站重构,用于非GET方法。(with non-GET links/operations)

1.2、临时重定向

有时候请求的资源无法从其标准地址访问,但是却可以从另外的地方访问。在这种情况下可以使用临时重定向。搜索引擎不会记录该新的、临时的链接。在创建、更新或者删除资源的时候,临时重定向也可以用于显示临时性的进度页面。

编码含义处理方法典型应用场景
302FoundGET 方法不会发生变更,其他方法有可能会变更为 GET 方法。该规范无意使方法发生改变,但在实际应用中用户代理会这么做。 307 状态码被创建用来消除在使用非 GET 方法时的歧义行为。由于不可预见的原因该页面暂不可用。在这种情况下,搜索引擎不会更新它们的链接。
303See OtherGET 方法不会发生变更,其他方法会变更为 GET 方法(消息主体会丢失)。用于PUT 或 POST 请求完成之后进行页面跳转来防止由于页面刷新导致的操作的重复触发。
307Temporary Redirect方法和消息主体都不发生变化。由于不可预见的原因该页面暂不可用。在这种情况下,搜索引擎不会更新它们的链接。当站点支持非 GET 方法的链接或操作的时候,该状态码优于 302 状态码。

 

1.3、特殊重定向

除了上述两种常见的重定向之外,还有两种特殊的重定向。304 (Not Modified,资源未被修改)会使页面跳转到本地陈旧的缓存版本当中(该缓存已过期(?)),而 300 (Multiple Choice,多项选择) 则是一种手工重定向:以 Web 页面形式呈现在浏览器中的消息主体包含了一个可能的重定向链接的列表,用户可以从中进行选择。

编码含义典型应用场景
300Multiple Choice不会太多:所有的选项在消息主体的 HTML 页面中列出。也可以返回 200 OK 状态码。
304Not Modified缓存刷新:该状态码表示缓存值依然有效,可以使用。

 

二、常用状态码

  • 重定向最常用为301,也有303,
  • 临时重定向用302,307

三、重定向和转发的区别

  • 转发是服务器行为,重定向是客户端行为。
  • 转发是一次请求,重定向是至少两次请求。
  • 转发之后地址栏上的地址不会变化,还是第一次请求的地址;重定向之后地址栏上的地址会发生变化,变化成第二次请求的地址。

 

转发过程:

  1. 客户浏览器发送http请求;
  2. web服务器接受此请求;
  3. 调用内部的一个方法在容器内部完成请求处理和转发动作;
  4. 将目标资源发送给客户;

在这里,转发的路径必须是同一个web容器下的url,其不能转向到其他的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。

重定向过程:

  1. 客户浏览器发送http请求
  2. web服务器接受后发送302状态码响应及对应新的location给客户浏览器
  3. 客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址
  4. 服务器根据此请求寻找资源并发送给客户。

在这里,location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的。

举个栗子:

假设你想找人帮忙解决一个事情

重定向:你去A家找A帮忙,A说:“这个忙我帮不了你,你去找B吧,他能帮你”,然后你离开了A家,去B家找B。

转发:你去A家找A帮忙,A说:“这个忙我帮不了你,但是我的一个朋友B能帮你,你先坐一下,我联系下他,让他过来一下帮你解决”

四、RetryAndFollowUpInterceptor拦截器

  • 负责失败重试以及重定向,处理了一些连接异常以及重定向。

我们先来看一下重定向拦截器的源码

4.1、intercept()代码

public final class RetryAndFollowUpInterceptor implements Interceptor {
    /**
     * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
     * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
     */
//我们应该尝试多少次重定向和验证? 
//Chrome遵循21次重定向; Firefox,curl和wget遵循20; Safari跟随16; 和HTTP / 1.0建议5。
    private static final int MAX_FOLLOW_UPS = 20;

    @Override public Response intercept(Chain chain) throws IOException {
//1 、Request阶段,该拦截器在Request阶段负责做的事情
        Request request = chain.request();
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Call call = realChain.call();
        EventListener eventListener = realChain.eventListener();

//1. 构建一个StreamAllocation对象,StreamAllocation相当于是个管理类,维护了

//Connections、Streams和Calls之间的管理,该类初始化一个Socket连接对象,获取输入/输出流对象。

        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
                createAddress(request.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;

        int followUpCount = 0;
        Response priorResponse = null;
        while (true) {
            if (canceled) {
                streamAllocation.release();
                throw new IOException("Canceled");
            }

            Response response;
            boolean releaseConnection = true;
            try {
//2、 调用RealInterceptorChain.proceed(),其实是在递归调用下一个拦截器的intercept()方法
//2. 继续执行下一个Interceptor,即BridgeInterceptor
                response = realChain.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.
//3. 抛出异常,则检测连接是否还可以继续。
                if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                    throw e.getFirstConnectException();
                }
                releaseConnection = false;
                continue;
            } catch (IOException e) {
                // An attempt to communicate with a server failed. The request may have been sent.
// 和服务端建立连接失败
                boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
                if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
                releaseConnection = false;
                continue;
            } finally {
                // We're throwing an unchecked exception. Release any resources.
//检测到其他未知异常,则释放连接和资源
                if (releaseConnection) {
                    streamAllocation.streamFailed(null);
                    streamAllocation.release();
                }
            }
//构建响应体,这个响应体的body为空。
            // 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;
            try {
//4。根据响应码处理请求,返回Request不为空时则进行重定向处理。
                followUp = followUpRequest(response, streamAllocation.route());
            } catch (IOException e) {
                streamAllocation.release();
                throw e;
            }

            if (followUp == null) {
                if (!forWebSocket) {
                    streamAllocation.release();
                }
//3 、Response阶段,完成了该拦截器在Response阶段负责做的事情,然后返回到上一层的拦截器。
                return response;
            }

            closeQuietly(response.body());
//重定向的次数不能超过20次
            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;
        }
    }
}

我们先来看看整个方法的流程:

  • 构建一个StreamAllocation对象,StreamAllocation相当于是个管理类,维护了Connections、Streams和Calls之间的管理,该类初始化一个Socket连接对象,获取输入/输出流对象。
  • 继续执行下一个Interceptor,即BridgeInterceptor
  • 抛出异常,则检测连接是否还可以继续,以下情况不会重试:
    • 客户端配置出错不再重试
    • 出错后,request body不能再次发送
    • 发生以下Exception也无法恢复连接:
      • ProtocolException:协议异常
      • InterruptedIOException:中断异常
      • SSLHandshakeException:SSL握手异常
      • SSLPeerUnverifiedException:SSL握手未授权异常
    • 没有更多线路可以选择
  • 根据响应码处理请求,返回Request不为空时则进行重定向处理,由followUpRequest()方法完成,重定向的次数不能超过20次。

接下来我们在看一下StreamAllocation类的作用

4.2、StreamAllocation类的作用(后面再做详细介绍)

作用:协调了三个实体类的关系(具体作用后面再做介绍,此处注意介绍拦截器)

  • Connections:连接到远程服务器的物理套接字,这个套接字连接可能比较慢,所以它有一套取消机制。
  • Streams:定义了逻辑上的HTTP请求/响应对,每个连接都定义了它们可以携带的最大并发流,HTTP/1.x每次只可以携带一个,HTTP/2每次可以携带多个。
  • Calls:定义了流的逻辑序列,这个序列通常是一个初始请求以及它的重定向请求,对于同一个连接,我们通常将所有流都放在一个调用中,以此来统一它们的行为。

最后是根据响应码来处理请求头,由followUpRequest()方法完成,具体方法如下:

4.3、followUpRequest()方法、

/**
 * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
 * either add authentication headers, follow redirects or handle a client request timeout. If a
 * follow-up is either unnecessary or not applicable, this returns null.
 */
//计算出响应接收{@code userResponse}的HTTP请求。 
//这将添加身份验证标头,遵循重定向或处理客户端请求超时。 如果后续操作不必要或不适用,则返回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) {
//HTTP状态码407:需要代理身份验证。
    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);
//HTTP状态码401:未授权。
    case HTTP_UNAUTHORIZED:
      return client.authenticator().authenticate(route, userResponse);
//数字状态码,307:临时重定向。
//如果收到307或308状态代码以响应GET或HEAD以外的请求,则用户代理不得自动重定向请求
    case HTTP_PERM_REDIRECT:
    case HTTP_TEMP_REDIRECT:
      // "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"
      if (!method.equals("GET") && !method.equals("HEAD")) {
        return null;
      }
//HTTP状态码300:多个选项。
      // fall-through 失败
    case HTTP_MULT_CHOICE:
//HTTP状态代码301:永久移动。
    case HTTP_MOVED_PERM:
//HTTP状态代码302:临时重定向。
    case HTTP_MOVED_TEMP:
//HTTP状态代码303:参见其他
    case HTTP_SEE_OTHER:
      // Does the client allow redirects?
//客户端在配置中是否允许重定向
      if (!client.followRedirects()) return null;

      String location = userResponse.header("Location");
      if (location == null) return null;
      HttpUrl url = userResponse.request().url().resolve(location);

      // Don't follow redirects to unsupported protocols.
// url为null,不允许重定向
      if (url == null) return null;

      // If configured, don't follow redirects between SSL and non-SSL.
//查询是否存在http与https之间的重定向
      boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
      if (!sameScheme && !client.followSslRedirects()) return null;

      // Most redirects don't include a request body.
//大多数重定向都不包含请求主体
      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'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;
      }

      if (retryAfter(userResponse, 0) > 0) {
        return null;
      }

      return userResponse.request();
//HTTP状态代码408:请求超时
    case HTTP_UNAVAILABLE:
      if (userResponse.priorResponse() != null
          && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
        // We attempted to retry and got another timeout. Give up.
//我们试图重试,结果又超时了。放弃。
        return null;
      }

      if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
        // specifically received an instruction to retry without delay
//特别收到指示重试,不得延误
        return userResponse.request();
      }

      return null;

    default:
      return null;
  }
}

五、重定向的关闭

  • okhttp重定向功能默认是开启的,可以选择关闭,然后去实现自己的重定向功能。
new OkHttpClient().newBuilder()
                              .followRedirects(false)  //禁制OkHttp的重定向操作,我们自己处理重定向
                              .followSslRedirects(false)//https的重定向也自己处理
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值