OkHttp源碼分析(二)OkHttpClient、Request、Call、Dispatcher詳解

文章首發在我的博客 https://www.nullobject.cn,公衆號 NullObject同步更新。
這篇文章主要介紹OkHttpClient、Request、Call、Dispatcher、Response等類

文章基於OkHttp3.14.3版本html

0. 說明

上一篇OkHttp源碼分析(一)請求和響應過程簡單分析中咱們簡單分析了OkHttp從請求到響應的過程,這篇就來深刻學習下其中涉及到的比較關鍵的類:java

  • OkHttpClient: OkHttp客戶端參數配置、Call工廠
  • Request: 請求連接、方法、參數配置
  • Call實現類Realcall: 請求任務
  • Dispatcher: 任務調度器

1. OkHttpClient

OkHttpClient類主要應用了外觀模式建造者模式兩種設計模式來設計,結合外觀模式的思想,將許多對應OkHttp中各個功能模塊的對象包含到類中,併爲這些功能對象的配置提供了共同的對外接口。同時使用建造者模式,提供一個Builder類爲這些衆多的功能模塊提供鏈式的配置方式,使得繁雜的功能模塊配置變得簡潔。首先,建立一個OkHttpClient.builder對象,接着按需設置builder各個參數:git

OkHttpClient.Builder builder = new OkHttpClient.Builder();

1.1 超時、失敗重連

OkHttpClient中超時時間參數有如下幾種:github

  • callTimeout: 設置完整的請求過程超時時間。該參數計算的是整個請求過程的時間:從解析DNS、與Server創建鏈接、發送請求、Server響應處理到讀取請求結果。同時,若是請求中包含重定向和失敗重連,這兩個過程執行時間也包含在callTimeOut計時時間內。取值範圍:0~Integer.MAX_VALUE,其中0是默認值,表示不設置超時時間;
// 設置整個請求過程最大超時時間爲60s
builder.callTimeout(Duratino.ofSeconds(60));
  • connectTimeout: 設置鏈接創建的超時時間。該參數計算的是創建與服務端之間tcp socket鏈接的時間。取值範圍:0~Integer.MAX_VALUE,其中0表示不設置超時時間,默認值爲10s
// 設置鏈接創建超時時間
builder.connectTimeout(Duration.ofSeconds(10));
  • readTimeout: 設置鏈接的IO讀操做超時時間。該參數應用於請求中的TCP socket和各個IO讀操做,包括對Source和Response的讀操做。其中0表示不設置超時時間,默認值爲10s
// 設置讀超時時間
builder.readTimeout(Duration.ofSeconds(10));
  • writeTimeout: 設置鏈接的IO寫操做超時時間。該參數應用於請求中的各個IO寫操做,其中0表示不設置超時時間,默認值爲10s
// 設置寫超時時間
builder.writeTimeout(Duration.ofSeconds(10));
  • retryOnConnectionFailure: 是否容許OkHttp自動執行失敗重連,默認爲true。當設置爲true時,okhttp會在如下幾種可能的請求失敗的狀況下恢復鏈接並從新請求:1.IP地址不可達;2.太久的池化鏈接;3.代理服務器不可達。
builder.retryOnConnectionFailure(true);

1.2 攔截器

OkHttpClient支持添加多個HTTP/HTTPS請求攔截器和WebSocket攔截器:web

  • addInterceptor: 添加自定義的HTTP/HTTPS請求攔截器:
builder.addInterceptor(chain -> chain.proceed(chain.request()));
  • addNetworkInterceptor: 添加自定義的WebSocket請求攔截器,該方法添加的攔截器只在請求爲websocket的狀況下有效:
builder.addNetworkInterceptor(chain -> chain.proceed(chain.request()));

1.3 緩存和Cookie

OkHttp支持自定義緩存的路徑和大小,以及Cookie的緩存處理:設計模式

  • cache: 設置緩存的路徑和緩存空間大小,用於讀取和寫入已緩存的響應信息Response
// 設置緩存文件,用於將HTTP/HTTPS響應緩存到文件系統從而達到重用的目的以節省時間和網絡帶寬
builder.cache(new Cache(new File("cache_path"), 1024 * 1024))
  • cookieJar: 設置Cookie處理器,用於從HTTP響應中接收Cookie,而且能夠將Cookie提供給即將發起的請求。該參數默認值爲CookieJar.NO_COOKIES,即不處理Cookie。
// 將cookie緩存到內存中
builder.cookieJar(new CookieJar() {
    private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
    @Override
    public void saveFromResponse(final HttpUrl url, final List<Cookie> cookies) {
        cookieStore.put(url.host(), cookies);
    }
    @Override
    public List<Cookie> loadForRequest(final HttpUrl url) {
        List<Cookie> cookies = cookieStore.get(url.host());
        return null != cookies ? cookies : new ArrayList<Cookie>();
    }
})

1.4 域名解析、重定向

  • dns: 設置域名解析服務。默認狀況下,OkHttp內部使用系統提供的域名解析服務。也能夠經過該方法設置自定義的域名解析規則,好比屏蔽某些域名請求、或強制解析到固定的IP下。
// 自定義dns解析,屏蔽百度用域名解析並使用系統提供的DNS解析服務解析其餘域名
builder.dns(hostname -> {
    // 屏蔽百度連接
    if (hostname.contains("baidu.com")) {
        List<InetAddress> addresses = new ArrayList<>();
        addresses.add(InetAddress.getByAddress(new byte[]{(byte) 127, (byte) 0, (byte) 0, (byte) 1}));
        return addresses;
    }
    return Dns.SYSTEM.lookup(hostname);
})
  • followRedirects: 設置是否容許請求重定向,默認爲true容許;
  • followSslRedirects: 設置是否容許HTTP與HTTPS請求之間互相重定向,默認爲true容許。區別於HttpURLConnectiond呃這個選項默認是不容許的。
builder.followRedirects(true)
             .followSslRedirects(true);

1.5 ping心跳機制和協議設置

  • pingInterval: 設置ping信號發送時間間隔,該選項通常用於維持Websocket/Http2長鏈接,發送心跳包。默認值爲0表示禁用心跳機制。
builder.pingInterval(Duration.ofSeconds(59));
  • protocols: 設置OkHttpClient使用的協議,默認爲HTTP/2和HTTP/1.1
builder.protocols(Util.immutableList(Protocol.HTTP_2, Protocol.HTTP_1_1));

1.6 配置鏈接池、請求調度器

  • connectionPool: 手動配置鏈接池。當前OkHttp版本的鏈接池默認爲容納最大5個鏈接數,並在鏈接空閒超時5分鐘後將其從池中移除。
  • connectionPool: 自定義鏈接池,當前OkHttp版本默認的鏈接池最大容納5個鏈接數,並在鏈接空閒超過5分鐘後將其回收,從鏈接池中移除。
// 手動配置鏈接池,其中ConnectionPool第一個參數表示池內容納的最大鏈接
builder.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES));
  • dispatcher: 自定義請求任務調度器,多數狀況下使用默認的請求調度器便可。當須要手動配置執行請求任務的線程池時能夠經過此選項設置實現。
builder.dispatcher(new Dispatcher());
//手動配置執行請求任務的線程池
builder.dispatcher(new Dispatcher(Executors.newFixedThreadPool(64)));

1.7 爲請求設置「上帝視角」:事件監聽器

OkHttp中的EventListener,對於每次請求,猶如"上帝視角"般的存在:若是爲OkHttpClient設置了EventListener,則一個請求從發起到結束的全部步驟都會被EventListener「看」到,請求的完整生命週期事件都會經過EventListener對應的接口回調給上層,所以,在開發debug階段,或想要了解一個請求須要經歷哪些流程時,也能夠經過設置EventListener來獲取相應信息。緩存

  • eventListenerFactory: 爲每個Call指定單獨的事件監聽器。
// EventListener.NONE不監放任何事件
builder.eventListenerFactory(call -> EventListener.NONE);
  • eventListener: 爲OkHttpClient設置統一的事件監聽器,該選項對同一個OkHttpClient實例的全部Call都生效;實際上該選項內部也調用了eventListenerFactory方法,爲每個call設置了相同的事件監聽器:
public Builder eventListener(EventListener eventListener) {
  if (eventListener == null) throw new NullPointerException("eventListener == null");
  this.eventListenerFactory = EventListener.factory(eventListener);
  return this;
}

1.8 代理設置

能夠經過如下三個選項設置請求代理:安全

  • proxySelector: 設置代理選擇策略
builder.proxySelector(ProxySelector.getDefault());
  • proxyAuthenticator: 設置代理身份驗證
builder.proxyAuthenticator(Authenticator.NONE);
  • proxy: 設置使用指定代理。該選項優先級高於proxySelector
builder.proxy(Proxy.NO_PROXY);

1.9 socket相關

  • socketFactory: 設置自定義的用於建立socket鏈接的socket工廠對象,OkHttp默認使用無參的SocketFactory.createSocket()重載方法來建立未鏈接狀態的socket。能夠經過該參數設置實現將建立的socket綁定到特定的地址。
builder.socketFactory(SocketFactory.getDefault());

1.10 HTTPS相關

  • certificatePinner: 設置固定證書
// 設置默認固定證書
builder.certificatePinner(CertificatePinner.DEFAULT);
builder.connectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
  • sslSocketFactory: 設置用於建立SSLSocket鏈接的工廠對象和X509信任管理:
builder.sslSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault(), Util.platformTrustManager());

最後,生成OkHttpClient對象:服務器

OkHttpClient client = builder.build();

接下來看看Request類。websocket

2. Request

Request 封裝了請求的內容,包括連接、請求體參數、請求頭等,以及請求tag,比較簡單。Request也是經過Builder構建:

// Step 2. 構建一個Request用於封裝請求地址、請求類型、參數等信息
Request request = new Request.Builder().get()
                                       .url("https://www.baidu.com")
                                       .build();

建立好OkHttpClientRequest以後,就能夠生成請求任務,發起請求了,接下來看Call和其實現類RealCall

3. Call和RealCall

當前版本的OkHttp中,接口Call只有RealCall這一個實現類。Call表示一個已經準備好,能夠執行的請求任務,Call執行時能夠取消,但一個Call只能被執行一次。Call除了封裝分別用於執行同步異步請求的execute()enqueue(callback)兩個接口外,還封裝了其餘幾個請求相關的接口:

  • Request request(); 獲取發起本次請求任務的請求對象Request;
  • void cancel(); 取消正在執行的本次請求,已經結束的請求不能取消;
  • boolean isExecuted(); 判斷請求任務是否已經執行過了,避免同個請求任務調用一次以上;
  • boolean isCanceled(); 判斷請求是否已取消;
  • Timeout timeout(); 獲取請求任務的超時時間對象,該參數對應於OkHttpClient.Builder.callTimeOut方法設置的超時參數;
  • Call clone(); 克隆本次請求爲一個新的請求,若是同一個請求任務須要重複執行,能夠經過該方法克隆出一個新的Call來實現。

接下來看看RealCall類源碼。

咱們在發起Http請求時會經過OkHttpClient和Request對象來建立Call:

// 建立一個新的請求任務Call
Call call = httpClient.newCall(request);

而httpClient.newCall()方法內部經過RealCall的靜態方法newCall建立並返回一個RealCall對象,因此在執行同步/異步請求時實際調用的是RealCall中的實現方法:

// OkHttpClient.newCall 
@Override 
public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}

打開RealCall源碼能夠看到,RealCall內部持有本次請求的Request對象和OkHttpClient對象,同時還發現,RealCall還聲明瞭一個Transmitter類型的對象並隨着RealCall的建立而建立。Transmitter做爲OkHttp的應用層和網絡層的鏈接,負責對外暴露OkHttp中的高級應用程序層原語,包括鏈接、請求、響應和流等。結合RealCall源碼能夠發現,RealCall負責的是請求發起和執行,Transmitter則負責請求任務的狀態、超時時間、生命週期事件的更新以及請求任務背後的鏈接、鏈接池的維護管理等。

/*RealCall.java:經過transmitter控制超時時間的計算、生命週期步驟更新、取消請求等*/
@Override public Response execute() throws IOException {
  // ... 忽略其餘代碼
  transmitter.timeoutEnter();
  transmitter.callStart();
  // ... 忽略其餘代碼
}

@Override public void enqueue(Callback responseCallback) {
 // ... 忽略其餘代碼
 transmitter.callStart();
 // ... 忽略其餘代碼
}

@Override public void cancel() {
  transmitter.cancel();
}

@Override public Timeout timeout() {
  return transmitter.timeout();
}

再看看clone()方法實現:

@SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state.
@Override public RealCall clone() {
  return RealCall.newRealCall(client, originalRequest, forWebSocket);
}

能夠看到,clone內部建立了一個新的Call,至關於調用了(RealCall)client.newCall(request),所以能夠用這個克隆的Call繼續發起請求。

RealCall還封裝了個內部類AsyncCall用於執行異步請求,AsyncCall聲明瞭CallBack變量用於回調通知異步請求結果,以及一個線程安全的AtomicInteger類型變量callsPerHost用於計量同一主機的請求數。經過上一篇的分析知道,AsyncCall是一個Runnable而且最終經過AsyncCall.execute()方法執行網絡請求。那RealCall內部是怎樣執行到這個這個方法的呢?咱們以前是經過快速跳轉實現的方式找到了這個方法的,而AsyncCall封裝的異步請求任務是在RealCall.enqueue執行時被添加到Dispatch中的請求隊列:

// RealCall.enqueue()
@Override public void enqueue(Callback responseCallback) {
 // ... 忽略無關代碼
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

那麼只要搞清楚Dispatcher.enqueue()背後的隊列中的任務什麼時候何地執行的,上面的問題就有答案了。

4. Dispatcher

Dispatcher是OkHttp中的請求任務調度器,內部維護了一個線程池和相關的請求隊列用於實現高併發的異步請求:

public final class Dispatcher {
  // 最大併發請求數,默認爲64個
  private int maxRequests = 64;
  // 相同服務器主機的最大併發請求數,默認爲5個
  private int maxRequestsPerHost = 5;
  // 空閒回調,若是設置,則當該調度器空閒時(正在執行的任務數變爲0)時回調通知idleCallback.run()
  private @Nullable Runnable idleCallback;
  // 執行異步任務的線程池
  private @Nullable ExecutorService executorService;
  // 存放已經準備就緒能夠執行,但還沒有執行的異步請求任務的雙向隊列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  // 存放正在執行的異步請求任務,包括已經取消但未徹底結束的請求的雙向隊列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  // 存放正在執行的同步請求任務,包括已經取消但未徹底結束的請求的雙向隊列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
  //...其餘代碼
}

其中線程池對象經過懶加載方式建立:

public synchronized ExecutorService executorService() {
  if (executorService == null) {
    executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                                             new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
  }
  return executorService;
}

能夠看到,executorService實際建立的是一個無邊界、核心線程數爲0的線程池。其中池內線程空閒等待時長爲60s,超過空閒時間自動結束;工做隊列workQueue爲SynchronousQueue類型的隊列,該隊列是一個同步隊列,保證併發的任務順序執行,且該類型隊列內部不存儲值,只做傳遞,因爲executorService建立的線程數無限制,不會有隊列等待,因此使用SynchronousQueue(參考Executors.newCachedThread()建立緩存線程池);

參考 https://www.jianshu.com/p/074dff0f4ecb

SynchronousQueue每一個插入操做必須等待另外一個線程的移除操做,一樣任何一個移除操做都等待另外一個線程的插入操做。所以隊列內部其實沒有任何一個元素,或者說容量爲0,嚴格說並非一種容器,因爲隊列沒有容量,所以不能調用peek等操做,所以只有移除元素纔有元素,顯然這是一種快速傳遞元素的方式,也就是說在這種狀況下元素老是以最快的方式從插入者(生產者)傳遞給移除者(消費者),這在多任務隊列中最快的處理任務方式。對於高頻請求場景,無疑是最合適的。

在OKHttp中,建立了一個閥值是Integer.MAX_VALUE的線程池,它不保留任何最小線程,隨時建立更多的線程數,並且若是線程空閒後,只能多活60秒。因此也就說若是收到20個併發請求,線程池會建立20個線程,當完成後的60秒後會自動關閉全部20個線程。他這樣設計成不設上限的線程,以保證I/O任務中高阻塞低佔用的過程,不會長時間卡在阻塞上。

接着第3節RealCall分析最後的問題,來看看Dispatcher.enqueue()方法的實現:

// Dispatcher.enqueue()
void enqueue(AsyncCall call) {
  synchronized (this) {
    // 將新的異步請求任務添加到readyAsyncCalls隊列
    readyAsyncCalls.add(call);
        // 修改call,使其共享runningAsyncCalls或readyAsyncCalls中現有的請求相同主機的call.callsPerHost變量
    if (!call.get().forWebSocket) {
      AsyncCall existingCall = findExistingCallWithHost(call.host());
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
    }
  }
  // 執行AsyncCall請求調度策略,發起call請求
  promoteAndExecute();
}

對於新請求入列的異步請求任務call,首先將其添加到readyAsyncCalls隊列,以表示這個call準備就緒,能夠執行請求;接着修改這個call的callsPerHost屬性爲與先前添加的相同主機請求任務的call共享,從而實現對相同主機的請求計數,對相同主機的最大併發請求數進行限制。接着調用promoteAndExecute()方法,將readyAsyncCalls隊列中的任務提高到runningAsyncCalls,並執行請求:

private boolean promoteAndExecute() {
  assert (!Thread.holdsLock(this));
  // 建立可執行任務列表,用於篩選出readyAsyncCalls中可執行的任務
  List<AsyncCall> executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    // 遍歷篩選readyAsyncCalls
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall asyncCall = i.next();
            // 若是runningAsyncCalls中的請求任務數超過最大併發請求數限制maxRequests則任務繼續放在readyAsyncCalls中等待執行
      if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
      // 若是與asyncCall相同主機的請求數超過最大併發同主機請求數則,則不執行該請求
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
            // 從readyAsyncCalls中移除符合條件的請求任務
      i.remove();
      // 將asyncCall關聯的主機請求數增1
      asyncCall.callsPerHost().incrementAndGet();
      // 加入可執行請求任務列表
      executableCalls.add(asyncCall);
      // 加入runningAsyncCalls任務隊列
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }
    // 遍歷執行請求任務
  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    AsyncCall asyncCall = executableCalls.get(i);
    asyncCall.executeOn(executorService());
  }

  return isRunning;
}

能夠看到,Dispatcher請求調度器最終是在promoteAndExecute()方法中實現最大併發請求數量和最大併發同主機請求數量限制的。在方法的最後遍歷執行請求任務,調用了每個AsyncCall的executeOn()方法並將當前Dispatcher的線程池做爲參數傳入,在看看這個AsyncCall.executeOn(executorService)方法的實現:

// RealCall.AsyncCall.executeOn()
void executeOn(ExecutorService executorService) {
  assert (!Thread.holdsLock(client.dispatcher()));
  boolean success = false;
  try {
    // 經過executorService線程池執行請求任務
    executorService.execute(this);
    success = true;
  } catch (RejectedExecutionException e) {
    InterruptedIOException ioException = new InterruptedIOException("executor rejected");
    ioException.initCause(e);
    // 發生異常時關閉鏈接
    transmitter.noMoreExchanges(ioException);
    responseCallback.onFailure(RealCall.this, ioException);
  } finally {
    // 最後若是請求任務執行失敗則結束任務,從runningAsyncCalls中將任務移除
    if (!success) {
      client.dispatcher().finished(this); // This call is no longer running!
    }
  }
}

AsyncCall.executeOn()方法內部調用線程池的execute方法執行本次任務,由此觸發AsyncCall父類的run方法,並執行到AsyncCall的execute()方法,完成了本次請求!所以,AsyncCall.execute()調用過程大體以下:

AsyncCall.execute調用過程

最後,調用Dispatcher.finished(call)方法結束本次請求:

@Override protected void execute() {
  // ... 忽略無關代碼
  try {
    // ... 忽略無關代碼
  } catch (IOException e) {
    // ... 忽略無關代碼
  } finally {
    // 結束本次請求
    client.dispatcher().finished(this);
  }
}
// Dispatcher.finished(call)
void finished(AsyncCall call) {
  // 將call同主機併發請求數減1
  call.callsPerHost().decrementAndGet();
  // 結束本次請求任務,主動將call從runningAsyncCalls隊列移除
  finished(runningAsyncCalls, call);
}

以上從Dispatcher.enqueue()開始到Dispatcher.finished(call)結束就是Dispatcher調度異步請求的過程。Dispatcher對同步請求的調度執行就簡單多了,單線程任務,直接從RealCall.execute跟進分析便可。不論是同步仍是異步請求,最終都是經過RealCall的getResponseWithInterceptorChain()方法完成請求和獲取響應結果的,那getResponseWithInterceptorChain()方法內部是如何經過攔截器鏈完成請求的呢?下一篇就來分析分析OkHttp中的攔截器Interceptor。

5. The End :)

歡迎關注我的公衆號:
NullObject

相關文章
相關標籤/搜索