OkHttp源码解析(二)重定向

Android框架学习

Posted by Cc1over on January 10, 2020

OkHttp源码解析(二)重定向

RetryAndFollowUpInterceptor-> intercept

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    // 获取Transmitter
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      // 准备连接  
      transmitter.prepareToConnect(request);
      // 处理取消事件
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }
      
      Response response;
      boolean success = false;
      try {
        // 从下层获取Response  
        response = realChain.proceed(request, transmitter, null);
        // 标记获取成功  
        success = true;
      } catch (RouteException e) {
        // 判断是否满足重定向条件,满足则重试,不满足就抛异常
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // 与服务器连接失败,检查是否满足重定向条件,满足则重试,不满足就抛异常
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // 检测到其他未知异常,则释放连接和资源
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }     

      // 绑定上一个Response,指定body为空
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }
     
      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      // 根据响应码处理请求,返回Request不为空时则进行重定向处理
      Request followUp = followUpRequest(response, route);

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }
      // 限制重定向次数最多为20,超过则抛出异常
      if (++followUpCount > 20) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;
    }
  }

首先看到的是Transmitter的处理预备连接处理,这里其实会对url进行判断,如果存在相同主机号,端口号,协议的url,那么就可以复用连接了

主要的逻辑就是从下层拿到一个Response,然后判断是否满足重定向的条件,如果满足就重试,如果不满足就直接抛出异常

然后会根据上一步拿到的Response绑定上一个Response,然后转调followUpRequest方法根据相应拿到一个新的Request

而当重试的次数超过最大次数,也就是20,就会抛出异常

RetryAndFollowUpInterceptor-> followUp

 private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      // 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);
      // 401
      case HTTP_UNAUTHORIZED:
        // 身份认证    
        return client.authenticator().authenticate(route, userResponse);
      // 308 
      case HTTP_PERM_REDIRECT:
      // 307      
      case HTTP_TEMP_REDIRECT:
        // 如果是307、308 这两种状态码 
        // 不对GET、HEAD 以外的请求重定向
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      // 300       
      case HTTP_MULT_CHOICE:	
      // 301
      case HTTP_MOVED_PERM:
      // 302
      case HTTP_MOVED_TEMP:
      // 303
      case HTTP_SEE_OTHER:
        // 客户端关闭重定向
        if (!client.followRedirects()) return null;
        // 从响应头中获取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());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // 重建一个新的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.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();
      // 408
      case HTTP_CLIENT_TIMEOUT:
      // 需要发送一次相同的请求
      // ......      
      // 503
      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;
    }
  }

主要的逻辑就是根据不同的响应码做不同的处理

包含了407的代理认证和401的身份认证

而307、308两种响应码,只对get、head两种请求方式做重定向

然后针对30x的响应码其实处理就是从Response中拿到Location字段,然后构建一个新的请求重新发送出去

总结

其实感觉Ok的实现就是通过拦截器机制由上往下,由下往上的自己实现Http协议,看到Ok就顺便回顾Http协议中的知识

重定向这个拦截器主要是针对Http提供的重定向这种功能的具体实现,通过在服务器的响应中设置3xx的响应码来触发客户端的重定向操作,并且在相应头中设置了一个Location填写所指向的url,然后重新请求得到所需的数据

针对不同的响应码处理为:

  • 301 Moved Permanently:表示请求的资源已经永久转移成了Location字段中的URL,客户端不应该再使用旧的URL,而是应该使用Location字段中的URL。搜索引擎在发现该状态码后也会触发更新操作,在索引库中修改其对应的数据
  • 302 Found:表示请求的资源暂时转移成了Location字段中的URL,客户端在这次请求中应该去请求Location字段中的URL,不过在以后的请求中可能仍然会使用现在的URL。搜索引擎发现该状态码后不会更新数据
  • 303 See Other:含义同302,但是该状态码会强制将非GET请求的方法变成GET方法(请求主体会丢失)
  • 304 Not Modified:这个状态码在上一篇HTTP缓存机制中提到过,这个状态码实际是用于HTTP缓存使用的,不过它其实同样也是重定向的含义:服务器指示客户端将当前的请求重定向到缓存中的资源数据中。因为是指示客户端使用缓存,所以这个状态码是不会返回Location字段的
  • 307 Temporary Redirect:含义同302,不过区别在于这个状态码不会改变请求的方法和实体数据
  • 308 Permanent Redirect:含义同301,不过区别在于这个状态码不会改变请求的方法和实体数据

在上面的状态码介绍中提到了307和302, 308和301的区别,具体说来,301和302在大多数情况下不会改变请求的方法和实体,不过在某些旧的用户代理上,可能会发生意想不到的改变,除了GET和HEAD方法以外的请求方法在使用301和302进行重定向时可能会被改变成GET方法,并且请求的实体也会被清空。因此,如果需要在使用非GET方法的请求中进行重定向的话,那么307和308是要优于301和302的。