okhttp3

okhttp分享三:RetryAndFollowUpInterceptor与BridgeInterceptor

之前两篇文章分析了okhttp的基本使用、线程及任务分发,我们知道,okhttp的发送请求及接收响应都是通过其interceor链实现的,现在我们就开始从源码分析每个拦截器的作用,按顺序首先我们看下最前面的两个拦截器RetryAndFollowUpInterceptor与BridgeInterceptor。

一、RetryAndFollowUpInterceptor

从名字可以看出,这个拦截器主要处理重试逻辑,重试发生的场景有两种,一是返回码表明需要重试(如302),二是请求发送或接收过程中产生非致命的异常时。

1.1、http返回码

我们知道http返回码中1xx表示信息提示,2xx表示成功,3xx表示重定向(实际上304也可以理解为一种重定向),4xx表示客户端请求错误,5xx表示服务器错误。这边我们着重看一下与重试相关错误码。

  • 300 Multiple Choices

被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。

  • 301 redirect 永久性转移(Permanently Moved)

永久是指原来访问的资源已经永久删除,客户端应该根据新的URI访问重定向。

  • 302 redirect 暂时性转移(Temporarily Moved )

临时是指访问的资源可能暂时先用location的URI访问,但旧资源还在的,下次你再来访问的时候可能就不用重定向了。

  • 303 See Other

303 的出现其实是为了规范301、302的,一开始301、302的是不允许在重定向的时候改变请求方式的(get改为post),但是各大浏览器在使用过程中并没有遵循这一规定。因此http又添加了303,303实际上就是302,只不过303可以改变请求方式。但是实际上并没有很多浏览器按照这一规定执行,于是http只能将错就错推出了307、308,307对应302,308对应301。区别是301、302由原来的不可以改变请求方式改为可以,307、308则表示重定向过程中不可以改变请求方式。

  • 307 Temporary Redirect(临时重定向)

与302对应,但是不可以在重定向过程中修改请求方式

  • 308 Permanent Redirect (永久重定向)

与301对应,但是不可以在重定向过程中修改请求方式

  • 401 Unauthorized

当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。

  • 407 Proxy Authentication Required

与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个 Proxy-Authenticate 用以进行身份询问。客户端可以返回一个 Proxy-Authorization 信息头用以验证。

  • 408 Request Timeout

请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。
408请求超时错误是一个 HTTP状态代码,这意味着你的请求发送到该网站服务器(即请求加载网页)花的时间比该网站的服务器准备等待的时间要长。这个时候客户端与服务器的链接已经建立,但没发请求。
HTTP请求的步骤:
1、建立链接、套接字
2、通过该套接字写 HTTP 数据流。
3、从您的Web服务器接受响应的 HTTP 数据流。
408发生的时机:
408发生预示着1-2之间的处理超时。通常套接字开通和通过该套接字书写入 HTTP 数据流之间只有很短的时间间隔(毫秒)。

  • 503 Service Unavailable

由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间。如果没有给出这个 Retry-After 信息,那么客户端应当以处理500响应的方式处理它。

1.2、RetryAndFollowUpInterceptor源码分析

从之前的文章我们知道,interceptor核心方法是其intercept方法,我们就直接看这个方法

/**
   * 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.
   */
//单个请求最大恢复次数:
  private static final int MAX_FOLLOW_UPS = 20;

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    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 {
        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.
        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();
        }
      }

      // 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 {
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

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

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

简单整理下流程

  • 1、根据url创建一个Address对象,初始化一个Socket连接对象。
  • 2、用前面创建的address作为参数去实例化StreamAllocation(后面会着重讲这个类,大家可以暂时将它理解为用于维护网络连接、及请求流)
  • 3、开启一个while(true)循环,执行如下逻辑逻辑
    • 3.1 如果取消,释放资源并抛出异常,结束流程
    • 3.2 执行下一个拦截器,一般是BridgeInterceptor
    • 3.3 如果发生异常,走到catch里面,判断是否继续请求,不继续请求则退出
    • 3.4 如果priorResponse不为空,则说明前面已经获取到了响应,这里会结合当前获取的Response和先前的Response
    • 3.5 调用followUpRequest查看响应是否需要重定向,如果不需要重定向则返回当前请求
    • 3.6 检查是否有相同的链接,是:释放,重建创建
    • 3.7 重新设置request,并把当前的Response保存到priorResponse,继续while循环

我们看到3.5,followUpRequest方法,这个方法就是按照http协议根据返回的code来做相应的处理逻辑。我们来看下代码

 /**
   * 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.
   */
  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");
        }
        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"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // 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;

        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;

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

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

这段代码我整理了一张代码逻辑图,直接看图说


http重定向.jpg

followUpRequest是根据返回的httpcode来判断是否需要重试,我们上文说到,RetryAndFollowUpInterceptor还会处理请求发送过程中出现的异常,判断是否可以重试。这块逻辑在intercept()方法中存在于catch里面,其核心方法为recover()方法,我们来简单看下这个方法。

/**
   * Report and attempt to recover from a failure to communicate with a server. Returns true if
   * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
   * be recovered if the body is buffered or if the failure occurred before the request has been
   * sent.
   */
  private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt.
    if (!streamAllocation.hasMoreRoutes()) return false;

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

  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // If there was a protocol problem, don't recover.
    if (e instanceof ProtocolException) {
      return false;
    }

    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // we should try the next route (if there is one).
    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.
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      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.
    return true;
  }

看上面代码可以这样理解:判断是否可以恢复如果下面几种条件符合,则返回true,代表可以恢复,如果返回false,代表不可恢复。具体逻辑不详细说了,简单说下不可恢复的情况

  • 用户配置不可重试
  • 请求出错不能重试
  • 特定的Exception不可重试
    • 协议错误(ProtocolException)
    • 中断异常(InterruptedIOException)
    • SSL握手异常(SSLHandshakeException && CertificateException)
    • certificate pinning异常(SSLPeerUnverifiedException)
  • 没用更多route可选择,不可重复

总结来说,RetryAndFollowUpInterceptor的执行逻辑为通过一个while循环获取response,若成功则通过followUpRequest方法,根据http code判断是否需要重定向,若需要则将followUpRequest方法返回的request通过下一个while循环发送出去,若不需要则直接返回response,推出循环;若在发送请求流程中出现exception则通过recover方法判断是否可以重试,若可以则重新发送,若不可则作失败处理。

二、BridgeInterceptor

这个Interceptor比较简单但是确非常重要,http协议的一些必须的请求、响应头都是通过这个拦截器添加、解析的,如果缺失这个过程,服务器与客户端都无法正确读取对方发送的内容。照例看代码

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

    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }

代码比较简单,简单说下逻辑

  • 添加必须的请求头:Content-Type、Content-Length、Transfer-Encoding、Host、Connection、Accept-Encoding、User-Agent。
  • 根据用户设定的Cookie策略向请求头中加入cookie
  • 构建好request,调用proceed进入后续拦截器链,返回response
  • 根据响应跟新cookie
  • 根据返回响应头的Content-Encoding字段,判断报文是否为gzip压缩过的,若是则会先进行解压缩,移除响应中的header Content-Encoding和Content-Length,构造新的响应返回。否则直接返回response。

你可能感兴趣的:(okhttp3)