徹底掌握網絡通信(十九)走進OkHttp3的世界(四)攔截器深入分析

徹底掌握網絡通信(一)Http協議基礎知識
徹底掌握網絡通信(二)Apache的HttpClient基礎知識
徹底掌握網絡通信(三)Android源碼中HttpClient的在不同版本的使用
徹底掌握網絡通信(四)Android源碼中HttpClient的發送框架解析
徹底掌握網絡通信(五)DefaultRequestDirector解析
徹底掌握網絡通信(六)HttpRequestRetryHandler解析
徹底掌握網絡通信(七)ConnectionReuseStrategy,ConnectionKeepAliveStrategy解析
徹底掌握網絡通信(八)AsyncHttpClient源碼解讀
徹底掌握網絡通信(九)AsyncHttpClient爲什麼無法用Fiddler來抓包
徹底掌握網絡通信(十)AsyncHttpClient如何發送JSON解析JSON,以及一些其他用法
徹底掌握網絡通信(十一)HttpURLConnection進行網絡請求的知識準備
徹底掌握網絡通信(十二)HttpURLConnection進行網絡請求概覽
徹底掌握網絡通信(十三)HttpURLConnection進行網絡請求深度分析
徹底掌握網絡通信(十四)HttpURLConnection進行網絡請求深度分析二:緩存
徹底掌握網絡通信(十五)HttpURLConnection進行網絡請求深度分析三:發送與接收詳解
徹底掌握網絡通信(十六)走進OkHttp3的世界(一)引言
徹底掌握網絡通信(十七)走進OkHttp3的世界(二)請求/響應流程分析
徹底掌握網絡通信(十八)走進OkHttp3的世界(三)詳解Http請求的連接,發送和響應

  一個Http請求是由攔截器組順序執行,並冒泡返回給上層調用者;在這個過程中,參加的攔截器主要有:

  1. RetryAndFollowUpInterceptor
  2. BridgeInterceptor
  3. CacheInterceptor
  4. ConnectInterceptor
  5. CallServerInterceptor

針對每一個攔截器的大致作用,我們在前面的分析已經有了大致的瞭解;這篇我們主要深入瞭解下各個攔截器以及http背後的知識


RetryAndFollowUpInterceptor 詳解

  1. 主要作用
    這個攔截器主要作用是從一個失敗的連接中恢復並對某些連接進行重定向

  2. 在Http中如何表示一個重定向連接
    通過Location字段表示

  3. 核心代碼分析

 @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = 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.getFirstConnectException();
        }
        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;
      try {
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      closeQuietly(response.body());

      if (++followUpCount > MAX_FOLLOW_UPS) {
        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())) {
        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;
    }
  }

第1行:獲取發送的請求request
第2行:構建RealInterceptorChain,通過該主鏈可以找到下一個攔截器
第7行:傳入request,call等參數構建 StreamAllocation
第11行:初始化重定向次數爲0
第12行:設置一個Response變量priorResponse,因爲某些請求需要重定向,顧我們需要對上一次的響應保存,來構建一個新的response來進行解析
第13行:開啓死循環,對Response進行判斷,如果需要重定向則重新發起連接請求
第14行:以一個請求被cancel之後,會拋出IOException的異常
第19行:構建Response變量response,用於保存當前請求的響應
第22行:讓下一個攔截器開始執行,下一個攔截器返回response返回給RetryAndFollowUpInterceptor,並最終返回給上層調用者
第26行:當一個連接發生異常的時候,我們將嘗試重新連接,recover方法返回false,表示不可恢復該連接,如果返回true,表示可恢復該連接;比如有些致命的異常是不可恢復的,如ProtocolException,SSLHandshakeException等
第39行:當一個請求被正常執行的時候,releaseConnection是爲false的,顧此段代碼不會被執行
第46行:第一次執行的時候,priorResponse爲null,但是如果一個請求爲重定向請求,則會將當前重定向請求的相應保存在priorResponse
第47行:根據上一次重定向返回的響應重新構建response
第54行:構建一個Request變量followUp,該變量表示一個重定向的連接
第56行:根據followUpRequest方法獲得的response來判斷該請求的響應能否創建一個重定向的請求,如果followUpRequest返回null,則表示該請求不需要重定向;舉例:一個響應頭中狀態碼爲308,307,300,303等的時候,如果okhttpclient設置了不允許重定向,則followUpRequest方法返回爲null,如果響應頭中沒有Location字段,則返回爲null … …
   308:狀態碼錶示:這個請求和以後的請求都應該被另一個URI地址重新發送
   307:狀態碼錶示:這也是一個重定向的狀態碼,對於get請求,則繼續發送請求,對於post請求的重定向,則不繼續,需要用戶確認
   303:狀態碼錶示:臨時重定向,發送Post請求,收到303,直接重定向爲get,發送get請求,不需要向用戶確認
   300:狀態碼錶示:被請求的資源有一系列可供選擇的回饋信息,每個都有自己特定的地址和瀏覽器驅動的商議信息。用戶或瀏覽器能夠自行選擇一個首選的地址進行重定向

第58行:當出現異常的時候,關閉socket連接,釋放資源
第62~第66行:一個請求如果最終被執行,則返回當前response給上層
第71~74行:當重定向次數大於20次,則關閉socket,釋放資源並拋出ProtocolException異常
第81~89行:重新創建StreamAllocation,用於重定向再次發起請求.


BridgeInterceptor 詳解

  1. 該類主要作用有兩個
    第一個是對上層的http請求做補全處理,如補充頭信息
    第二個是對返回的響應做處理,然後返回給RetryAndFollowUpInterceptor,接着返回給上層

  2. 核心代碼

  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }

第9行:  添加Content-Type頭信息
第14行: 添加Content-Length頭信息
第15行: 移除Transfer-Encoding頭信息,Transfer-Encoding含義爲分塊編碼,表達方式爲Transfer-Encoding: chunked,允許客戶端請求分成多塊發送給服務器或者服務器響應分成多塊發送給客戶端。分塊傳輸編碼只在HTTP協議1.1版本(HTTP/1.1)中提供。數據分解成一系列數據塊,並以一個或多個塊發送,這樣服務器可以發送數據而不需要預先知道發送內容的總大小。因爲我們提供了Content-Length字段可以準確判斷傳輸大小,顧此處就不需要Transfer-Encoding頭信息了
第35行 : 添加Accept-Encoding頭信息,說明客戶端支持gzip類型編碼
第47行: 通過下一個攔截器的執行,獲得http請求的響應networkResponse
第49行: 通過響應頭信息,將cookie保存至cookieJar;Cookie總是保存在客戶端中,可以理解爲一個鍵值對,客戶端發送cookie給服務器的時候,只是發送對應的名稱和值,如Cookie: name=value; name2=value2;從服務器端發送cookie給客戶端,是對應的Set-Cookie。包括了對應的cookie的名稱,值,以及各個屬性。如Set-Cookie: made_write_conn=1295214458; Path=/; Domain=.169it.com;在receiveHeaders方法中,我們應該注意到,如果

cookieJar == CookieJar.NO_COOKIES

則我們不會將服務端響應中的cookie進行保存;同時在OkHttpClient.java中,cookieJar 默認爲CookieJar.NO_COOKIES,顧默認情況下,客戶端是不會有保存cookie的操作的;我們可以通過如下代碼來設置客戶端的cookie的存儲機制

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cookieJar(new CookieJar() {
                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                      
                    }

                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                                    
                    }
                })
                .build();

ok,我們繼續分析BridgeInterceptor的核心代碼

第55行:如果服務端的響應中,Content-Encoding內容編碼格式如果gzip,則將響應轉爲GzipSource格式;其中GzipSource的包名爲

package okio

這裏我們簡單將GzipSource理解爲將服務端響應解壓到GzipSource當中即可;

Accept-Encoding :描述的是客戶端接收的編碼格式
Content-Encoding:描述的是正文的編碼格式,目的是優化傳輸,例如用 gzip 壓縮文本文件,能大幅減小體積。內容編碼通常是選擇性的,例如 jpg / png 這類文件一般不開啓,因爲圖片格式已經是高度壓縮過的,再壓一遍沒什麼效果不說還浪費 CPU。
流程如下:
1:客戶端發送請求時,通過Accept-Encoding帶上自己支持的內容編碼格式列表;
2:服務端在接收到請求後,從中挑選出一種用來對響應信息進行編碼,並通過Content-Encoding來說3:明服務端選定的編碼信息
4:客戶端在拿到響應正文後,依據Content-Encoding進行解壓。服務端也可以返回未壓縮的正文,但這種情況不允許返回Content-Encoding

第67行:返回response給上層

BridgeInterceptor 中值得學習的地方

  1. 如果服務端響應中Content-Encoding爲gzip,客戶端會使用okio來解壓縮正文

CacheInterceptor 詳解

  1. 緩存當中If-Modified-Since和Last-Modified解釋
       Last-Modified:是服務端返回給客戶單的一個頭字段,用來描述改請求內容的最後修改時間;
       If-Modified-Since:是客戶端發送給服務端的一個頭字段,用來表示從這個時間段開始,被請求內容是否發生變化
    具體流程爲
       1)客戶端第一次請求服務端頁面,服務端響應報文中有Last-Modified表示正文最後修改時間,此處假設爲:2019年3月10日10:05:100
        2)客戶端第二次請求服務端頁面,客戶端帶上請求字段If-Modified-Since,此處假設爲2019年3月10日10:05:100
        3)服務端根據If-Modified-Since字段的值,並對比本身Last-Modified的值,如果大於If-Modified-Since,則說明正文已經被修改,則返回200給客戶端;如果小於If-Modified-Since,則說明正文未被修改,返回304給客戶端;

  2. 如何設置緩存

 OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(new File("填入緩存地址"),1024*1024))
                .build();
  1. 在介紹CacheInterceptor核心代碼之前,我們先看下緩存攔截器涉及到的一個重要類:Cache.java
    如上面的代碼,我們通過.cache的方法,創建了一個Cache實例,看下構造函數
  public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

第六行:在創建Cache實例的同時,我們也創建了一個DiskLruCache的實例,簡單說下DiskLruCache,DiskLruCache常用方法如下

方法 含義
DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 打開一個緩存目錄,如果沒有則首先創建它,directory:指定數據緩存地址; appVersion:APP版本號,當版本號改變時,緩存數據會被清除; maxSize:最大可以緩存的數據量
Editor edit(String key) 通過key可以獲得一個DiskLruCache.Editor,通過Editor可以得到一個輸出流,進而緩存到本地存儲上
Snapshot get(String key) 通過key值來獲得一個Snapshot,如果Snapshot存在,則移動到LRU隊列的頭部來,通過Snapshot可以得到一個輸入流InputStream

Cache.java中有一個重要成員:internalCache,其定義如下

final InternalCache internalCache = new InternalCache() {
    @Override public Response get(Request request) throws IOException {
      return Cache.this.get(request);
    }

    @Override public CacheRequest put(Response response) throws IOException {
      return Cache.this.put(response);
    }

    @Override public void remove(Request request) throws IOException {
      Cache.this.remove(request);
    }

    @Override public void update(Response cached, Response network) {
      Cache.this.update(cached, network);
    }

    @Override public void trackConditionalCacheHit() {
      Cache.this.trackConditionalCacheHit();
    }

    @Override public void trackResponse(CacheStrategy cacheStrategy) {
      Cache.this.trackResponse(cacheStrategy);
    }
  };

可見internalCache不是直接面向開發者的,Cache.java類通過internalCache來操作緩存
  第3行:通過internalCache的get方法,調用Cache的get方法,Cache的get方法實際上是對DiskLruCache相關操作,通過獲取DiskLruCache的快照DiskLruCache.Snapshot來獲得緩存的響應
  第6行:通過internalCache的put方法,調用Cache的put方法,Cache的put方法實際上是對DiskLruCache相關操作,通過獲取DiskLruCache的DiskLruCache.Editor來保存響應到DiskLruCache,在put方法中,我們應該注意一點:對於非GET的請求,Cache.java是不會進行緩存的

  internalCache的其他方法本質上都是調用Cache對應方法,Cache的相關方法通過對DiskLruCache的操作,來完成緩存的增刪改查;

   4. 現在迴歸到CacheInterceptor 核心代碼上
 @Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

第2行:因爲在構造CacheInterceptor的時候是通過RealCall.java中new CacheInterceptor(client.internalCache())方法創建,這裏的client.internalCache()代碼爲

InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}

顧第2行中的cache != null,同時CacheInterceptor.java中InternalCache cache實現者爲Cache.java中的internalCache
  第3行:調用Cache.java中的internalCache中的get方法,調用Cache.java的get方法返回Response cacheCandidate
  第8行:通過Factory方法,構建Factory對象,如果在第3行獲得的響應不爲空,則我們可以獲得緩存請求的過期時間,上次是否被修改,過期時間等信息;最後調用Factory的get方法完成CacheStrategy實例的創建
CacheStrategy有兩個重要的成員

  1. public final @Nullable Request networkRequest; //發送給服務端的call
  2. public final @Nullable Response cacheResponse; //緩存的響應

  第21行~第31行:特殊情況處理,返回一個空的response,那什麼時候networkRequest會爲null同時cacheResponse爲null?
在第8行中,我們通過調用Factory方法,傳入當前發送給網絡的request,然後調用其get方法來構建CacheStrategy,當構建CacheStrategy傳入的第一個參數爲null,則networkRequest爲null;我們可以通過閱讀CacheStrategy源碼229行來進行分析;通過CacheStrategy源碼229行分析之後,當返回一個空的response的時候,會在頭部加上Warning字段

  第34行,當有緩存,但是當前發送網絡的networkRequest爲null,則將當前緩存返回
  第42行,將請求交給下一個攔截器,返回Response networkResponse
  第51行~70行:當緩存不爲空,同時返回的響應狀態碼爲304,表明服務器正文並沒有發生變化,顧我們封裝好緩存返回給上層,並更新本地緩存
  第72行~75行:使用當前網絡響應返回Response response給上層
  第77行~91行:如果OkHttpClient設置了緩存策略,將響應流信息寫入到緩存文件中,即我們之前提到的Cache.java的DiskLruCache中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章