OkHttp 源碼剖析系列(六)——連接復用機制及連接的建立

系列索引

本系列文章基于 OkHttp3.14

OkHttp 源碼剖析系列(一)——請求的發起及攔截器機制概述

OkHttp 源碼剖析系列(二)——攔截器大體流程分析

OkHttp 源碼剖析系列(三)——緩存機制分析

OkHttp 源碼剖析系列(四)——連接的建立概述

OkHttp 源碼剖析系列(五)——路由選擇機制

OkHttp 源碼剖析系列(六)——連接復用機制及連接的建立

OkHttp 源碼剖析系列(七)——請求的發起及響應的讀取

前言

findConnection 的過程中無法從 transmitter 中取得 Connection 時,會調用 connectionPool.transmitterAcquirePooledConnection 方法來嘗試從連接池中獲取連接,讓我們從這篇文章開始研究一下 OkHttp 中連接池的實現。

HTTP 中的復用機制

HTTP/1.0

在 HTTP/1.0 中,由于 HTTP 協議是一種無連接的網絡協議,進行一次 HTTP 請求是這樣的一條流程:

image-20190803100926274

這樣設計可以保證每條 HTTP 請求都是獨立的,互不干擾。但這樣的設計有一個致命的缺點——如果我們向同一個服務器發起數十個 HTTP 請求,則我們的每條 HTTP 請求都需要與這個服務器建立一條 TCP 連接。而我們知道,建立 TCP 連接需要經過三次握手,而關閉 TCP 連接則需要四次揮手,可想而知這樣頻繁地建立與關閉 TCP 連接對網絡資源的消耗是十分嚴重的,極大地降低了網絡的效率,并且提高了服務器的壓力。

在 HTTP/1.0 中存在一個名為 Connection:Keep-Alive 的 Header,但沒有官方的標準規定其工作機制,它默認是關閉的,可以通過在 Header 中加入從而開啟。當客戶端及服務端都對 Keep-Alive 機制支持時,就可以維持該 TCP 連接從而使得下一次可以進行復用。

HTTP/1.1

而在 HTTP/1.1 中,真正引入了 Keep-Alive 機制,它默認是開啟的,可以通過 Connection:close 進行關閉。在 HTTP 請求結束時,若啟動了 Keep-Alive 機制,則該連接并不會立即關閉,此時如果有新的請求到來,且 host 相同,則會復用這條 TCP 連接進行請求,減少了 TCP 連接的頻繁建立與關閉的資源消耗。

image-20190803103322539

通過這樣的連接復用的做法,可以大幅地減少對資源的消耗,如下圖所示:

image

同時,在 HTTP/1.1 中還引入了 Keep-Alive 請求頭,在其中可以設定兩個值:timeoutmax ,從而設定這個連接何時被關閉。

  • timeout:指定了一個空閑連接需要保持打開狀態的最小時長(以秒為單位)
  • max:在連接關閉之前,在此連接可以發送的請求的最大值

但這樣就存在了一個問題,在原來不采用 Keep-Alive 的時候,客戶端可以通過 TCP 連接是否關閉來判斷數據是否接收完成,但在采用了 Keep-Alive 的情況下,客戶端如何才能得知自己需要的數據已經接收完畢了呢?

Content-Length

看過我之前的多線程下載的實現博文的讀者,應該知道在服務端的 ResponseHeader 中,會包含 Content-Length 這一字段,它表示了實體內容的長度(比如文件 / 圖片的大小),通過該字段客戶端就可以確定自己需要接受的字節數。從而確認數據已接收完成。

Transfer-Encoding:chunked

前面的 Content-Length 看上去完美解決了無法判斷數據接收完畢的問題。但對于一些動態的場景,比如一些動態頁面,服務端是無法預先知道該頁面的大小的,在該頁面創建完成前,其長度是不可知的,服務端也就無法返回一個確切的 Content-Length 字段給客戶端了,只能開啟一個足夠大的 buffer。

此時,就可以采用 Transfer-Encoding:chunked 來實現,它表示一種分塊編碼的意思,它只在 HTTP/1.1 中提供,允許服務端將發送給客戶端的數據分成多個部分。

如果使用了分塊編碼,則請求及響應有以下的特點:

  1. 在 Header 中加入 Transfer-Encoding:chunked,表示使用分塊編碼
  2. 每一個分塊有兩行,每一行都以 \r\n 結尾,第一行表示這個分塊的數據長度,是一個十六進制的數(不包括數據結尾的 \r\n,第二行則是這個分塊的具體數據。
  3. 最后一個分塊長度為0,且數據沒有內容,表示整個實體的結束。

HTTP/2

在前面的 HTTP/1.1 中,雖然實現了 TCP 連接的復用,但仍有如下幾個缺陷:

  1. 如果客戶端想要發起并行的請求,則必須建立多個 TCP 連接,這對網絡資源的消耗也是十分嚴重的。
  2. 不會讀對請求及響應的 Header 進行壓縮,造成了網絡流量的浪費。
  3. 不支持資源優先級導致 TCP 連接利用率低下。

多路復用

為了解決上面幾個問題,HTTP/2 引入了多路復用機制,同時引入了幾個新的概念:

  • 數據流:基于 TCP 連接上的一個雙向的字節流,每發起一個請求,就會建立一個數據流,后續的請求過程的數據傳遞都通過該流進行
  • 數據幀:HTTP/2 中的數據最小切片單位,其中又分為了 Header FrameData Frame 等等。
  • 消息:一個請求或響應對應的一系列數據幀。

引入了這些概念之后,在 HTTP 請求的過程中,服務端/客戶端首先會將我們的請求/響應切分為不同的數據幀,當另一方接收到后再將其組裝從而形成完整的請求/響應,如下所示

image-20190803114234258

這樣,就實現了對 TCP 連接的多路復用,將一個請求或響應分為了一個個的數據幀,使得多個請求可以并行地進行。

多路復用與 Keep-Alive 的區別

  1. Keep-Alive 機制雖然解決了復用 TCP 連接問題,但沒有解決請求阻塞的問題,需要等到上一個請求結束后,才能復用該 TCP 連接進行下一個請求。
  2. HTTP/1.x 對數據的傳遞仍然是以一個整體進行傳遞,而在 HTTP/2 中引入了數據幀的概念,使得多個請求可以同時在流中進行傳遞。
  3. HTTP/2 采用了 HPACK 壓縮算法對 Header 進行壓縮,降低了請求的流量消耗。
img

OkHttp 中的復用機制

前面提了 HTTP 中的復用機制,通過對 TCP 連接的復用,大幅提高了網絡請求的效率。無論是 HTTP/1.1 中的 Keep-Alive 還是 HTTP/2 中的多路復用,都需要連接池來維護 TCP 連接,讓我們看看 OkHttp 中連接池的實現。

我們知道,在 findConnection 過程中,若無法從 transimitter 中獲取到連接,則會嘗試從連接池中獲取連接。

我們可以看到 RealConnectionPool.connections,它是一個 Deque,保存了所有的連接:

private final Deque<RealConnection> connections = new ArrayDeque<>();

連接清理機制

同時會發現,在這個類中還存在著一個 executor,它的設置與 OkHttp 用于異步請求的線程池的設置幾乎一樣,它是用來做什么的呢?

/**
 * Background threads are used to cleanup expired connections. There will be at most a single
 * thread running per connection pool. The thread pool executor permits the pool itself to be
 * garbage collected.
 */
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
        Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
        new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

通過上面的注釋可以看出,它是用來執行清理過期連接的任務的,并且最多每個連接池只會有一個線程在執行清理任務。這個清理的任務就是下面的 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) {
                }
            }
        }
    }
};

可以看到它是采用一個循環的方式調用 cleanup 方法進行清理,并從返回值中獲取了需要 wait 的秒數,調用 wait 方法進入阻塞,也就是說每次清理的間隔由 cleanup 的返回值進行決定

我們看到 cleanup 方法:

/**
 * Performs maintenance on this pool, evicting the connection that has been idle the longest if
 * either it has exceeded the keep alive limit or the idle connections limit.
 *
 * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
 * -1 if no further cleanups are required.
 */
long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;
    synchronized (this) {
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
            RealConnection connection = i.next();
            // 統計連接被引用的transimitter的個數,若大于0則說明是正在使用的連接
            if (pruneAndGetAllocationCount(connection, now) > 0) {
                inUseConnectionCount++;
                continue;
            }
            // 否則是空閑連接
            idleConnectionCount++;
            // 找出空閑連接中空閑時間最長的連接
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > longestIdleDurationNs) {
                longestIdleDurationNs = idleDurationNs;
                longestIdleConnection = connection;
            }
        }
        if (longestIdleDurationNs >= this.keepAliveDurationNs
                || idleConnectionCount > this.maxIdleConnections) {
            // 如果發現空閑時間最久的連接所空閑時間超過了Keep-Alive設定的時間,或者是空閑連接數超過了最大空閑連接數
            // 將前面的其從隊列中刪除,并且在之后對其socket進行關閉
            connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {
            // 返回離達到keep-alive設定的時間的距離,將在達到時執行進行清理
            return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {
            // 如果當前連接都是正在使用的,返回keep-alive所設定的時間
            return keepAliveDurationNs;
        } else {
            // 沒有連接了,停止運行cleanup
            cleanupRunning = false;
            return -1;
        }
    }
    // 關閉空閑最久的連接,繼續嘗試清理
    closeQuietly(longestIdleConnection.socket());
    return 0;
}

可以看到,主要是下面幾步:

  1. 調用 pruneAndGetAllocationCount 方法統計連接被引用的數量,大于 0 說明連接正在被使用
  2. 通過上面的方法統計空閑連接數及正在使用的連接數,并從中找出空閑最久的連接
  3. 若空閑最久的連接空閑的時間超過了所設定的 keepAliveDurationNs(這里不是指的 Keep-Alive 所設定時間),或者空閑連接數超過了所設定的 maxIdleConnections,清理該連接(移除并關閉socket),并返回 0 表示立即繼續清理。
  4. 若還未超過,則返回下一次超過外部設定的 keepAliveDurationNs,表示等到下次超時的時候再進行清理
  5. 若當前連接都正處于使用中,返回所設定的 keepAliveDurationNs
  6. 若當前沒有連接,則將 cleanupRunning 置為 false 停止清理

在 OkHttp 中,將空閑連接的最長存活時間設定為了 5 分鐘,并且將最大空閑連接數設置為了 5

我們看看 pruneAndGetAllocationCount 是如何對連接被引用的數量進行統計的:

/**
 * Prunes any leaked transmitters and then returns the number of remaining live transmitters on
 * {@code connection}. Transmitters are leaked if the connection is tracking them but the
 * application code has abandoned them. Leak detection is imprecise and relies on garbage
 * collection.
 */
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<Transmitter>> references = connection.transmitters;
    for (int i = 0; i < references.size(); ) {
        Reference<Transmitter> reference = references.get(i);
        if (reference.get() != null) {
            i++;
            continue;
        }
        // We've discovered a leaked transmitter. This is an application bug.
        TransmitterReference transmitterRef = (TransmitterReference) reference;
        String message = "A connection to " + connection.route().address().url()
                + " was leaked. Did you forget to close a response body?";
        Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace);
        references.remove(i);
        connection.noNewExchanges = 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();
}

可以看到,connection 中是有維護一個引用它的 TransmitterReference 隊列的,通過遍歷并判斷該 Transimitter 是否為 null 即可進行統計。這里的 Reference 所存的實際是一個繼承自 WeakReferenceTransimitterReference 類:

static final class TransmitterReference extends WeakReference<Transmitter> {
   // ...
}

可以發現,這種設計有點像 JVM 中的引用計數法 + 標記清除,實際上就是 OkHttp 仿照 JVM 的垃圾回收設計了這樣一種類似引用計數法的方式來統計一個連接是否是空閑連接,同時采用標記清除法對空閑且不滿足設定的規則的連接進行清除

獲取連接

我們看到 connectionPool.transmitterAcquirePooledConnection 方法,了解一下連接池獲取連接的過程:

/**
 * Attempts to acquire a recycled connection to {@code address} for {@code transmitter}. Returns
 * true if a connection was acquired.
 *
 * <p>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<Route> 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;
}

可以看到,首先注釋中對我們傳入不同的 routes 參數進行了解釋,若 routes 不為 null 說明這是已解析過的路由,可以將其合并到同一個 HTTP/2 連接。

而在 connection.isMultiplexed 的注釋中說到,若該連接為 HTTP/2 連接,則會返回 true。

connection.isEligible 注釋中則說到,若該連接可以給對應的 address 分配 stream,則返回 true。

在代碼中,對 connections 進行了遍歷:

  1. 當需要進行多路費用且當前的連接不是 HTTP/2 連接時,則放棄當前連接

  2. 當當前連接不能用于為 address 分配 stream,則放棄當前連接。

  3. 前兩者都不滿足,則獲取該連接,并設置到 transimitter 中。

三次獲取連接的區別

我們回顧一下 findConnection 中三次嘗試從連接池獲取連接的過程:

  • 第一次嘗試:connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)

  • 第二次嘗試(需要在進行了路由選擇的情況下):connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)

  • 第三次嘗試:connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)

可以發現,其傳入的參數是不同的。第一次由于是嘗試從已經解析過的路由的連接池中獲取連接,因此 route 設置為 null。

第二次由于是在無法找到對應的連接,在進行了路由選擇的條件下進行的,因此將 route 設置為了 null。

而最后一次嘗試從連接池獲取連接之所以需要將 requireMultiplexed 設置為 true,因為這次只有可能是在多個請求并行進行的情況下才有可能發生,這種情況只有 HTTP/2 的連接才有可能發生。

加入連接

通過 RealConnectionPool.put 方法可以向連接池中加入連接:

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
        cleanupRunning = true;
        executor.execute(cleanupRunnable);
    }
    connections.add(connection);
}

由于之前判斷了如果連接池中沒有連接,就會暫停連接清理線程,所以這里如果放入了新的連接,就會判斷連接清理線程是否正在執行,若已停止執行則將其繼續執行。之后將該連接放入了 Deque 中。

通知連接空閑

每當外部調用了 Transimitter.releaseConnectionNoEvents 方法時,最后都會調用到 RealConnection.connectionBecameIdle 方法來通知連接池連接進入了空閑狀態:

/**
 * Notify this pool that {@code connection} has become idle. Returns true if the connection has
 * been removed from the pool and should be closed.
 */
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;
    }
}

此時如果該連接不支持用于創建新 Exchange,或不允許有空閑連接,則會直接將該連接移除,否則會通過 notifyAll 方法喚醒阻塞的清理線程,嘗試對空閑連接進行清理,這樣能保證每當有空閑連接時最及時地對連接池進行清理。

連接的建立

我們知道,在尋找連接的過程中,若從 Transimitter 及連接池中都無法獲取到連接時,就會創建一個新的連接,讓我們看看這個創建連接的過程是怎樣的:

在尋找連接的代碼中,創建連接的核心代碼如下:

// ...
result = new RealConnection(connectionPool, selectedRoute);
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);

我們先看到 RealConnection 的構造函數:

public RealConnection(RealConnectionPool connectionPool, Route route) {
    this.connectionPool = connectionPool;
    this.route = route;
}

只是進行了簡單的賦值,我們接著看到 RealConnection.connect 方法:

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
                    int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
                    EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");
    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    // ...一些錯誤處理
    while (true) {
        try {
            if (route.requiresTunnel()) {
                // 如果使用了隧道技術,調用connectTunnel方法
                connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
                if (rawSocket == null) {
                    // We were unable to connect the tunnel but properly closed down our resources.
                    break;
                }
            } else {
                // 未使用隧道技術,調用connectSocket方法
                connectSocket(connectTimeout, readTimeout, call, eventListener);
            }
            // 建立協議
            establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
            eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
            break;
        } catch (IOException e) {
            //... 異常下的資源釋放
        }
    }
    // ... 一些錯誤處理
}

可以看到,這里是一個循環,不斷嘗試建立連接,其中核心步驟如下:

  1. 若使用了隧道技術,調用 connectTunnel 方法
  2. 若未使用隧道技術,調用 connectSocket 方法
  3. 調用 establishProtocol 方法建立協議

讓我們看看三個方法分別是如何實現的。

直接連接

我們先看看直接連接是如何實現的,我們看到 connectSocket 方法:

/**
 * Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket.
 */
private void connectSocket(int connectTimeout, int readTimeout, Call call,
                           EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();
    // 初始化rawSocket,其中對SOCKS代理采用了SOCKS代理服務器
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.socketFactory().createSocket()
            : new Socket(proxy);
    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    try {
        // 調用connectSocket方法對Socket進行連接,這里預置了不同平臺的實現
        Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
        ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
        ce.initCause(e);
        throw ce;
    }
    // 獲取source及sink,用于讀取及寫入
    try {
        source = Okio.buffer(Okio.source(rawSocket));
        sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
        if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
            throw new IOException(npe);
        }
    }
}

可以看到,這里主要是進行 Socket 的連接,首先根據代理類型創建了 Socket,之后調用了 connectSocket 方法進行連接(里面調用的其實仍然是 socket.connect 方法)。最后調用 Okio 的方法獲取 sourcesink

這個過程還是比較簡單的,和正常使用 Socket 的流程大致相同:創建Socket=>連接=>獲取 stream,其中在 connectSocket 時根據不同平臺做了不同的處理。

通過隧道連接

首先我們要理解一下什么是隧道。這個其實是計網中的知識,之前在 《計算機網絡——自頂向下方法》中看到過,不過書中沒有詳細介紹,這里剛好學習一下。

隧道技術的出現主要是為了適配 IPv4 到 IPv6 的轉變。通過這種隧道技術,可以通過一種網絡協議來傳輸另外一種網絡協議的數據,比如 A 主機與 B 主機都是采用 IPv6,而連接 A 與 B 的是 IPv4 的網絡,為了實現 A 與 B 的通信,可以使用隧道技術,數據包經過 IPv4 的多協議路由時將 IPv6 的數據包放入 IPv4 的數據包中,傳遞給 B。當到達 B 的路由器時,數據又被剝離之后傳遞給 B。這樣在 A 與 B 看來,它們使用的都是 IPv6 與對方通信。如下圖所示:

image-20190803160002636

那么怎么打開隧道呢?

HTTP 提供了一個特殊的 method—— CONNECT,它是 HTTP/1.1 協議中預留的方法,可以通過它將連接改為隧道的代理服務器。客戶端發送一個 CONNECT 請求給隧道網關請求打開一條 TCP 連接,當隧道打通之后,客戶端通過 HTTP 隧道發送的所有數據會轉發給 TCP 連接,服務器響應的所有數據會通過隧道發給客戶端。

而在 OkHttp 中,對隧道的支持主要是為了支持 SSL 隧道——SSL 隧道的初衷是為了通過防火墻來傳輸加密的 SSL 數據,此時隧道的作用就是將非 HTTP 的流量(SSL流量)傳過防火墻到達指定的服務器(比如 HTTPS)。

接著我們看到 connectTunnel 方法的實現:

/**
 * Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a
 * proxy server can issue an auth challenge and then close the connection.
 */
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
                           EventListener eventListener) throws IOException {
    // 創建隧道Request
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
        // 通過connectSocket建立Socket
        connectSocket(connectTimeout, readTimeout, call, eventListener);
        // 創建隧道
        tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
        // 當創建的隧道為null時,說明隧道成功建立,break
        if (tunnelRequest == null) break; 
        // 回收資源
        closeQuietly(rawSocket);
        rawSocket = null;
        sink = null;
        source = null;
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
    }
}

這里首先構建了一個隧道的 tunnelRequest。之后進行了循環,不斷嘗試建立隧道,不過 OkHttp 限制了其最大嘗試次數為 21 次。

建立隧道的過程首先通過 connectSocket 方法建立了 Socket 連接,然后通過 createTunnel 方法建立隧道。

我們看看 createTunnelRequest 方法做了什么:

private Request createTunnelRequest() throws IOException {
    Request proxyConnectRequest = new Request.Builder()
            .url(route.address().url())
            .method("CONNECT", null)
            .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();
    Response fakeAuthChallengeResponse = new Response.Builder()
            .request(proxyConnectRequest)
            .protocol(Protocol.HTTP_1_1)
            .code(HttpURLConnection.HTTP_PROXY_AUTH)
            .message("Preemptive Authenticate")
            .body(Util.EMPTY_RESPONSE)
            .sentRequestAtMillis(-1L)
            .receivedResponseAtMillis(-1L)
            .header("Proxy-Authenticate", "OkHttp-Preemptive")
            .build();
    Request authenticatedRequest = route.address().proxyAuthenticator()
            .authenticate(route, fakeAuthChallengeResponse);
    return authenticatedRequest != null
            ? authenticatedRequest
            : proxyConnectRequest;
}

可以看到,這里構建了一個 method 為 CONENCT 的請求。

我們接著看看 createTunnel 方法又做了什么事情:

/**
 * To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
 * the proxy connection. This may need to be retried if the proxy requires authorization.
 */
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
                             HttpUrl url) throws IOException {
    // 構造HTTP/1.1請求
    String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    while (true) {
        Http1ExchangeCodec tunnelCodec = new Http1ExchangeCodec(null, null, source, sink);
        source.timeout().timeout(readTimeout, MILLISECONDS);
        sink.timeout().timeout(writeTimeout, MILLISECONDS);
        tunnelCodec.writeRequest(tunnelRequest.headers(), requestLine);
        tunnelCodec.finishRequest();
        // 發出隧道請求
        Response response = tunnelCodec.readResponseHeaders(false)
                .request(tunnelRequest)
                .build();
        tunnelCodec.skipConnectBody(response);
        switch (response.code()) {
            case HTTP_OK:
                // 返回200說明成功建立隧道,返回null
                if (!source.getBuffer().exhausted() || !sink.buffer().exhausted()) {
                    throw new IOException("TLS tunnel buffered too many bytes!");
                }
                return null;
            case HTTP_PROXY_AUTH:   // 表示服務端要進行代理認證
                // 進行代理認證
                tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
                // 代理認證不通過
                if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");
                // 代理認證通過,但需要關閉TCP連接
                if ("close".equalsIgnoreCase(response.header("Connection"))) {
                    return tunnelRequest;
                }
                break;
            default:
                throw new IOException(
                        "Unexpected response code for CONNECT: " + response.code());
        }
    }
}

可以看到,這里主要進行如下的工作:

  1. 拼接 HTTP/1.1 請求
  2. 發出隧道請求,讀取響應
  3. 若隧道請求返回 200,說明隧道建立成功,返回 null
  4. 若隧道返回 407,說明服務器需要進行代理認證,調用對應方法進行代理認證

隧道打通之后,就可以通過隧道進行網絡請求了。

發布協議

經過前面的步驟,我們建立了一條與服務端的 Socket 通道,我們接著看到 establishProtocol 方法:

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
                               int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
    // 如果不是https地址
    if (route.address().sslSocketFactory() == null) {
        // 如果協議中包含了 http2 with prior knowledge
        if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
            socket = rawSocket;
            protocol = Protocol.H2_PRIOR_KNOWLEDGE;
            startHttp2(pingIntervalMillis);
            return;
        }
        // 協議為 HTTP/1.1
        socket = rawSocket;
        protocol = Protocol.HTTP_1_1;
        return;
    }
    eventListener.secureConnectStart(call);
    // TLS握手
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);
    if (protocol == Protocol.HTTP_2) {
        // 如果是HTTP2協議,調用 startHttp2 方法
        startHttp2(pingIntervalMillis);
    }
}

可以看到,這個方法主要是在建立了 Socket 連接的基礎上,對各個協議進行支持。

首先判斷了當前地址是否是 HTTPS 地址。

不是 HTTPS 的情況下,若協議中包含了 H2_PRIOR_KNOWLEDGE 則采用 HTTP/2 進行請求,調用 startHttp2 方法,否則采用 HTTP/1.1。

是 HTTPS 的情況下,首先調用了 connectTls 方法進行 TLS 握手,之后若是 HTTP/2 協議,則調用 startHttp2 方法。

啟動 HTTP/2 連接

讓我們先看看 startHttp2 方法究竟是做了什么:

private void startHttp2(int pingIntervalMillis) throws IOException {
    socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
    http2Connection = new Http2Connection.Builder(true)
            .socket(socket, route.address().url().host(), source, sink)
            .listener(this)
            .pingIntervalMillis(pingIntervalMillis)
            .build();
    http2Connection.start();
}

這里主要是構建了一個 HTTP/2 的 Http2Connection,并且將 listener 設置為了該 RealConnection,之后通過 http2Connection.start 方法啟動了 HTTP/2 連接。

/**
 * @param sendConnectionPreface true to send connection preface frames. This should always be true
 *                              except for in tests that don't check for a connection preface.
 */
void start(boolean sendConnectionPreface) throws
        IOException {
    if (sendConnectionPreface) {
        writer.connectionPreface();
        writer.settings(okHttpSettings);
        int windowSize = okHttpSettings.getInitialWindowSize();
        if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
            writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
        }
    }
    new Thread(readerRunnable).start(); // Not a daemon thread.
}

這里 sendConnectionPreface 默認為 true,它首先調用了 writer.connectionPreface 方法,之后調用了 writer.settings 方法。最后,啟用了一個 readerRunnable 的讀取線程。

在 HTTP/2 中,每個終端都需要發送一個連接 preface 作為在使用的協議的一個最終的確認,并為 HTTP/2 連接建立初始的設定。客戶端和服務器相互發送一個不同的連接 preface。

連接 preface 以字符串 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n 開始,這個序列后面必須跟著一個 SETTINGS 幀。因此,在之后又調用了 writer.settings 方法,寫入 SETTINGS 幀。

我們先看到 connectionPreface 方法:

public synchronized void connectionPreface() throws IOException {
    if (closed) throw new IOException("closed");
    if (!client) return; // Nothing to write; servers don't send connection headers!
    if (logger.isLoggable(FINE)) {
        logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex()));
    }
    sink.write(CONNECTION_PREFACE.toByteArray());
    sink.flush();
}

這里實際上是向 HTTP/2 連接的 Socket 中寫入了 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n 這一字符串。之后我們看到 writer.settings 方法:

/**
 * Write okhttp's settings to the peer.
 */
public synchronized void settings(Settings settings) throws IOException {
    if (closed) throw new IOException("closed");
    int length = settings.size() * 6;
    byte type = TYPE_SETTINGS;
    byte flags = FLAG_NONE;
    int streamId = 0;
    frameHeader(streamId, length, type, flags);
    for (int i = 0; i < Settings.COUNT; i++) {
        if (!settings.isSet(i)) continue;
        int id = i;
        if (id == 4) {
            id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered.
        } else if (id == 7) {
            id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered.
        }
        sink.writeShort(id);
        sink.writeInt(settings.get(i));
    }
    sink.flush();
}

這里主要是寫入了一些配置的數據,其中調用了 frameHeader 寫入了幀頭。

最后我們看到 readerRunnable.execute

@Override
protected void execute() {
    ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
    ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
    IOException errorException = null;
    try {
        reader.readConnectionPreface(this);
        while (reader.nextFrame(false, this)) {
        }
        connectionErrorCode = ErrorCode.NO_ERROR;
        streamErrorCode = ErrorCode.CANCEL;
    } catch (IOException e) {
        errorException = e;
        connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
        streamErrorCode = ErrorCode.PROTOCOL_ERROR;
    } finally {
        close(connectionErrorCode, streamErrorCode, errorException);
        Util.closeQuietly(reader);
    }
}

可以看到,這里主要是調用了 reader.readConnectionPreface 方法讀取服務端發送來的 preface,并判斷是否為對應字符串,從而完成 HTTP/2 連接的啟動。

TLS 握手

接著我們看到 TLS 握手的過程,讓我們看看 connectTls 方法:

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
        // 基于之前建立的Socket建立一個包裝對象SSLSocket
        sslSocket = (SSLSocket) sslSocketFactory.createSocket(
                rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
        // 對TLS相關信息進行配置
        ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
        if (connectionSpec.supportsTlsExtensions()) {
            Platform.get().configureTlsExtensions(
                    sslSocket, address.url().host(), address.protocols());
        }
        // 進行握手
        sslSocket.startHandshake();
        // 獲取SSLSession
        SSLSession sslSocketSession = sslSocket.getSession();
        Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
        // 驗證證書對該主機是否有效
        if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
            List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
            if (!peerCertificates.isEmpty()) {
                X509Certificate cert = (X509Certificate) peerCertificates.get(0);
                throw new SSLPeerUnverifiedException(
                        "Hostname " + address.url().host() + " not verified:"
                                + "\n    certificate: " + CertificatePinner.pin(cert)
                                + "\n    DN: " + cert.getSubjectDN().getName()
                                + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
            } else {
                throw new SSLPeerUnverifiedException(
                        "Hostname " + address.url().host() + " not verified (no certificates)");
            }
        }
        address.certificatePinner().check(address.url().host(),
                unverifiedHandshake.peerCertificates());
        String maybeProtocol = connectionSpec.supportsTlsExtensions()
                ? Platform.get().getSelectedProtocol(sslSocket)
                : null;
        socket = sslSocket;
        // 獲取source及sink
        source = Okio.buffer(Okio.source(socket));
        sink = Okio.buffer(Okio.sink(socket));
        handshake = unverifiedHandshake;
        protocol = maybeProtocol != null
                ? Protocol.get(maybeProtocol)
                : Protocol.HTTP_1_1;
        success = true;
    } catch (AssertionError e) {
        if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
        throw e;
    } finally {
        if (sslSocket != null) {
            Platform.get().afterHandshake(sslSocket);
        }
        if (!success) {
            closeQuietly(sslSocket);
        }
    }
}

可以看到,這里的步驟主要是下列步驟:

  1. 基于之前建立的 Socket 建立包裝類 SSLSocket
  2. 對 TLS 相關信息進行配置
  3. 通過 SSLSocket 進行握手
  4. 驗證一些證書相關信息
  5. 獲取 sourcesink

總結

OkHttp 中采用了連接池機制實現了連接的復用,避免了每次都創建新的連接從而導致資源的浪費。獲取連接的過程主要如下:

  1. 嘗試在 transimitter 中尋找已經分配的連接
  2. transimitter 中獲取不到,嘗試從連接池中獲取連接
  3. 連接池中仍然獲取不到,嘗試進行一次路由選擇,再次從連接池中獲取連接
  4. 連接池中仍然找不到需要的連接,則創建一個新的連接
  5. 由于 HTTP/2 下采用了連接的多路復用機制,所以連接可以并行進行,因此再次嘗試從連接池中獲取連接,獲取到則丟棄創建的連接
  6. 若連接池中仍獲取不到連接,則將剛剛創建的連接放入連接池

其中,在連接池中采用了一個清理線程對超過了設定參數的空閑連接進行清理,每次清理后會計算下一次需要清理的時間并進入阻塞,每當有新連接進入或連接進入空閑時會重新喚醒該清理線程。

對于每個連接,都采用了一種類似 GC 中的引用計數法的形式,每個 RealConnection 都持有了使用它的 Transimitter 的弱引用,通過判斷持有的弱引用個數從而判斷該連接是否空閑。

OkHttp 默認將最大存活空閑連接個數設置為了 5,且每個連接空閑時間不能超過 5 分鐘,否則將被清理線程所回收

而在連接建立過程中,首先會判斷該連接是否需要 SSL 隧道,若不需要則直接建立了 Socket 并獲取了其 sourcesink,若需要則會先嘗試建立 SSL 隧道,最后再進行 Socket 連接。

Socket 連接建立成功后,會通過 establishProtocol 方法對每個協議進行不同的處理,從而對各個協議進行支持(如對 HTTPS 的支持)

參考資料

Keep-Alive

【HTTP】keep-alive

HTTP Keep-Alive模式

okhttp連接池復用機制

Okhttp對http2的支持簡單分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。