OkHttp講解(四)-鏈接池 一、 Interceptor 關聯類分析 二、創建鏈接流程 三、鏈接池的存取數據

一、 Interceptor 關聯類分析

1.1、 StreamAllocation 的成員變量

簡介
StreamAllocation是用來協調Connections、Streams和Calls這三個實體的。

  • Connections:連接到遠程服務器的物理套接字,這個套接字連接可能比較慢,所以它有一套取消機制。
  • Streams:定義了邏輯上的HTTP請求/響應對,每個連接都定義了它們可以攜帶的最大並* 發流,HTTP/1.x每次只可以攜帶一個,HTTP/2每次可以攜帶多個。
    Calls:定義了流的邏輯序列,這個序列通常是一個初始請求以及它的重定向請求,對於同一個連接,我們通常將所有流都放在一個調用中,以此來統一它們的行爲。

HTTP通信 執行 網絡請求Call 需要在 連接Connection 上建立一個新的 流Stream,我們將 StreamAllocation 稱之 流 的橋樑,它負責爲一次 請求 尋找 連接 並建立 流,從而完成遠程通信。

  public final Address address;//地址
  private Route route; //路由
  private final ConnectionPool connectionPool;  //連接池
  private final Object callStackTrace; //日誌

  // State guarded by connectionPool.
  private final RouteSelector routeSelector; //路由選擇器
  private int refusedStreamCount;  //拒絕的次數
  private RealConnection connection;  //連接
  private boolean released;  //是否已經被釋放
  private boolean canceled  //是否被取消了

1.2、 RealConnection

  • okhttp是底層實現框架,與httpURLconnection是同一級別的。OKHttp底層建立網絡連接的關鍵就是RealConnection類。RealConnection類底層封裝socket,是真正的創建連接者。分析這個類之後就明白了OKHttp與httpURLconnection的本質不同點。
  • RealConnection是Connection的實現類,代表着鏈接socket的鏈路,如果擁有了一個RealConnection就代表了我們已經跟服務器有了一條通信鏈路,而且通過RealConnection代表是連接socket鏈路,RealConnection對象意味着我們已經跟服務端有了一條通信鏈路了,在這個類裏面實現的三次握手。
  • 在OKHttp裏面,記錄一次連接的是RealConnection,這個負責連接,在這個類裏面用socket來連接,用HandShake來處理握手。
  //鏈接的線程池
  private final ConnectionPool connectionPool;
  private final Route route;
  //下面這些字段,通過connect()方法開始初始化,並且絕對不會再次賦值
  /** The low-level TCP socket. */
  private Socket rawSocket; //底層socket
  private Socket socket;  //應用層socket
  //握手
  private Handshake handshake;
   //協議
  private Protocol protocol;
   // http2的鏈接
  private Http2Connection http2Connection;
  //通過source和sink,大家可以猜到是與服務器交互的輸入輸出流
  private BufferedSource source;
  private BufferedSink sink;
  //下面這個字段是 屬於表示鏈接狀態的字段,並且有connectPool統一管理
  //如果noNewStreams被設爲true,則noNewStreams一直爲true,不會被改變,並且表示這個鏈接不會再創新的stream流
  public boolean noNewStreams;
  //成功的次數
  public int successCount;
  //此鏈接可以承載最大併發流的限制,如果不超過限制,可以隨意增加
  public int allocationLimit = 1;

RealConnection的connect方法,connect()裏面進行了三次握手

public void connect(。。。) {
     //如果協議不等於null,拋出一個異常
    if (protocol != null) throw new IllegalStateException("already connected");

   。。 省略部分代碼。。。。

    while (true) {//一個while循環
         //如果是https請求並且使用了http代理服務器
        if (route.requiresTunnel()) {
          connectTunnel(...);
        } else {//
            //直接打開socket鏈接
          connectSocket(connectTimeout, readTimeout);
        }
        //建立協議
        establishProtocol(connectionSpecSelector);
        break;//跳出while循環
        。。省略部分代碼。。。
  }

 //當前route的請求是https並且使用了Proxy.Type.HTTP代理
 public boolean requiresTunnel() {
    return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
  }

普通連接的建立過程爲建立TCP連接,建立TCP連接的過程爲

 private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    //根據代理類型來選擇socket類型,是代理還是直連
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    try {
    //連接socket
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      throw new ConnectException("Failed to connect to " + route.socketAddress());
    }
    source = Okio.buffer(Okio.source(rawSocket));//從socket中獲取source 對象。
    sink = Okio.buffer(Okio.sink(rawSocket));//從socket中獲取sink 對象。
  }

Okio.source(rawSocket)Okio.sink(rawSocket)的原碼

  public static Source source(Socket socket) throws IOException {
    if (socket == null) throw new IllegalArgumentException("socket == null");
    AsyncTimeout timeout = timeout(socket);
    Source source = source(socket.getInputStream(), timeout);
    return timeout.source(source);
  }

  public static Sink sink(Socket socket) throws IOException {
    if (socket == null) throw new IllegalArgumentException("socket == null");
    AsyncTimeout timeout = timeout(socket);
    Sink sink = sink(socket.getOutputStream(), timeout);
    return timeout.sink(sink);
  }

建立隧道連接的過程

  private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout)
      throws IOException {
      //1、創建隧道請求對象
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    int attemptedConnections = 0;
    int maxAttempts = 21;
    //一個while循環
    while (true) {
       //嘗試連接詞說超過最大次數
      if (++attemptedConnections > maxAttempts) {
        throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);
      }
      //2、打開socket鏈接
      connectSocket(connectTimeout, readTimeout);
     //3、請求開啓隧道並返回tunnelRequest 
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

     //4、成功開啓了隧道,跳出while循環
      if (tunnelRequest == null) break; /

      //隧道未開啓成功,關閉相關資源,繼續while循環    
      //當然,循環次數超限後拋異常,退出wiile循環
      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
    }
  }
  //隧道請求是一個常規的HTTP請求,只是請求的內容有點特殊。最初創建的隧道請求如
  private Request createTunnelRequest() {
    return new Request.Builder()
        .url(route.address().url())
        .header("Host", Util.hostHeader(route.address().url(), true))
        .header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
        .header("User-Agent", Version.userAgent())
        .build();
  }

二、創建鏈接流程

ConnectInterceptor裏面創建鏈接,並把創建的鏈接放如鏈接池中。具體過程如下:

@Override 
public Response intercept(Chain chain) throws IOException {
   RealInterceptorChain realChain = (RealInterceptorChain) chain;
   Request request = realChain.request();
    //獲取可複用流
   StreamAllocation streamAllocation = realChain.streamAllocation();
   boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //創建輸出流
   HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
   //根據HTTP/1.x(keep-alive)和HTTP/2(流複用)的複用機制,發起連接
   RealConnection connection = streamAllocation.connection();
   return realChain.proceed(request, streamAllocation, httpCodec, connection);
 }

通過ConnectInterceptor 中的HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);方法調用StreamAllocation#newStream方法

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

    try {
      //獲取一個健康的連接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      //實例化HttpCodec,如果是HTTP/2則是Http2Codec否則是Http1Codec
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

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

然後調用StreamAllocation#findHealthyConnection方法

    private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                                 int writeTimeout, int pingIntervalMillis,
                                                 boolean connectionRetryEnabled,
                                                 boolean doExtensiveHealthChecks) throws IOException {
        while (true) {
            //todo 找到一個連接
            RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
                    pingIntervalMillis, connectionRetryEnabled);

            //todo 如果這個連接是新建立的,那肯定是健康的,直接返回
            synchronized (connectionPool) {
                if (candidate.successCount == 0) {
                    return candidate;
                }
            }

            //todo 如果不是新創建的,需要檢查是否健康
            if (!candidate.isHealthy(doExtensiveHealthChecks)) {
                //todo 不健康 關閉連接,釋放Socket,從連接池移除
                // 繼續下次尋找連接操作
                noNewStreams();
                continue;
            }
            return candidate;
        }
    }

然後調用StreamAllocation#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");
            releasedConnection = this.connection;
            toClose = releaseIfNoNewStreams();
            if (this.connection != null) {
                // 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) {
                //todo 嘗試從連接池獲取連接,如果有可複用的連接,會給第三個參數 this的connection賦值
                //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;
        }

        // If we need a route selection, make one. This is a blocking operation.
        //todo 創建一個路由 (dns解析的所有ip與代理的組合)
        boolean newRouteSelection = false;
        if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
            newRouteSelection = true;
            routeSelection = routeSelector.next();
        }

        synchronized (connectionPool) {
            if (canceled) throw new IOException("Canceled");

            if (newRouteSelection) {
                //todo 根據代理和不同的ip從連接池中找可複用的連接
                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;
                    }
                }
            }
            //todo 還是沒找到,必須新建一個連接了
            if (!foundPooledConnection) {
                if (selectedRoute == null) {
                    selectedRoute = routeSelection.next();
                }
                route = selectedRoute;
                refusedStreamCount = 0;
                result = new RealConnection(connectionPool, selectedRoute);
                acquire(result, false);
            }
        }
        if (foundPooledConnection) {
            eventListener.connectionAcquired(call, result);
            return result;
        }
        // Do TCP + TLS handshakes. This is a blocking operation.
        //todo 實際上就是創建socket連接,但是要注意的是如果存在http代理的情況
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        routeDatabase().connected(result.route());

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

            // Pool the connection.
            //todo 將新創建的連接放到連接池中
            Internal.instance.put(connectionPool, result);
            if (result.isMultiplexed()) {
                socket = Internal.instance.deduplicate(connectionPool, address, this);
                result = connection;
            }
        }
        closeQuietly(socket);

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

然後調用StreamAllocation#isHealthy判斷是否健康鏈接

    public boolean isHealthy(boolean doExtensiveChecks) {
        //todo Socket關閉,肯定不健康
        if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
            return false;
        }

        if (http2Connection != null) {
            return !http2Connection.isShutdown();
        }

        if (doExtensiveChecks) {
            try {
                int readTimeout = socket.getSoTimeout();
                try {
                    socket.setSoTimeout(1);
                    if (source.exhausted()) {
                        return false; // Stream is exhausted; socket is closed.
                    }
                    return true;
                } finally {
                    socket.setSoTimeout(readTimeout);
                }
            } catch (SocketTimeoutException ignored) {
                // Read timed out; socket is good.
            } catch (IOException e) {
                return false; // Couldn't read; socket is closed.
            }
        }
        return true;
    }

三、鏈接池的存取數據

根據圖我們看ConnectionPool裏面代碼:

  • 1、創建
//這是一個用於清楚過期鏈接的線程池,每個線程池最多隻能運行一個線程,並且這個線程池允許被垃圾回收
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  //每個address的最大空閒連接數。
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  //清理任務
  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };
  //鏈接的雙向隊列
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  //路由的數據庫
  final RouteDatabase routeDatabase = new RouteDatabase();
   //清理任務正在執行的標誌
  boolean cleanupRunning;
//創建一個適用於單個應用程序的新連接池。
 //該連接池的參數將在未來的okhttp中發生改變
 //目前最多可容乃5個空閒的連接,存活期是5分鐘
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(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);
    }
  }

管理http和http/2的鏈接,以便減少網絡請求延遲。同一個address將共享同一個connection。該類實現了複用連接的目標。

  • 1、主要就是connections,可見ConnectionPool內部以隊列方式存儲連接;
  • 2、routDatabase是一個黑名單,用來記錄不可用的route,但是看代碼貌似ConnectionPool並沒有使用它。所以此處不做分析。
  • 3、剩下的就是和清理有關了,所以executor是清理任務的線程池,cleanupRunning是清理任務的標誌,cleanupRunnable是清理任務。
  • 2、put 鏈接
    /**
     * todo 保存連接以複用。
     * 本方法沒上鎖,只加了斷言: 當前線程擁有this(pool)對象的鎖。
     * 表示使用這個方法必須要上鎖,而且是上pool的對象鎖。
     * okhttp中使用到這個函數的地方確實也是這麼做的
     */
    void put(RealConnection connection) {
        assert (Thread.holdsLock(this));
        //todo 如果清理任務未執行就啓動它,再把新連接加入隊列
        if (!cleanupRunning) {
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
  • 2、get 鏈接
    /**
     * todo 獲取可複用的連接
     */
    @Nullable
    RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
            //todo 要拿到的連接與連接池中的連接  連接的配置(dns/代理/域名等等)一致 就可以複用
            // 在使用了,所以 acquire 會創建弱引用放入集合記錄
            if (connection.isEligible(address, route)) {
                streamAllocation.acquire(connection, true);
                return connection;
            }
        }
        return null;
    }

執行RealConnection#isEligible檢查是否可以複用

    public boolean isEligible(Address address, @Nullable Route route) {
        // If this connection is not accepting new streams, we're done.
        //todo 實際上就是在使用(對於http1.1)就不能複用
        if (allocations.size() >= allocationLimit || noNewStreams) return false;
        // If the non-host fields of the address don't overlap, we're done.
        //todo 如果地址不同,不能複用。包括了配置的dns、代理、證書以及端口等等 (域名還沒判斷,所有下面馬上判斷域名)
        if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

        // If the host exactly matches, we're done: this connection can carry the address.
        //todo 都相同,那就可以複用了
        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.
        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.
    }
  • 2、清理 鏈接池
    long cleanup(long now) {
        int inUseConnectionCount = 0;
        int idleConnectionCount = 0;
        RealConnection longestIdleConnection = null;
        long longestIdleDurationNs = Long.MIN_VALUE;

        // Find either a connection to evict, or the time that the next eviction is due.
        synchronized (this) {
            for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
                RealConnection connection = i.next();

                //todo 檢查連接是否正在被使用
                //If the connection is in use, keep searching.
                if (pruneAndGetAllocationCount(connection, now) > 0) {
                    inUseConnectionCount++;
                    continue;
                }
                //todo 否則記錄閒置連接數
                idleConnectionCount++;

                // If the connection is ready to be evicted, we're done.
                //TODO 獲得這個連接已經閒置多久
                // 執行完遍歷,獲得閒置了最久的連接
                long idleDurationNs = now - connection.idleAtNanos;
                if (idleDurationNs > longestIdleDurationNs) {
                    longestIdleDurationNs = idleDurationNs;
                    longestIdleConnection = connection;
                }
            }
            //todo 超過了保活時間(5分鐘) 或者池內數量超過了(5個) 馬上移除,然後返回0,表示不等待,馬上再次檢查清理
            if (longestIdleDurationNs >= this.keepAliveDurationNs
                    || idleConnectionCount > this.maxIdleConnections) {
                // We've found a connection to evict. Remove it from the list, then close it
                // below (outside
                // of the synchronized block).
                connections.remove(longestIdleConnection);
            } else if (idleConnectionCount > 0) {
                // A connection will be ready to evict soon.
                //todo 池內存在閒置連接,就等待 保活時間(5分鐘)-最長閒置時間 =還能閒置多久 再檢查
                return keepAliveDurationNs - longestIdleDurationNs;
            } else if (inUseConnectionCount > 0) {
                // All connections are in use. It'll be at least the keep alive duration 'til we
                // run again.
                //todo 有使用中的連接,就等 5分鐘 再檢查
                return keepAliveDurationNs;
            } else {
                // No connections, idle or in use.
                //todo 都不滿足,可能池內沒任何連接,直接停止清理(put後會再次啓動)
                cleanupRunning = false;
                return -1;
            }
        }
        closeQuietly(longestIdleConnection.socket());
        // Cleanup again immediately.
        return 0;
    }

ConnectionPool#pruneAndGetAllocationCount檢查連接是否正在被使用

    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
        //todo 這個連接被使用就會創建一個弱引用放入集合,這個集合不爲空就表示這個連接正在被使用
        // 實際上 http1.x 上也只能有一個正在使用的。
        List<Reference<StreamAllocation>> references = connection.allocations;
        for (int i = 0; i < references.size(); ) {
            Reference<StreamAllocation> reference = references.get(i);
            if (reference.get() != null) {
                i++;
                continue;
            }

            // We've discovered a leaked allocation. This is an application bug.
            StreamAllocation.StreamAllocationReference streamAllocRef =
                    (StreamAllocation.StreamAllocationReference) reference;
            String message = "A connection to " + connection.route().address().url()
                    + " was leaked. Did you forget to close a response body?";
            Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

            references.remove(i);
            connection.noNewStreams = true;


            // If this was the last allocation, the connection is eligible for immediate eviction.
            if (references.isEmpty()) {
                connection.idleAtNanos = now - keepAliveDurationNs;
                return 0;
            }
        }
        return references.size();
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章