OkHttp 源碼深入分析(二)

一、前言

上一篇文章OkHttp 源碼深入分析(一)讓我們了解了 OKHttp 從初始化到建立連接的一系列過程龄捡,同時也對 OkHttp 的幾個關(guān)鍵類以及前幾個攔截器汁果,尤其是 ConnectInterceptor 攔截器有了更深的了解湃望,接下來我還會接著上一篇繼續(xù)分析后續(xù)的過程.

二驶赏、ConnectInterceptor 中的隱藏 socket 連接和 DNS

Socket 在哪連接的耕驰, DNS 解析好像也沒看到爷辱, ExchangeCodec實例對象是怎么創(chuàng)建的?

這是上一篇文章的最后我拋出的幾個問題朦肘,接下來我會一一解答饭弓,首先讓我們回到 findConnection 方法

final class ExchangeFinder {

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {

   //省略若干代碼...

   // 如果上面的條件都沒有獲取到連接,那么說明我們要創(chuàng)建一個新的連接
    //并通過 DNS 解析獲取要連接的 route
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

     // 做 TCP+TLS 握手媒抠,注意這是一個阻塞操作
    //到這一步執(zhí)行完整個連接算是真正的建立了
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    //省略若干代碼...
    return result;
  }
}
2.1弟断、DNS 解析

代碼做了精簡,我們首先看看 DNS 解析的地方趴生,在分析之前先說說幾個類,我們在前面分析代碼的時候總會看見這幾個類如下

  • class RouterSelector
  • class Selection
  • class Route

這幾個類其實做的事情并不復(fù)雜阀趴, RouterSelector 的主要作用是為了幫我門獲取 DNS 解析的 IP 地址,內(nèi)部有個 next 方法代碼如下

 public Selection next() throws IOException {
   //省略若干代碼...
   
    List<Route> routes = new ArrayList<>();
    while (hasNextProxy()) {//循環(huán)遍歷所有的代理方式
    
      //DNS 解析
      Proxy proxy = nextProxy();
      //遍歷解析的地址苍匆,添加到 Route 集合中
      for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
        Route route = new Route(address, proxy, inetSocketAddresses.get(i));
        if (routeDatabase.shouldPostpone(route)) {
          postponedRoutes.add(route);
        } else {
          routes.add(route);
        }
      }

      if (!routes.isEmpty()) {
        break;
      }
    
    }
    //將解析的地址包裝成 Selection 對象返回
    return new Selection(routes);
}

Selection 對象是 RouteSelector 的一個靜態(tài)類刘急,有一個關(guān)鍵方法 next 用來獲取 route, 代碼很簡單就不貼了,繼續(xù)進入到 nextProxy 方法后發(fā)現(xiàn)最終調(diào)用的是 resetNextInetSocketAddress 方法

/** Prepares the socket addresses to attempt for the current proxy or host. */
  private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    inetSocketAddresses = new ArrayList<>();
        
     //調(diào)用系統(tǒng) DNS 獲取所有地址
      List<InetAddress> addresses = address.dns().lookup(socketHost);
     
    
      //將解析的地址放入地址列表中
      for (int i = 0, size = addresses.size(); i < size; i++) {
        InetAddress inetAddress = addresses.get(i);
        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
      }
    }
  }

代碼很多這里我們只看關(guān)鍵的代碼浸踩,經(jīng)過這么多層的兜兜轉(zhuǎn)轉(zhuǎn)終于是拿到了 IP 地址叔汁,這些 IP 地址最終會變成一個 Route 集合被包裝成 Selection 對象供 Socket 使用,而這個 DNS 的實例對象其實是在創(chuàng)建 OkHttpClient 的時候通過 builder 構(gòu)建出來的检碗,我當初找到這里的時候就納悶据块,這個 DNS 哪里冒出來的,沒看到在那創(chuàng)建的啊折剃,找了半天才發(fā)現(xiàn)原來隱藏在初始化的過程里另假。至于 Router 對象則是對我們請求信息的一個封裝,包含我們請求地址怕犁,代理類型浪谴,和解析后的 IP 地址的封裝類

2.2 Socket 連接和 ExchangeCodec 的創(chuàng)建

在上面代碼中可以發(fā)現(xiàn) Scoket 的連接是通過 result.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);

    //省略大量的 address 判斷

    while (true) {
      try {
        //判斷是否是 Http 代理的一個 Https 請求
        if (route.requiresTunnel()) {
         //連接 Https 隧道?
          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);
        }
        //處理 https 的 TLS 建立
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, 
        //省略若干代碼
        break;
      } catch (IOException e) {
        //省略若干代碼...
      }
    }

上面的代碼基本都是 http/https 的具體協(xié)議處理過程因苹,這里就不過多解讀了(主要還是我本人網(wǎng)絡(luò)底子也不好就不獻丑了)苟耻,而且不影響我們理解整體流程,不過還是要說明一下扶檐, connectTunnel 最終還是會調(diào)用 connectSocket 方法凶杖, connectSocket 內(nèi)部會創(chuàng)建 Socket 的實例并創(chuàng)建 I/O 流進行關(guān)聯(lián),代碼如下

/** 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();

    //創(chuàng)建 Socket 實例
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);
    
    rawSocket.setSoTimeout(readTimeout);
    //這里開始根據(jù) route 提供的地址和目標服務(wù)器建立連接
    Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
  
  
    try {
      //創(chuàng)建連接所需要的 I/O 流款筑,負責(zé)寫入請求的 header智蝠、body 讀取響應(yīng)的 header腾么、 body
      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);
      }
    }
  }

接下來我們重點看看 establishProtocol 方法

 private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
      // 如果SSL 協(xié)議不為空且是 HTTP2 連接 
    if (route.address().sslSocketFactory() == null) {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        //協(xié)議標記為 http2
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        //創(chuàng)建一個 Http2Connection 對象并啟動
        startHttp2(pingIntervalMillis);
        return;
      }
        
      socket = rawSocket;
      //協(xié)議標記為 http1
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    //建立 TLS 鏈接
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

代碼不多,也很好理解杈湾,establishProtocol 內(nèi)部除了處理 Https 連接的問題解虱,更重要的是會判斷是否采用 Http2 連接并創(chuàng)建對應(yīng)的 Http2Connection 對象,這也為后面 創(chuàng)建 ExchangeCodec 實例對象提供了判斷依據(jù)漆撞。

當 establishProtocol 方法執(zhí)行完后所有跟 scoket 連接的大致流程也就分析完了殴泰,所以讓我們回過頭在看看獲取 ExchangeCodec 實例對象的地方

final class ExchangeFinder {
    public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
        //省略若干代碼...
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      return resultConnection.newCodec(client, chain);
      //省略若干代碼
  }
}

可以看到創(chuàng)建 ExChangeCodec 實例對象的方法是 RealConnection 對象的 newCodec 方法,進入此方法看看

ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
    if (http2Connection != null) {
      return new Http2ExchangeCodec(client, this, chain, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1ExchangeCodec(client, this, source, sink);
    }
  }

經(jīng)過前面的一大串分析浮驳,終于看到了 ExchangeCodec 實例對象的創(chuàng)建邏輯悍汛,上面的代碼很簡單就是判斷 http2Connection 對象是否為 null ,以此為條件判斷創(chuàng)建對應(yīng)的 ExchangeCodec 對象

三至会、CallServerInterceptor

經(jīng)過了那么大篇幅的分析离咐,終于, 我們來到了最后一個攔截器,相比于 ConnectInterceptor 攔截器奉件,CallServerInterceptor 攔截器就容易理解多了宵蛀,因為和目標服務(wù)器的連接已經(jīng)建立了,接下來就是通過 ConnectInterceptor 傳過來的 Exchange 對象將我們的 header县貌、body 通過流寫入糖埋,再將返回的 response 讀取,代碼如下

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();
    
    //將請求頭信息寫入 stream 中
    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    //非 GET 請求且 body 不為空
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // 忽略 100-continue 頭域
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();//將請求頭信息流推出
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        //讀取響應(yīng)信息
        responseBuilder = exchange.readResponseHeaders(true);
      }

     //如果服務(wù)端沒有返回響應(yīng)則創(chuàng)建一個 requestBody 寫入
      if (responseBuilder == null) {
        if (request.body().isDuplex()) { // http2 協(xié)議
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else { //http1 協(xié)議
          //如果“Expect: 100-continue”的期望滿足窃这,則寫入請求體。
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      }//省略部分代碼
    } else {
      exchange.noRequestBody();
    }

    //省略部分代碼...
    
    //讀取響應(yīng)信息
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }
    
    //構(gòu)建獲取的響應(yīng)
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    
    //省略部分代碼
    return response;
  }

代碼進行了精簡征候,展示了主要邏輯杭攻,可以看到 CallServerInterceptor 的內(nèi)部邏輯并不復(fù)雜,就是通過 Exchange 內(nèi)部封裝的 ExchangeCodec 對象疤坝,對 request 和 response 進行寫入兆解、讀取而已,然后將從服務(wù)端獲取的信息構(gòu)建成 response 返回跑揉。

此時再看下面的代碼是不是覺得更清晰了呢

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
                .url("https://raw.github.com/square/okhttp/master/README.md")
                .build();
                
Response response = client.newCall(request).execute()

System.out.println("OKHTTP : " + response.body().string());

四锅睛、結(jié)語

經(jīng)過漫長的分析,終于將所有的攔截器走了一個遍历谍,此時回想整個過程现拒,既有抓耳撓腮的煩躁,也有恍然大悟的愉悅望侈,翻到前面又仔細看了一眼開篇的流程圖印蔬,不禁會心一笑,希望讀到這里的朋友也是如此脱衙。 完結(jié)撒花~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侥猬,一起剝皮案震驚了整個濱河市例驹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌退唠,老刑警劉巖鹃锈,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瞧预,居然都是意外死亡屎债,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門松蒜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扔茅,“玉大人,你說我怎么就攤上這事秸苗≌倌龋” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵惊楼,是天一觀的道長玖瘸。 經(jīng)常有香客問我,道長檀咙,這世上最難降的妖魔是什么雅倒? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮弧可,結(jié)果婚禮上蔑匣,老公的妹妹穿的比我還像新娘。我一直安慰自己棕诵,他們只是感情好裁良,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著校套,像睡著了一般价脾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笛匙,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天侨把,我揣著相機與錄音,去河邊找鬼妹孙。 笑死秋柄,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的蠢正。 我是一名探鬼主播华匾,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜘拉?” 一聲冷哼從身側(cè)響起萨西,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎旭旭,沒想到半個月后谎脯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡持寄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年源梭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稍味。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡废麻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出模庐,到底是詐尸還是另有隱情烛愧,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布掂碱,位于F島的核電站怜姿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏疼燥。R本人自食惡果不足惜沧卢,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望醉者。 院中可真熱鬧但狭,春花似錦、人聲如沸撬即。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搞莺。三九已至,卻和暖如春掂咒,著一層夾襖步出監(jiān)牢的瞬間才沧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工绍刮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留温圆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓孩革,卻偏偏與公主長得像岁歉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

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