探究OkHttpClient的運行原理(2---RetryAndFollowUpInterceptor)

上一篇文章我們分析了 OkHttpClient 創(chuàng)建請求哑子,以及相關(guān)隊列操作的一些方法。具體可查看
探究Okhttp的運行原理(1)
此篇文章我們繼續(xù)來看 OkHttpClient 另外一個重要的流程getResponseWithInterceptorChain() 方法去獲取請求響應(yīng)的乾蓬。

getResponseWithInterceptorChain 方法

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);// RetryAndFollowUpInterceptor 攔截器
    interceptors.add(new BridgeInterceptor(client.cookieJar())); // BridgeInterceptor 攔截器
    interceptors.add(new CacheInterceptor(client.internalCache())); // CacheInterceptor 攔截器
    interceptors.add(new ConnectInterceptor(client)); // ConnectInterceptor 攔截器
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket)); // CallServerInterceptor 攔截器

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis()); // 初始化 RealInterceptorChain 內(nèi)部變量

    return chain.proceed(originalRequest); // 執(zhí)行
  }

getResponseWithInterceptorChain 加入了五個主要的攔截器剥懒,攔截器即攔截請求鱼鸠,做一些對應(yīng)的處理;

了解攔截器工作原理之前色迂,首先看下 RealInterceptorChain 類,Okhttp 通過 RealInterceptorChain 使用責(zé)任鏈模式處理下發(fā)的請求手销;

RealInterceptorChain 類內(nèi)部維護了 Request 請求歇僧、interceptors 攔截器容器、當(dāng)前攔截器(初始為0);

RealInterceptorChain

public final class RealInterceptorChain implements Interceptor.Chain {
  private final List<Interceptor> interceptors;
  private final StreamAllocation streamAllocation;
  private final HttpCodec httpCodec;
  private final RealConnection connection;
  private final int index;
  private final Request request;
  private final Call call;
  private final EventListener eventListener;
  private final int connectTimeout;
  private final int readTimeout;
  private final int writeTimeout;
  private int calls;

  public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
      HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,
      EventListener eventListener, int connectTimeout, int readTimeout, int writeTimeout) {
    this.interceptors = interceptors;
    this.connection = connection;
    this.streamAllocation = streamAllocation;
    this.httpCodec = httpCodec;
    this.index = index;
    this.request = request;
    this.call = call;
    this.eventListener = eventListener;
    this.connectTimeout = connectTimeout;
    this.readTimeout = readTimeout;
    this.writeTimeout = writeTimeout;
  }

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    ......

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);  // 創(chuàng)建下一個RealInterceptorChain 
    Interceptor interceptor = interceptors.get(index);// 獲取到 index 的攔截器
    Response response = interceptor.intercept(next); // 執(zhí)行攔截器的 intercept
   ......

    return response;
  }

攔截器調(diào)用 proceed 方法主要做了一下操作:
1 诈悍、創(chuàng)建下一個攔截器(傳參為 index + 1)祸轮;
2、 獲取當(dāng)前 index 值(初始為 0)的攔截器侥钳,第一個即為 RetryAndFollowUpInterceptor 攔截器
3适袜、執(zhí)行 RetryAndFollowUpInterceptor 的 intercept 方法;

intercept 方法具有傳遞效果(即鏈?zhǔn)絺鬟f給下一個攔截器)舷夺,在 RetryAndFollowUpInterceptor 內(nèi)部苦酱,會再次調(diào)用 RealInterceptorChain (即傳入的 next 參數(shù))的 proceed 方法,index 會繼續(xù)自增给猾,拿到下一個攔截器疫萤,從而完成責(zé)任傳遞效果;

直到最終返回結(jié)果的處理耙册,過程如下圖所示:

攔截器.png

接下來给僵,分析第一個攔截器的 intercept 方法 RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 請求重定向攔截器

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request(); // 獲取請求
    RealInterceptorChain realChain = (RealInterceptorChain) chain;// 獲取攔截器
    Call call = realChain.call(); // 獲取 RealCall 對象
    EventListener eventListener = realChain.eventListener();// 獲取監(jiān)聽事件

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation; // 創(chuàng)建 streamAllocation 實例

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) { // 請求取消 釋放資源
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Request followUp = followUpRequest(response, streamAllocation.route()); // 如果地址被重定向,重新組裝重定向的請求

      if (followUp == null) {// 沒有重定向 釋放資源
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      closeQuietly(response.body());

      if (++followUpCount > MAX_FOLLOW_UPS) { // 重定向超過一定次數(shù) 釋放資源
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      if (followUp.body() instanceof UnrepeatableRequestBody) { 
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }

      if (!sameConnection(response, followUp.url())) { // 不是同一個連接详拙,重定向操作 重新創(chuàng)建 StreamAllocation 實例
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
      } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }

      request = followUp;
      priorResponse = response;
    }
  }

RetryAndFollowUpInterceptor 攔截器主要去處理請求的重定向相關(guān)操作帝际,攔截器先創(chuàng)建了 StreamAllocation 實例對象,而后通過調(diào)用 followUpRequest 方法去查看相應(yīng)返回 response 數(shù)據(jù)饶辙,判斷是否需要進(jìn)行重定向蹲诀;

  private Request followUpRequest(Response userResponse, Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      ......
      case HTTP_MULT_CHOICE: // 300
      case HTTP_MOVED_PERM:// 301
      case HTTP_MOVED_TEMP:// 302
      case HTTP_SEE_OTHER: // 302
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // Most redirects don't include a request body.
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();
        ......
    }
  }

當(dāng)返回碼 300、301弃揽、302脯爪、303 時代表請求需要重定向,此時重新構(gòu)建 Request 請求并進(jìn)行返回矿微;

當(dāng) RetryAndFollowUpInterceptor 攔截器發(fā)現(xiàn)請求需要重定向的時候痕慢。即 followUpRequest 返回的 Requset 不為空的時候,會重新創(chuàng)建 StreamAllocation 實例對象涌矢;

這里分析下 StreamAllocation 對象掖举,后續(xù)的攔截器會使用到;

StreamAllocation

實例方法
new StreamAllocation(client.connectionPool(),createAddress(request.url()), call, eventListener, callStackTrace);

client.connectionPool() ------主要維護 RealConnection 類的隊列娜庇;

createAddress ------- 維護請求地址的相關(guān)信息

  private Address createAddress(HttpUrl url) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (url.isHttps()) {
      sslSocketFactory = client.sslSocketFactory();
      hostnameVerifier = client.hostnameVerifier();
      certificatePinner = client.certificatePinner();
    }

    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector()); // 包括 host 塔次、port 、dns 等數(shù)據(jù)
  }

call ------ RealCall 實例

eventListener ------ 監(jiān)聽實例

callStackTrace ------ 記錄

看下 StreamAllocation 類名秀;

public final class StreamAllocation {
  public final Address address;
  private RouteSelector.Selection routeSelection;
  private Route route;
  private final ConnectionPool connectionPool;
  public final Call call;
  public final EventListener eventListener;
  private final Object callStackTrace;

  // State guarded by connectionPool.
  private final RouteSelector routeSelector;
  private int refusedStreamCount;
  private RealConnection connection;
  private boolean reportedAcquired;
  private boolean released;
  private boolean canceled;
  private HttpCodec codec;

  public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
      EventListener eventListener, Object callStackTrace) {
    this.connectionPool = connectionPool;
    this.address = address;
    this.call = call;
    this.eventListener = eventListener;
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
    this.callStackTrace = callStackTrace;
  }
}

StreamAllocation 主要維護了上述變量的相關(guān)方法励负,這里先存儲著,等到后續(xù)攔截器進(jìn)行使用匕得;

RetryAndFollowUpInterceptor 的攔截器到這里就分析完了继榆,總結(jié) RetryAndFollowUpInterceptor 主要做了以下事情:

1、 創(chuàng)建 StreamAllocation 實例進(jìn)行保存,內(nèi)部維護了 RealConnection 類的隊列池裕照,同時保存請求相關(guān)的 Host攒发、Port、DNS 等信息供后續(xù)攔截器使用晋南;
2惠猿、 對請求返回的重定向進(jìn)行重新創(chuàng)建 Request 、StreamAllocation 负间;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末偶妖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子政溃,更是在濱河造成了極大的恐慌趾访,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件董虱,死亡現(xiàn)場離奇詭異扼鞋,居然都是意外死亡,警方通過查閱死者的電腦和手機愤诱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門云头,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淫半,你說我怎么就攤上這事溃槐。” “怎么了科吭?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵昏滴,是天一觀的道長。 經(jīng)常有香客問我对人,道長谣殊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任牺弄,我火速辦了婚禮姻几,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猖闪。我一直安慰自己,他們只是感情好肌厨,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布培慌。 她就那樣靜靜地躺著,像睡著了一般柑爸。 火紅的嫁衣襯著肌膚如雪吵护。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音馅而,去河邊找鬼祥诽。 笑死,一個胖子當(dāng)著我的面吹牛瓮恭,可吹牛的內(nèi)容都是我干的雄坪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼屯蹦,長吁一口氣:“原來是場噩夢啊……” “哼维哈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起登澜,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阔挠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脑蠕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體购撼,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年谴仙,在試婚紗的時候發(fā)現(xiàn)自己被綠了迂求。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡狞甚,死狀恐怖锁摔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哼审,我是刑警寧澤谐腰,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站涩盾,受9級特大地震影響十气,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜春霍,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一砸西、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧址儒,春花似錦芹枷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喧伞,卻和暖如春走芋,著一層夾襖步出監(jiān)牢的瞬間绩郎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工翁逞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肋杖,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓挖函,卻偏偏與公主長得像状植,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子挪圾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

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

  • OkHttp3源碼解析(一)分發(fā)器Dispatcher原理分析OkHttp3源碼解析(二)五大攔截器原理分析 從上...
    程序員三千_閱讀 1,026評論 0 7
  • okhttp[https://github.com/square/okhttp]是Android攻城獅必須掌握的網(wǎng)...
    展翅而飛閱讀 1,542評論 1 6
  • 首先講講okHttp吧浅萧,okhttp算是對于android原生請求的升級,有從TCP連接建立哲思,到ssl建立(就是h...
    帝王鯊kingcp閱讀 2,470評論 0 2
  • 久違的晴天奴潘,家長會兰粉。 家長大會開好到教室時涮帘,離放學(xué)已經(jīng)沒多少時間了系吩。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,524評論 16 22
  • 今天感恩節(jié)哎靠益,感謝一直在我身邊的親朋好友丧肴。感恩相遇!感恩不離不棄胧后。 中午開了第一次的黨會芋浮,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,569評論 0 11