OKHttp攔截器之ConnectInterceptor連接攔截器

連接攔截器仁卷,它的作用主要是和服務(wù)器建立一個(gè)連接舞痰,只有建立連接了客戶端才能與服務(wù)端交換數(shù)據(jù),算是比較重要的一環(huán)了氨菇,我們來(lái)看一下這個(gè)攔截器的一些實(shí)現(xiàn):

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

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    // 負(fù)責(zé)管理連接儡炼、流和請(qǐng)求
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    // 有兩個(gè)實(shí)現(xiàn)類,分別是Http1Codec和Http2Codec门驾,主要是用來(lái)進(jìn)行Http請(qǐng)求和響應(yīng)的編碼/解碼操作
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    //交給下一個(gè)攔截器執(zhí)行真正的網(wǎng)絡(luò)請(qǐng)求
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

看到這里射赛,可能有人就會(huì)說(shuō)了,逗我呢奶是,這么重要的攔截器楣责,才這么幾行代碼竣灌,沒(méi)錯(cuò),本身這個(gè)攔截器沒(méi)啥東西秆麸,但是有一個(gè)很重要的類 StreamAllocation 負(fù)責(zé)管理連接初嘹、流和請(qǐng)求這三者;不知道還有沒(méi)有印象沮趣,在之前的重試攔截器中我們創(chuàng)建了一個(gè) StreamAllocation 對(duì)象屯烦,然后傳到這個(gè)連接攔截器中,然后通過(guò) StreamAllocation 來(lái)生成一個(gè) HttpCodec房铭,這個(gè)主要是用來(lái)進(jìn)行Http請(qǐng)求和響應(yīng)的編碼/解碼驻龟,看看這個(gè)方法:

public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    try {
      // 獲取可用的連接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      // 構(gòu)造一個(gè)HttpCodec,后面一個(gè)攔截器會(huì)用到
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
}

這個(gè)方法主要就是尋找一個(gè)可用的連接缸匪,然后通過(guò)找到的連接來(lái)生成一個(gè)HttpCodec翁狐,那是怎么樣去找這個(gè)可用的連接的呢?

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
    // 這里會(huì)一直去找一個(gè)可用的連接凌蔬,直到找到為止
    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) {
        // 如果是新連接的話successCount一定為0
        if (candidate.successCount == 0) {
          return candidate;
        }
      }
      // 否則的話會(huì)判斷是否是可用的連接
      // 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)) {
        // 禁止新的流被創(chuàng)建
        noNewStreams();
        continue;
      }
      return candidate;
    }
}      

可以看到砂心,這里開(kāi)了一個(gè)死循環(huán)會(huì)通過(guò) findConnection 方法一直找有沒(méi)有連接懈词,找到之后會(huì)判斷是否是可用的連接,如果可用就直接返回辩诞,否則會(huì)繼續(xù)尋找坎弯,那么問(wèn)題來(lái)了,何為可用的連接呢译暂?怎么判斷荞怒?

public boolean isHealthy(boolean doExtensiveChecks) {
    // 檢查socket的狀態(tài)
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
      return false;
    }
    // 檢查http2Connection是否關(guān)閉
    if (http2Connection != null) {
      return !http2Connection.isShutdown();
    }
    if (doExtensiveChecks) {
      // 非GET請(qǐng)求會(huì)判斷Socket的inputStream相關(guān)的read操作阻塞的等待時(shí)間
      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;
}

首先會(huì)檢查socket的狀態(tài),以及socket的input和output是否關(guān)閉了秧秉;然后看有沒(méi)有使用http2,會(huì)判斷http2連接是否關(guān)閉衰抑;最后如果是非GET請(qǐng)求的話會(huì)判斷Socket的inputStream相關(guān)的read操作阻塞的等待時(shí)間象迎;通過(guò)上述操作來(lái)判斷一個(gè)連接是否可用。再回到前面呛踊,看看findConnection 的內(nèi)部是怎么找連接的:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    
    ...
    // 判斷當(dāng)前的連接是否為空砾淌,不為空則復(fù)用當(dāng)前的
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      result = this.connection;
      releasedConnection = null;
    }
    
    if (result == null) {
      // Attempt to get a connection from the pool.
      // 嘗試從連接池中獲取一個(gè)連接,get方法是從連接池中的隊(duì)列中獲取
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
      } else {
        selectedRoute = route;
      }
    }
    ...
    // 否則嘗試切換路由
    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) {
        // 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);
          // 每切換一次路由都嘗試從連接池中尋找一個(gè)連接谭网,有的話就返回汪厨,沒(méi)有就繼續(xù)切換路由
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }
      // 最后還沒(méi)找到的話,就會(huì)構(gòu)造一個(gè)新的愉择,
      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);
        // 引用計(jì)數(shù)
        acquire(result, false);
      }
    }
    // Do TCP + TLS handshakes. This is a blocking operation.
    // 創(chuàng)建的新連接需要進(jìn)行connect操作劫乱,也就是TCP三次握手织中,阻塞操作,會(huì)判斷是否超時(shí)
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());
    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;
      // Pool the connection.
      // 連接之后同步添加到連接池衷戈,復(fù)用
      Internal.instance.put(connectionPool, result);
      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      // Http2的多路復(fù)用判斷
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
}

上述代碼比較長(zhǎng)狭吼,我們分成幾個(gè)部分來(lái)看:

  • 1、首先有幾個(gè)前置的判斷殖妇,判讀當(dāng)前連接是否釋放了刁笙,是否編碼了,是否被用戶取消了
  • 2谦趣、然后會(huì)嘗試用當(dāng)前連接(不為空)作為返回值返回
  • 3疲吸、否則的話會(huì)嘗試從連接池中獲取
  • 4、如果還沒(méi)獲取到就會(huì)嘗試切換路由前鹅,再重復(fù)從連接池中獲取
  • 5摘悴、最后如果還沒(méi)獲取到的話就會(huì)創(chuàng)建一個(gè)新的,然后進(jìn)行連接操作嫡纠,再將該連接放入連接池等待下一次被復(fù)用

這里有兩個(gè)比較重要的邏輯烦租,第一就是路由的切換,簡(jiǎn)單說(shuō)一下除盏,相信大家都知道一個(gè)域名是對(duì)應(yīng)多個(gè)IP地址的叉橱,而我們發(fā)起請(qǐng)求目標(biāo)服務(wù)器的IP是唯一一個(gè),所以需要找到我們實(shí)際請(qǐng)求的目標(biāo)服務(wù)器IP地址者蠕,而路由選擇器的作用就是幫我們找到匹配的目標(biāo)服務(wù)器IP窃祝,這個(gè)過(guò)程中DNS會(huì)幫我們解析域名服務(wù)器的IP地址信息,然后存到路由選擇器里踱侣,每次切換路由就會(huì)挨個(gè)取出來(lái)粪小,然后從連接池中取出連接將當(dāng)前的地址信息和路由中的進(jìn)行比對(duì),如果匹配的上就說(shuō)明該連接是可以拿出來(lái)復(fù)用的抡句,就不用重新構(gòu)造新的連接探膊;第二就是新創(chuàng)建的連接需要進(jìn)行 connect 操作,我們來(lái)看一下是干嘛的:

// TCP TLS待榔,區(qū)分Http1/Http2逞壁,Http2需要進(jìn)行TLS數(shù)據(jù)加密傳輸,以及握手锐锣,證書認(rèn)證等一系列操作
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
    
    // 協(xié)議已經(jīng)存在腌闯,說(shuō)明已經(jīng)連接了,拋出異常
    if (protocol != null) throw new IllegalStateException("already connected");
    if (route.address().sslSocketFactory() == null) {
      // Http1明文判斷
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      // 是否允許明文傳輸雕憔,在Android 9.0以上不允許明文傳輸姿骏,于是乎就有了網(wǎng)上的解決方案
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    }
    while (true) {
      // 判斷是使用Socket連接還是隧道連接(需要三次握手等操作)
      try {
        // 如果是Https請(qǐng)求并且使用了Http代理,就是用隧道連接的方式
        if (route.requiresTunnel()) {
          // 隧道連接
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
          // socket連接
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        // 建立協(xié)議
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;
        eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }
}

首先還是一些前置的判斷斤彼,判斷當(dāng)前協(xié)議協(xié)議是否存在分瘦,如果存在的話那么說(shuō)明已經(jīng)連接過(guò)了蘸泻,這時(shí)候會(huì)拋出異常;然后會(huì)進(jìn)行Http的明文判斷擅腰,是否允許明文蟋恬;然后會(huì)根據(jù)路由來(lái)判斷是使用Socket連接還是使用隧道連接,建立連接之后還會(huì)建立連接的協(xié)議趁冈,這個(gè)我們后面來(lái)看歼争,先來(lái)看一下Socket連接(我們一般的請(qǐng)求都不會(huì)用到代理),因?yàn)樗淼肋B接也是需要進(jìn)行Socket連接的渗勘,只不過(guò)隧道連接多了一個(gè)創(chuàng)建隧道請(qǐng)求的操作:

private void connectSocket(int connectTimeout, int readTimeout, Call call, EventListener eventListener) throws IOException {
    // 拿到代理和路由地址
    Proxy proxy = route.proxy();
    Address address = route.address();
    // 初始化socket連接沐绒,根據(jù)代理的類型來(lái)判斷是直接連還是使用代理連
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);
    eventListener.connectStart(call, route.socketAddress(), proxy);
    // 讀取數(shù)據(jù)時(shí)阻塞鏈路的超時(shí)時(shí)間
    rawSocket.setSoTimeout(readTimeout);
    try {
      // 打開(kāi)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;
    }
    try {
      // 使用Okio來(lái)進(jìn)行數(shù)據(jù)的讀寫(數(shù)據(jù)交換)操作
      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);
      }
    }
}

首先會(huì)拿到代理和路由地址的信息,因?yàn)樾枰鶕?jù)是否有代理來(lái)創(chuàng)建不同的Socket旺坠,然后設(shè)置一下超時(shí)時(shí)間乔遮,最后通過(guò) connectSocket 方法(會(huì)調(diào)用Socket的connect方法)打開(kāi)一個(gè)Socket連接,連接完成之后最重要的就是數(shù)據(jù)的交換了取刃,這里都交給Okio的Source和Sink來(lái)完成蹋肮。好,現(xiàn)在再回過(guò)頭來(lái)看看建立連接之后是怎么建立協(xié)議的:

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
    // Http1
    if (route.address().sslSocketFactory() == null) {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
      return;
    }
    eventListener.secureConnectStart(call);
    // 連接TLS
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);
    // Http2
    if (protocol == Protocol.HTTP_2) {
      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();
    }
}

因?yàn)槲覀僅ttp1和Http2的請(qǐng)求不太一樣璧疗,所以建立的協(xié)議也不太一樣坯辩,總的來(lái)說(shuō)Http2請(qǐng)求會(huì)復(fù)雜一點(diǎn),Http2請(qǐng)求會(huì)建立TLS協(xié)議崩侠,也就是我們通常說(shuō)的加密傳輸漆魔,這個(gè)階段會(huì)進(jìn)行TLS握手以及證書的驗(yàn)證等等。

OKHttp其他攔截器詳細(xì)的說(shuō)明却音,可以看我Github上的項(xiàng)目

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末改抡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子系瓢,更是在濱河造成了極大的恐慌阿纤,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夷陋,死亡現(xiàn)場(chǎng)離奇詭異阵赠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)肌稻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)匕荸,“玉大人爹谭,你說(shuō)我怎么就攤上這事¢簧Γ” “怎么了诺凡?”我有些...
    開(kāi)封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵东揣,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我腹泌,道長(zhǎng)嘶卧,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任凉袱,我火速辦了婚禮芥吟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘专甩。我一直安慰自己钟鸵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布涤躲。 她就那樣靜靜地躺著棺耍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪种樱。 梳的紋絲不亂的頭發(fā)上蒙袍,一...
    開(kāi)封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音嫩挤,去河邊找鬼害幅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俐镐,可吹牛的內(nèi)容都是我干的矫限。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼佩抹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼叼风!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起棍苹,我...
    開(kāi)封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤无宿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后枢里,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體孽鸡,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年栏豺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了彬碱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奥洼,死狀恐怖巷疼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情灵奖,我是刑警寧澤嚼沿,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布估盘,位于F島的核電站,受9級(jí)特大地震影響骡尽,放射性物質(zhì)發(fā)生泄漏遣妥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一攀细、第九天 我趴在偏房一處隱蔽的房頂上張望箫踩。 院中可真熱鬧,春花似錦辨图、人聲如沸班套。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吱韭。三九已至,卻和暖如春鱼的,著一層夾襖步出監(jiān)牢的瞬間理盆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工凑阶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留猿规,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓宙橱,卻偏偏與公主長(zhǎng)得像姨俩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子师郑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容