吊打面試官——史上最詳細【OkHttp】四(完結篇)

簡介:大三學生黨一枚!主攻Android開發,對於Web和後端均有了解。
語錄取乎其上,得乎其中,取乎其中,得乎其下,以頂級態度寫好一篇的博客。

上一篇博客我們介紹了前三個攔截器,其中比較有難度的就是CacheInterceptor攔截器,它的底層是基於DiskLruCache的,面試也有可能會被問到原理!本篇繼續介紹剩下的兩種攔截器,ConnectInterceptorCallServerInterceptor攔截器。開始學習!
在這裏插入圖片描述

@TOC

一.ConnectInterceptor

1.1 源碼分析

@Override 
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //streamAllocation是在RetryAndFollowupInterceptor中創建的
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //重點是這句,streamAllocation.newStream()獲取可用的connection
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    //獲取一個物理連接,提供給下一個攔截器進行IO操作
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

看起來ConnectInteceptor內部很簡單,但是並非如此,只是把大部分方法都進行了封裝。我們再深入研究一下他究竟是如何獲取可用連接的!

 public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
    //查詢一條可用連接,findHealthyConnection繼續深入
      RealConnection resultConnection = 
      findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
          //根據可用連接創建HttpCodec
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

findHealthyConnection方法

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
    //繼續調用findConnection查找可用連接,而且是while循環,也就是一定會找到一條連接纔會返回
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();//不健康怎麼做
        continue;
      }

      return candidate;
    }
  }

接着看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;
    Connection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new streams.
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
      //首先判斷之前這條stream的connection是否可用,這種情況對應重試,也就是第一次請求已經把連接建立好了,重試就無需要重新建立,直接可以複用
        // We had an already-allocated connection and it's good.
        result = this.connection;
        releasedConnection = null;
      }
      if (!reportedAcquired) {
        // If the connection was never reported acquired, don't report it as released!
        releasedConnection = null;
      }

      if (result == null) {
      //如果之前沒有建立stream的話,就去連接池進行第一次獲取
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          //獲取到了直接返回
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
      // If we found an already-allocated or pooled connection, we're done.
      return result;
    }

    //能夠來到這裏,說明第一次獲取連接失敗了,對路由進行處理後再次
    //在連接池中進行獲取
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    synchronized (connectionPool) {
    //爲什麼對路由進行處理後就可能會獲取到呢?
    //官方的理由是: This could match due to connection coalescing.
      if (canceled) throw new IOException("Canceled");

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        List<Route> routes = routeSelection.getAll();
        for (int i = 0, size = routes.size(); i < size; i++) {
          Route route = routes.get(i);
          //第二次去連接池中進行獲取
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }

      if (!foundPooledConnection) {
      //如果還沒有獲取到就創建一個新的連接
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        //創建一個新的連接
        result = new RealConnection(connectionPool, selectedRoute);
        //更新引用計數,方便後面回收
        acquire(result, false);
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }
//新建立的連接要進行TCP握手和TLS握手
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      //把新建立的連接放入連接池中
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
      //如果多路複用有冗餘,也就是有多條通往一個address的連接,就要清除
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;
  }

Internal.instance.get(connectionPool, address, this, route);是如何獲得連接的呢?
Internal是個接口只有唯一的實現就是OkhttpClient

 @Override 
 public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
        return pool.get(address, streamAllocation, route);
      }
 @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
      //遍歷連接池,根據address和route判斷是否可用連接
      //重點還是這個isEligible方法是怎麼判斷連接可用的
     
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }
 /**
   * Returns true if this connection can carry a stream allocation to {@code address}. If non-null
   * {@code route} is the resolved route for a connection.
   */
  public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    //如果這個連接不能再承載新的流,返回
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    //如果非host域不相等直接返回
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    //host與也相等,那就完美匹配直接返回
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

    // At this point we don't have a hostname match. But we still be able to carry the request if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. This connection must be HTTP/2.
    //能夠走到這一步說明host域不相等,但是如果是http2可以進行處理
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;

    // 3. This connection's server certificate's must cover the new host.
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    // 4. Certificate pinning must match the host.
    try {
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }

    return true; // The caller's address can be carried by this connection.
  }

經過一些列的判斷和處理以後,能夠找到一條Connection,進行IO操作

總結來看尋找連接的步驟如下:

  1. 查看當前連接是否可用(重試連接情況下)
  2. 第一次去連接池尋找可用連接
  3. 沒有找到,對路由信息進行處理
  4. 第二次去連接池尋找可用連接
  5. 還沒找到,直接創建一條可用連接
  6. 將該連接放入連接池中,更新引用計數

判斷連接是否可用步驟如下:

  1. 查看該連接是否能夠再承載一個Stream,如果不可以,直接返回fasle
  2. 查看非host域是否相等,如果不相等直接返回false
  3. 查看host域是否相等,如果相等完美匹配,返回true
  4. 如果不相等查看是否是http2連接,如果不是,返回false
  5. 進行一些列處理有可能返回true

1.2 原理分析

經過上面的分析,雖然ConnectInterceptorincept方法比較簡短,但是實際上,它的判斷也是非常多的,爲了能夠複用連接,減少重新創建連接進行三次握手的時間消耗,Okhttp可謂是煞費苦心!

原理:正是因爲OkHttpClient內部維護了一個連接池,才讓我們能夠複用連接,同時Http1x系列中,一個Connection對應一個邏輯上的雙向StreamHttp2實現多路複用,就是一個Connection可以對應多個Stream,OkhttpClient中的限制是1個。對於每一個請求Call,ConnectInterceptor不會立即去創建一個新的連接,而是嘗試尋找一個可用的連接,如果經過一系列處理仍然沒有,纔會創建一個,去進行TCP+TLS握手,當然,還需要對ConnectionPool進行清理,這裏就不再囉嗦了,用的清理Socket的方法是計數+標記清理,正是有了這樣的機制,OkhttpClient才能夠進行高效的,併發性強的,低延遲的網絡請求!

二.CallServerInterceptor

說到底上一個攔截器還是爲了CallServerInterceptor做鋪墊的,到了這個攔截器纔會真正根據之前建立的連接進行請求和響應的IO

2.1 源碼分析

@Override 
public Response intercept(Chain chain) throws IOException {
    final RealInterceptorChain realChain = (RealInterceptorChain) chain;
    final HttpCodec httpCodec = realChain.httpStream();
    //httpCode是什麼呢?可以理解爲編碼Http請求,解碼Http響應
    StreamAllocation streamAllocation = realChain.streamAllocation();
    //獲取之前獲得的streamAllocation,並拿到connection
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();//獲得經過一系列攔截器處理的請求

    long sentRequestMillis = System.currentTimeMillis();

    realChain.eventListener().requestHeadersStart(realChain.call());
    httpCodec.writeRequestHeaders(request);//重點查看1
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);

    HttpSink httpSink = null;
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method())
        && (request.body() != null || Internal.instance.isDuplex(request))) {
      // 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"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (Internal.instance.isDuplex(request)) {
          // Prepare a duplex body so that the application can send a request body later.
          final CountingSink requestBodyOut =
              new CountingSink(httpCodec.createRequestBody(request, -1L));
          final BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
          httpSink = new HttpSink() {
            @Override public BufferedSink sink() {
              return bufferedRequestBody;
            }

            @Override public void headers(Headers headers) throws IOException {
              List<Header> headerBlock = new ArrayList<>(headers.size() / 2);
              for (int i = 0, size = headers.size(); i < size; i++) {
                headerBlock.add(new Header(headers.name(i), headers.value(i)));
              }
              ((Http2Codec) httpCodec).writeRequestHeaders(headerBlock);
            }

            @Override public void close() throws IOException {
              bufferedRequestBody.close();
              realChain.eventListener()
                  .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
            }
          };
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          realChain.eventListener().requestBodyStart(realChain.call());
          long contentLength = request.body().contentLength();
          CountingSink requestBodyOut =
              new CountingSink(httpCodec.createRequestBody(request, contentLength));
          BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
          realChain.eventListener()
              .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
        }
      } else if (!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.
        streamAllocation.noNewStreams();
      }
    }

    if (Internal.instance.isDuplex(request)) {
      httpCodec.flushRequest();
    } else {
      httpCodec.finishRequest();
    }

    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis());
    Internal.instance.httpSink(responseBuilder, httpSink);
    Response response = responseBuilder.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
      responseBuilder = httpCodec.readResponseHeaders(false);

      responseBuilder
          .request(request)
          .handshake(streamAllocation.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis());
      Internal.instance.httpSink(responseBuilder, httpSink);
      response = responseBuilder.build();

      code = response.code();
    }

    if (Internal.instance.isDuplex(request)) {
      Response.Builder builder = response.newBuilder();
      Internal.instance.setHttp2Codec(builder, (Http2Codec) httpCodec);
      response = builder.build();
    }

    realChain.eventListener()
            .responseHeadersEnd(realChain.call(), 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(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

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

    return response;
  }

2.2 原理分析

這個攔截器就是最後的一步,獲取服務器原始的response,然後再返回到上一級的每一個攔截器經過處理纔會最後返回給用戶進行請求成功失敗的回調,這一個攔截器IO部分是使用OKIO,非常高效。但是這部分太底層了,面試一般問的比較少,我相信,如果我們能夠把OkHttp一些非常核心的東西說的有條有理,就可以了。

三.總結

Okhttp源碼講解系列到此結束了,還是有很多地方沒有說到,但是大體流程應該過了一遍,沒辦法這裏面的內容是在太多了呀,小夥伴們有沒有感覺對Okhttp有了進一步的理解呢?
看一張別人做好的流程圖!
在這裏插入圖片描述
這就是Okhttp一次請求的全部過程了,非常的詳細。

在這裏插入圖片描述
上面是OkHttp的架構圖!下次面試再問Okhttp源碼還怕嘛??

先別走,我有一個資源學習羣要推薦給你,它是白嫖黨的樂園,小白的天堂!
在這裏插入圖片描述
別再猶豫,一起來學習!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章