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

系列索引

本系列文章基于 OkHttp3.14

OkHttp 源碼剖析系列(一)——請(qǐng)求的發(fā)起及攔截器機(jī)制概述

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

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

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

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

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

OkHttp 源碼剖析系列(七)——請(qǐng)求的發(fā)起及響應(yīng)的讀取

前言

前面的文章分析完了 OkHttp 中的緩存機(jī)制睬辐,現(xiàn)在讓我們繼續(xù)來研究其在 ConnectInterceptor 中所進(jìn)行的連接建立的相關(guān)原理。由于連接建立的過程涉及到很多在 OkHttp 中非常重要的機(jī)制锐峭,因此將分為多篇文章進(jìn)行介紹,這篇文章主要是對(duì)連接建立的大體流程進(jìn)行介紹分瘦。

連接建立流程概述

ConnectInterceptor.intercept 方法中真正實(shí)現(xiàn)了連接的建立的代碼如下:

// 如果請(qǐng)求是GET格式骄恶,需要一些額外的檢查
boolean doExtensiveHealthChecks = !request.method().equals("GET");
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

根據(jù)上面的代碼我們可以推測(cè),這個(gè) Exchange 類與我們的連接是有一些關(guān)系的梅忌,真正連接的建立過程在 transmitter.newExchange 中實(shí)現(xiàn)狰腌。

我們看到 transmitter.newExchange 方法:

/**
 * Returns a new exchange to carry a new request and response.
 */
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對(duì)象
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    // 通過找到的codec對(duì)象構(gòu)建Exchange對(duì)象
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
    // 進(jìn)行一些變量的賦值
    synchronized (connectionPool) {
        this.exchange = result;
        this.exchangeRequestDone = false;
        this.exchangeResponseDone = false;
        return result;
    }
}

獲取連接

上面首先通過 exchangeFinder.find 方法進(jìn)行了對(duì) ExchangeCodec 的查找,找到對(duì)應(yīng)的 ExchangeCodec 對(duì)象牧氮,之后通過這個(gè) codec 對(duì)象構(gòu)建了一個(gè) Exchange 對(duì)象并返回

那么什么是 ExchangeCodec 對(duì)象呢琼腔?我們先看到 exchangeFinder.find 方法:

public ExchangeCodec find(
        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 {
        RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
        return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
        trackFailure();
        throw e;
    } catch (IOException e) {
        trackFailure();
        throw new RouteException(e);
    }
}

可以看到這里調(diào)用到了 findHealthyConnection 方法從而獲取 RealConnection 對(duì)象,看來這個(gè)就是我們的連接了踱葛,之后調(diào)用了 RealConnection.newCodec 方法獲取 ExchangeCodec 對(duì)象丹莲。

尋找可用連接

我們先看到 findHealthyConnection 方法:

/**
 * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
 * until a healthy connection is found.
 */
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                             int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
                                             boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
        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)) {
            candidate.noNewExchanges();
            continue;
        }
        return candidate;
    }
}

可以看到這里是一個(gè)循環(huán),不斷地在調(diào)用 findConnection 方法尋找連接尸诽,若找不到 Healthy(可用)的連接圾笨,則繼續(xù)循環(huán)直到找到為止。

尋找連接

我們先看到 findConnection 方法:

/**
 * Returns a connection to host a new stream. This prefers the existing connection if it exists,
 * then the pool, finally building a new connection.
 */
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) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");
        hasStreamFailure = false; // This is a fresh attempt.
        
        // 嘗試使用之前已分配的連接逊谋,但可能該連接不能用來創(chuàng)建新的Exchange
        releasedConnection = transmitter.connection;
        // 如果當(dāng)前的連接不能被用來創(chuàng)建新的Exchange擂达,則將連接釋放并返回對(duì)應(yīng)Socket準(zhǔn)備close
        toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
                ? transmitter.releaseConnectionNoEvents()
                : null;
        if (transmitter.connection != null) {
            // 存在已分配的連接,將其置為result,并置releasedConnection為null
            result = transmitter.connection;
            releasedConnection = null;
        }
        if (result == null) {
            // 如果不存在已經(jīng)分配的連接板鬓,則嘗試從連接池中獲取連接
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            } else if (nextRouteToTry != null) {
                // 修改當(dāng)前選擇路由為下一個(gè)路由
                selectedRoute = nextRouteToTry;
                nextRouteToTry = null;
            } else if (retryCurrentRoute()) {
                // 如果當(dāng)前Connection的路由應(yīng)當(dāng)重試悲敷,則將選擇的路由設(shè)置為當(dāng)前路由
                selectedRoute = transmitter.connection.route();
            }
        }
    }
    closeQuietly(toClose);
    if (releasedConnection != null) {
        eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
        // 如果已經(jīng)找到了已分配的或從連接池中取出的Connection,則直接返回
        return result;
    }
    // 如果需要進(jìn)行路由選擇俭令,則進(jìn)行一次路由選擇
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        routeSelection = routeSelector.next();
    }
    List<Route> routes = null;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");
        if (newRouteSelection) {
            // 路由選擇過后如今有了一組IP地址后德,我們?cè)俅螄L試從連接池中獲取連接
            routes = routeSelection.getAll();
            if (connectionPool.transmitterAcquirePooledConnection(
                    address, transmitter, routes, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            }
        }
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
            // 如果第二次嘗試從連接池獲取連接仍然失敗,則創(chuàng)建新的連接抄腔。
            result = new RealConnection(connectionPool, selectedRoute);
            connectingConnection = result;
        }
    }
    if (foundPooledConnection) {
        // 如果第二次嘗試從連接池獲取連接成功瓢湃,則將其返回
        eventListener.connectionAcquired(call, result);
        return result;
    }
    // 執(zhí)行TCP+TLS握手,這是個(gè)阻塞的過程
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
            connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());
    Socket socket = null;
    synchronized (connectionPool) {
        connectingConnection = null;
        // 最后一次嘗試從連接池中獲取連接赫蛇,這種情況只可能在一個(gè)host下多個(gè)并發(fā)連接這種情況下
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            // 如果成功拿到則關(guān)閉我們前面創(chuàng)建的連接的Socket绵患,并返回連接池中的連接
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
        } else {
            // 如果失敗則在連接池中放入我們剛剛創(chuàng)建的連接,并將其設(shè)置為transmitter中的連接
            connectionPool.put(result);
            transmitter.acquireConnectionNoEvents(result);
        }
    }
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    return result;
}

這個(gè)尋找連接的過程是非常復(fù)雜的悟耘,主要是下列幾個(gè)步驟:

  1. 嘗試獲取 transmitter 中已經(jīng)存在的連接落蝙,也就是當(dāng)前 Call 之前創(chuàng)建的連接。
  2. 若獲取不到暂幼,則嘗試從連接池中調(diào)用 transmitterAcquirePooledConnection 方法獲取連接筏勒,傳入的 routes 參數(shù)為 null
  3. 若仍獲取不到連接,判斷是否需要路由選擇旺嬉,如果需要管行,調(diào)用 routeSelector.next 進(jìn)行路由選擇
  4. 如果進(jìn)行了路由選擇,則再次嘗試從連接池中調(diào)用 transmitterAcquirePooledConnection 方法獲取連接邪媳,傳入的 routes 為剛剛路由選擇后所獲取的路由列表
  5. 若仍然獲取不到連接捐顷,則調(diào)用 RealConnection 的構(gòu)造函數(shù)創(chuàng)建新的連接,并對(duì)其執(zhí)行 TCP + TLS握手悲酷。
  6. TCP + TSL握手之后套菜,會(huì)再次嘗試從連接池中通過 transmitterAcquirePooledConnection 方法獲取連接亲善,這種情況只會(huì)出現(xiàn)在一個(gè) Host 對(duì)應(yīng)多個(gè)并發(fā)連接的情況下(因?yàn)?HTTP/2 支持了多路復(fù)用设易,使得多個(gè)請(qǐng)求可以并發(fā)執(zhí)行,此時(shí)可能有其他使用該 TCP 連接的請(qǐng)求也創(chuàng)建了連接蛹头,就不需要重新創(chuàng)建了)顿肺。
  7. 若最后一次從連接池中獲取連接獲取成功,會(huì)釋放之前創(chuàng)建的連接的相關(guān)資源渣蜗。
  8. 若仍獲取不到屠尊,則將該連接放入連接池,并將其設(shè)置為 transmitter 的連接耕拷。

可以看到讼昆,尋找連接的過程主要被分成了三種行為,分別是

  • 嘗試獲取 transmitter 中已經(jīng)分配的連接
  • 嘗試從線程池中調(diào)用 transmitterAcquirePooledConnection 獲取連接
  • 創(chuàng)建新連接骚烧。

有點(diǎn)類似圖片加載的三級(jí)緩存浸赫,顯然自上而下是越來越消耗資源的闰围,因此 OkHttp 更偏向于前面直接能夠獲取到連接,尤其是嘗試從連接池進(jìn)行獲取連接這一操作進(jìn)行了三次既峡。

不過我們現(xiàn)在只是知道了大體流程羡榴,還有許多疑問沒有解開。比如路由選擇是怎樣的运敢?OkHttp 中的連接池是如何實(shí)現(xiàn)的校仑?連接的建立過程是如何實(shí)現(xiàn)的?等等疑問都還沒有解開传惠,我們將在后續(xù)文章中介紹到迄沫。

判斷連接是否可用

我們接著看看 RealConnection.isHealthy 的實(shí)現(xiàn),看看它是如何判斷一個(gè)連接是否可用的:

/**
 * Returns true if this connection is ready to host new streams.
 */
public boolean isHealthy(boolean doExtensiveChecks) {
    // 判斷Socket是否可用
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
        return false;
    }
    // 如果包含Http2的連接涉枫,檢測(cè)是否shutdown
    if (http2Connection != null) {
        return !http2Connection.isShutdown();
    }
    if (doExtensiveChecks) {
        try {
            int readTimeout = socket.getSoTimeout();
            try {
                // 設(shè)置一秒延時(shí)邢滑,檢測(cè)Stream是否枯竭,若枯竭則該連接不可用
                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;
}

可以看到愿汰,上面主要是對(duì) Socket困后、HTTP2連接Stream 進(jìn)行了檢測(cè)衬廷,從而判斷該連接是否可用摇予。

什么是 Exchange

現(xiàn)在我們已經(jīng)知道了連接究竟是如何尋找到的,現(xiàn)在讓我們回到 Exchange 類吗跋,讓我們研究一下究竟什么是 Exchange侧戴,它是用來做什么的。

讓我們先從它的 JavaDoc 看到:

Transmits a single HTTP request and a response pair. This layers connection management and events
on {@link ExchangeCodec}, which handles the actual I/O.

可以看到跌宛,這里講到酗宋,Exchange 是一個(gè)用于發(fā)送 HTTP 請(qǐng)求和讀取響應(yīng)的類,而真正進(jìn)行 I/O 的類是它的一個(gè)成員變量——ExchangeCodec 疆拘。在 Exchange 中暴露了許多對(duì) Stream 進(jìn)行讀寫的方法蜕猫,如 writeRequestHeaderscreateRequestBody 等等哎迄,在 CallServerInterceptor 中就會(huì)通過 Exchange 回右,向服務(wù)器發(fā)起請(qǐng)求,并讀取其所返回的響應(yīng)漱挚。

什么是 ExchangeCodec

讓我們看看 ExchangeCodec 又是什么:

/**
 * Encodes HTTP requests and decodes HTTP responses.
 */
public interface ExchangeCodec {
   
    int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
    
    RealConnection connection();
   
    Sink createRequestBody(Request request, long contentLength) throws IOException;
    
    void writeRequestHeaders(Request request) throws IOException;
  
    void flushRequest() throws IOException;
   
    void finishRequest() throws IOException;
   
    @Nullable
    Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;
   
    long reportedContentLength(Response response) throws IOException;
    
    Source openResponseBodySource(Response response) throws IOException;
    
    Headers trailers() throws IOException;
   
    void cancel();
}

可以看到翔烁,它僅僅是個(gè)接口,根據(jù)上面的 JavaDoc 可以看出旨涝,它的作用是用于對(duì)請(qǐng)求進(jìn)行編碼蹬屹,以及對(duì)響應(yīng)進(jìn)行解碼

我們看看它有哪些實(shí)現(xiàn)類,通過 Android Studio 我們可以很容易找到它有如下兩個(gè)實(shí)現(xiàn)類:

  • Http1ExchangeCodec
  • Http2ExchangeCodec

看得出來慨默,OkHttp 采用了一種非常典型的面向接口編程秃踩,將對(duì) Http 請(qǐng)求的編碼及解碼等功能抽象成了接口,再通過不同的實(shí)現(xiàn)類來實(shí)現(xiàn)將相同的 Request 對(duì)象編碼為 HTTP1 及 HTTP2 的格式的數(shù)據(jù)业筏,將 HTTP1 及 HTTP2 格式的數(shù)據(jù)解碼為相同格式的 Response 對(duì)象憔杨。通過這樣的一種面向接口的設(shè)計(jì),大大地提高了 OkHttp 的可擴(kuò)展性蒜胖,可以通過實(shí)現(xiàn)接口的形式對(duì)更多的應(yīng)用層進(jìn)行支持消别。

什么是 Transmitter

接下來我們看看貫穿了我們整個(gè)請(qǐng)求流程的 Transimitter,究竟是一個(gè)用來做什么的類台谢。我們先從 JavaDoc 入手:

Bridge between OkHttp's application and network layers. This class exposes high-level application
layer primitives: connections, requests, responses, and streams.

根據(jù)上面的注釋可以看出寻狂,Transmitter 是一座 OkHttp 中應(yīng)用層與網(wǎng)絡(luò)層溝通的橋梁。就像我們之前的連接創(chuàng)建朋沮,就是在應(yīng)用層通過了 transmitter.newExchange 方法來通知網(wǎng)絡(luò)層進(jìn)行 Exchange 的獲取蛇券,并返回給應(yīng)用層。那么 Transmitter 是什么時(shí)候創(chuàng)建的呢樊拓?

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
}

可以看到纠亚,它是在 RealCall 被創(chuàng)建的時(shí)候進(jìn)行創(chuàng)建的,也就是說一個(gè) Trasmitter 對(duì)應(yīng)了一個(gè) Call筋夏。這個(gè) Call 在應(yīng)用層通過 trasnmitter 與它的網(wǎng)絡(luò)層進(jìn)行通信蒂胞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市条篷,隨后出現(xiàn)的幾起案子骗随,更是在濱河造成了極大的恐慌,老刑警劉巖赴叹,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸿染,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡乞巧,警方通過查閱死者的電腦和手機(jī)涨椒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摊欠,“玉大人丢烘,你說我怎么就攤上這事柱宦⌒┙罚” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵掸刊,是天一觀的道長(zhǎng)免糕。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么石窑? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任牌芋,我火速辦了婚禮,結(jié)果婚禮上松逊,老公的妹妹穿的比我還像新娘躺屁。我一直安慰自己,他們只是感情好经宏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布犀暑。 她就那樣靜靜地躺著,像睡著了一般烁兰。 火紅的嫁衣襯著肌膚如雪耐亏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天沪斟,我揣著相機(jī)與錄音广辰,去河邊找鬼。 笑死主之,一個(gè)胖子當(dāng)著我的面吹牛择吊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播槽奕,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼干发,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了史翘?” 一聲冷哼從身側(cè)響起枉长,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎琼讽,沒想到半個(gè)月后必峰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钻蹬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年吼蚁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片问欠。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肝匆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出顺献,到底是詐尸還是另有隱情旗国,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布注整,位于F島的核電站能曾,受9級(jí)特大地震影響度硝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寿冕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一蕊程、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驼唱,春花似錦藻茂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至纽窟,卻和暖如春肖油,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背臂港。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工森枪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人审孽。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓县袱,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親佑力。 傳聞我的和親對(duì)象是個(gè)殘疾皇子式散,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354