OkHttp原理分析(二)

前言

前面我们提到了Okhttp的五大拦截器,下面我们将分析每一个拦截器的具体作用。

一、RetryAndFollowUpInterceptor(重试和重定向拦截器)

重试和重定向截器顾名思义是用来负责请求的重试和重定向的。我们来看源码的实现:

1、重试

在RetryAndFollowUpInterceptor这个拦截器中本身对Request没有做什么特殊的处理,在源码中首先是开启了一个while(true循环),有两个关键点在两个catch 异常处执行了continue语句,继续执行while循环里的代码,说明这两种场景下需要重试。

 while (true) {
      transmitter.prepareToConnect(request);

      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } 
  //需要重试
    catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        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, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

我们来分析下需要重试的条件:当发生了RouteException 和IOException 之后,在catch异常处会根据相应的判断是否要continue重试。

  • RouteException 路由异常
    catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
    //路由异常,连接未成功,请求还没发出去,
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      
  • IOException IO异常
catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
//请求发出去了,但是和服务器通信失败了(Socket流正在读写数据的时候断开)
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      }

以上两个异常都是根据recover方法判断是否能够重试,如果返回true则表示能重试,否则直接抛出异常。

  • recover方法
  private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // The application layer has forbidden retries.
  //1、应用层禁止了重试,比如在Okhttpclient的时候设置了不允许重试。这种情况发生异常的时候,不会进行重  试
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
  //2、不能再次发送请求体
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // This exception is fatal.
  //3、这个异常是致命的,比如协议的异常
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt.
//4、没有可以用来连接的流程
    if (!transmitter.canRetry()) return false;

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

我们分析下recover方法不能重试的条件

  • 第一个判断表示应用层禁止了重试,比如我们在配置OkHttpClient的时候禁止了重试,设置 retryOnConnectionFailure;属性为false
    if (!client.retryOnConnectionFailure()) return false;
  • 第二个判断注释的意思是:不能再次发送请求体
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

主要是通过requestIsOneShot方法判断

  private boolean requestIsOneShot(IOException e, Request userRequest) {
    RequestBody requestBody = userRequest.body();
    return (requestBody != null && requestBody.isOneShot())
        || e instanceof FileNotFoundException;
  }

其中主要又是根据 requestBody.isOneShot()这个条件判断,大概意思是根据408、401、503、Retry-After: 0
这些条件判断这个请求是一次性。当满足这个条件的时候也说明不能重试。这里我也不是很理解。

   * 

This method returns false unless it is overridden by a subclass. * *

By default OkHttp will attempt to retransmit request bodies when the original request fails * due to a stale connection, a client timeout (HTTP 408), a satisfied authorization challenge * (HTTP 401 and 407), or a retryable server failure (HTTP 503 with a {@code Retry-After: 0} * header). */ public boolean isOneShot() { return false; }

  • 第三个异常的意思是这个异常是致命的,比如协议的异常。就不会进重试
   if (!isRecoverable(e, requestSendStarted)) return false;

具体又是通过调用isRecoverable方法来判断具体是那些协议是致命的。

 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.
//SSL握手异常,比如证书出现问题,不会重试
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
//SSL握手未授权,不能重试
    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;
  }

(1)如果是协议异常,不会进行重试(比如整个通信没有按照HTTP协议来通信)
(2)超时异常,由于网络造成的波动造成的超时,允许重试
(3)SSL握手失败,SSL验证失败,不会重试。前者是证书验证失败,后者可能压根没有证书。

  • 第四个判断意思是没有更多的路线,不会重试
    // No more routes to attempt.
    if (!transmitter.canRetry()) return false;

大概的意思是经过上面三个判断如果都通过了,那么就再判断是否有可用的路线,比如经过DNS解析域名可能返回多个IP,一个IP失败之后,尝试重试另外一个IP。
以上我们可以了解到OKHTTP进行重试的条件是非常苛刻的,一般是由于网络的波动。分别要经过4个大条件的判断。

2、重定向

我们 继续来看RetryAndFollowUpInterceptor下面的源码

 Route route = exchange != null ? exchange.connection().route() : null;
      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();
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;

当走过重试条件的代码之后,往下走,有两行比较关键的代码

Route route = exchange != null ? exchange.connection().route() : null;
      Request followUp = followUpRequest(response, route);

表示重定向,主要调用了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.
   */
  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) {
       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);

    .......
      default:
        return null;
    }
  }

判断重定向的代码非常多,最终目的就是要通过拿到一个Request,如果Request为空,就不需要重定向,如果Request不为空,则可以重定向。而里面的核心判断主要是通过服务器返回的响应码比如是3开头的,当服务器返回了一个location的响应头的时候,代表这是一个重定向的新地址。那么客户端就可以创建新的Request重定向到该地址。
而重定向不是无限制的重定向的,在接下来的源码中提现

     if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

当重定向次数大于MAX_FOLLOW_UPS = 20的时候,就不能再重定向了,抛出ProtocolException异常。

3、RetryAndFollowUpInterceptor拦截器小结

RetryAndFollowUpInterceptor拦截器主要是负责重试和重定向,本身对Request没有做其他的处理。

  • 重试的话会根据两个异常来判断RouteException和IOException,而他们最终都是通过调用recover方法来判断是否要重试。而主要的判断是用户禁止了重试、协议异常、RequestBody一次发送就不能重试。如果满足的话,在判断是否有可用的路线。如果有才能重试,如果没有也不能重试。整个重试的条件比较苛刻,一般是由于网络波动造成的超时,并且有多余的可用路线的情况下才能重试。
  • 重定向最终都是通过followUpRequest方法来获取一个重定向的Request,判断条件就比较多,里面的判断主要能重定向的就是服务器返回了一个3开头的状态码,并且返回了一个重定向地址Location。并且最大重定向次数为20

二、BridgeInterceptor(桥接拦截器)

在HTT协议中需要设置一些必须的请求头,因此在桥接拦截器中就是负责给我们补全这些请求头。比如设置请求内容长度、编码、gzip压缩、cookie等


image.png

桥接拦截器主要是将用户构建的Request转换成符合Http协议网络请求的Request,将符合网络规范的Request交接给下一个拦截器。
部分源码

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

三、CacheInterceptor(缓存拦截器)

缓存拦截器主要负责是否要写入缓存,请求的时候是否要使用缓存里的数据
核心源码

  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

  
    }

主要是根据networkRequest 和cacheResponse两个对象判断,分别意思是:是否需要网络请求和是否有缓存
具体判断根据以下表格

image.png

而这两个对象的诞生主要是根据CacheStrategy策略类来的

 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

最终都是通过下面这个方法来策略的

 /** Returns a strategy to use assuming the request can use the network. */
 // No cached response.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
...............

缓存的判断是非常复杂的,我自己也还不是很搞懂。。。

四、ConnectInterceptor(连接拦截器)

打开与目标服务器的连接。前面都是做准备的操作,到了连接拦截器那就是真正的进行网络连接了。
在连接拦截器中主要目的就是去查找或者创建一个与主机有效的连接。

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

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

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }
}

这个连接的创建或者查找主要是通过Transmitter类以下核心代码,

  Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if (exchange != null) {
        throw new IllegalStateException("cannot make a new request because the previous response "
            + "is still open: please call response.close()");
      }
    }

    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }

我们看到Transmitter封装了一个连接池connectionPool。然后调用exchangeFinder.find方法去查找
最终调用到了ExchangeFinder的findConnection方法

 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
....

最终就是通过操作这个连接池看是否是从连接池中拿连接还是创建新的连接
接下来我们来简单分析连接池的实现。RealConnectionPool

 public RealConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

构造方法中规定了连接池闲置连接数量的大小,以及存活时间

  • 将连接加入连接池
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

其实就是将连接加入到connections集合中,并且在第一次加入连接的时候开启了一个线程池任务呢,来维护连接池里的数量,及时清理操作。

 
 executor.execute(cleanupRunnable);
  private final Runnable cleanupRunnable = () -> {
    while (true) {
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
//典型的生产者消费者模式
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };
  • 移除连接
   */
  boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewExchanges || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }
  • 获取连接
    通过封装的Address尝试从连接池中获取连接。
  if (result == null) {
        // Attempt to get a connection from the pool.
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        } else if (nextRouteToTry != null) {
          selectedRoute = nextRouteToTry;
          nextRouteToTry = null;
        } else if (retryCurrentRoute()) {
          selectedRoute = transmitter.connection.route();
        }
  /**
   * Attempts to acquire a recycled connection to {@code address} for {@code transmitter}. Returns
   * true if a connection was acquired.
   *
   * 

If {@code routes} is non-null these are the resolved routes (ie. IP addresses) for the * connection. This is used to coalesce related domains to the same HTTP/2 connection, such as * {@code square.com} and {@code square.ca}. */ boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter, @Nullable List routes, boolean requireMultiplexed) { assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { if (requireMultiplexed && !connection.isMultiplexed()) continue; if (!connection.isEligible(address, routes)) continue; transmitter.acquireConnectionNoEvents(connection); return true; } return false; }

连接拦截器小结

连接拦截器主要是负责提供一个有效的Socket连接,在这个连接上进行HTTP数据的收发,而这个连接通过RealConnection类来封装,而在连接拦截器中通过一个Transmitter类的一个RealConnectionPool连接池来管理连接。RealConnectionPool连接池规定了最大闲置连接大小,以及连接的存活时间。连接池的加入连接和移除连接通过典型的生产者消费者模型。

五、CallServerInterceptor(请求服务器拦截器)

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

    long sentRequestMillis = System.currentTimeMillis();

    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (request.body().isDuplex()) {
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
          // from being reused. Otherwise we're still obligated to transmit the request body to
          // leave the connection in a consistent state.
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      exchange.noRequestBody();
    }

    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }

    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }

    exchange.responseHeadersEnd(response);

    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }

CallServerInterceptor请求服务器拦截器拿到上一个拦截器的连接之后负责和服务器真正的通信,收发数据。然后将响应结果返回到上一个拦截器,依次往上传递。最终传到用户层。

五、拦截器总结

  • 重试和重定向拦截器
    主要作用是当抛出IO和路线异常的时候,根据判断是否要重试,一般主要判断就是用户是否禁止了重试、是否是协议异常、是否有多余的路线等等
    而重定向主要是根据服务器的响应码中是否是3开头的以及是否返回了重定向的地址来决定是否重定向。并且重定向的最大次数是限制的,20次。
  • 桥接拦截器
    桥接拦截器主要是设置一些HTTP规范的请求头比如HOST,使得我们的请求是符合HTTP协议的。并添加一些默认的行为比如GZIP压缩。
  • 缓存拦截
    缓存拦截器主要是作用是决定是否需要缓存到本地和是否需要从本地读取缓存
  • 连接拦截器
    连接拦截器主要作用是提供一个有效的Socket连接,连接的管理通过一个连接池来控制。 并获得Socket的流,获得之后不做任何操作
  • 请求服务器拦截器
    真正的与服务器通信,根据上面连接拦截器提供的 Socket流向服务器发送数据并解析响应的数据。然后返回到上一个拦截器。最后每个拦截器依次返回最终返回到用户端。

六、OkHttp的优点与小结

  • 支持Http1、Http2、Quic以及WebSocket
  • 连接池服用,减少延迟,避免每次请求都创建一个新的连接,每一个新的连接需要TCP三次握手。
  • 支撑GZIP压缩,减少数据量
  • 支撑缓存
  • 请求失败有重试机制和重定向机制
  • OkHttp是基于Socket套接字实现的一个HTTP应用层协议。直接与TCP传输层协议打交道。

你可能感兴趣的:(OkHttp原理分析(二))