Okhttp使用指南與源碼分析

Okhttp使用指南與源碼分析

標簽(空格分隔): Android


使用指南篇#

為什么使用okhttp###

Android為我們提供了兩種HTTP交互的方式:HttpURLConnection 和 Apache HTTP Client,雖然兩者都支持HTTPS,流的上傳和下載,配置超時,IPv6和連接池,已足夠滿足我們各種HTTP請求的需求。但更高效的使用HTTP可以讓您的應用運行更快、更節省流量。而OkHttp庫就是為此而生。
OkHttp是一個高效的HTTP庫:

 - 支持 SPDY ,共享同一個Socket來處理同一個服務器的所有請求,socket自動選擇最好路線,并支持自動重連,擁有自動維護的socket連接池,減少握手次數
 
 - 擁有隊列線程池,輕松寫并發
 
 - 如果SPDY不可用,則通過連接池來減少請求延時
 
 - 擁有Interceptors輕松處理請求與響應(比如透明GZIP壓縮,LOGGING),無縫的支持GZIP來減少數據流量
 
 - 基于Headers的緩存策略,緩存響應數據來減少重復的網絡請求
 
-會從很多常用的連接問題中自動恢復。如果您的服務器配置了多個IP地址,當第一個IP連接失敗的時候,OkHttp會自動嘗試下一個IP。OkHttp還處理了代理服務器問題和SSL握手失敗問題。

-使用 OkHttp 無需重寫您程序中的網絡代碼。OkHttp實現了幾乎和java.net.HttpURLConnection一樣的API。如果您用了 Apache HttpClient,則OkHttp也提供了一個對應的okhttp-apache 模塊。

Okio庫###

Okio庫是一個由square公司開發的,它補充了Java.io和java.nio的不足,以便能夠更加方便,快速的訪問、存儲和處理你的數據。而OkHttp的底層也使用該庫作為支持。而在開發中,使用該庫可以大大給你帶來方便。
因為okhttp用到了Okio庫,所以在Gradle的配置也要引入Okio
compile 'com.squareup.okio:okio:1.6.0' //具體版本以最新的為準


用法細節###

同步就用execute,用這個實現異步,要自己寫線程所以還不如用下面的enqueue實現異步
異步就用enqueue,okHttp內部自動實現了線程,自帶工作線程池

取消操作
  網絡操作中,經常會使用到對請求的cancel操作,okhttp的也提供了這方面的接口,當call沒有必要的時候,使用這個api可以節約網絡資源。例如當用戶離開一個應用時。不管同步還是異步的call都可以取消。
  call的cancel操作。使用Call.cancel()可以立即停止掉一個正在執行的call。如果一個線程正在寫請求或者讀響應,將會引發IOException,
  同時可以通過Request.Builder.tag(Objec tag)給請求設置一個標簽,并使用OkHttpClient.cancel(Object tag)來取消所有帶有這個tag的call。但如果該請求已經在做讀寫操作的時候,cancel是無法成功的,會拋出IOException異常。

具體用法請見以下的博客::####

[博客一][1]
[博客二][2]
[也可以看一下鴻洋的封裝自己的Okhttp庫的文章,在前面的部分也提及到一些基礎用法][3]
[稀土掘金的翻譯文章][4]
注意上面的幾篇博客對應的OkHttp版本已經過時了,例如

 private void postRequest() {
        final OkHttpClient client = new OkHttpClient();
       // RequestBody formBody = new FormEncodingBuilder()//在3.0版本FormEncodingBuilder已經被FormBody代替
        RequestBody formBody = new FormBody.Builder()
                .add(Constant.PUSHID,pushID)
                .build();

        final Request request = new Request.Builder()
                .url(Constant.TUTOR_LOOKQUESKTION)
                .post(formBody)
                .build();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Response response = null;
                try {
                    response = client.newCall(request).execute();
                    if (response.isSuccessful()) {
                        String result=response.body().string();
                        //String result2=response.body().string();//每個 body 只能被消費一次,多次消費會拋出異常;例如這里的result2會報錯,因為是第二次獲取body了
                        response.body().close();//body 必須被關閉,否則會發生資源泄漏;
                        Log.i("WY","打印POST響應的數據:" +result );
                        try {
                            getQuestionData(result,1);
                        }
                        catch (Exception e){
                            e.printStackTrace();
                        }
                    } else {
                        throw new IOException("Unexpected code " + response);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

緩存

Okhttp已經內置了緩存,使用DiskLruCache;默認是不使用的,如果想使用緩存我們需要手動設置。要建立緩存,要有以下條件:

  • 可以讀寫的緩存目錄
  • 緩存大小的限制
  • 緩存目錄應該是私有的,不信任的程序不能讀取緩存內容
  • 全局用戶唯一的緩存訪問實例。okhttp框架全局必須只有一個OkHttpClient實例(new OkHttpClient()),并在第一次創建實例的時候,配置好緩存。
OkHttp內部維護著清理線程池,實現對緩存文件的自動清理,而且內部的DiskLrucache結合了LinkedHashMap使用LRU算法,從而實現對緩存文件的有效管理

如果服務器支持緩存,請求返回的Response會帶有這樣的Header:Cache-Control, max-age=xxx,這種情況下我們只需要手動給okhttp設置緩存就可以讓okhttp自動幫你緩存了。這里的max-age的值代表了緩存在你本地存放的時間,可以根據實際需要來設置其大小。
例如可以在reponse的header設置max-age的值,,一般用在服務器不支持緩存,然后需要使用Interceptor來重寫Respose的頭部信息 ,見下文

 Request request = chain.request();
        Response response = chain.proceed(request);
        Response response1 = response.newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                //cache for 30 days
.header("Cache-Control", "max-age=" + 3600 * 24 * 30)
//設置max-age的值       
                .build();

也可以在request的header設置max-age的值,一般用在服務器支持緩存

int maxStale = 60 * 60 * 24 * 28; //4周
Request request = new Request.Builder()
        .header("Cache-Control", "max-stale=" + maxStale)
        .url("http://publicobject.com/helloworld.txt")
        .build();

注:HTTP header中的max-age 和max-stale區別:
max-age 指示客戶機可以接收生存期不大于指定時間(以秒為單位)的響應。
max-stale 指示客戶機可以接收超出超時期間的響應消息。如果指定max-stale消息的值,那么客戶機可以接收超出超時期指定值之內的響應消息。


如果服務器支持緩存:###

開啟緩存,并且設置緩存目錄

int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheDirectory = new File("cache");
//出于安全性的考慮,在Android中我們推薦使用Context.getCacheDir()來作為緩存的存放路徑
if (!cacheDirectory.exists()) {
    cacheDirectory.mkdirs();
}
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient newClient = okHttpClient.newBuilder()
        .Cache(cache)
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .build();

如果服務器不支持緩存:###

如果服務器不支持緩存就可能沒有指定這個頭部,或者指定的值是如no-store等,但是我們還想在本地使用緩存的話要怎么辦呢?這種情況下我們就需要使用Interceptor來重寫Respose的頭部信息,從而讓okhttp支持緩存。
如下所示,我們重寫的Response的Cache-Control字段

public class CacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        Response response1 = response.newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                //cache for 30 days
            .header("Cache-Control", "max-age=" + 3600 * 24 * 30)
                .build();
        return response1;
    }
}

然后將該Intercepter作為一個NetworkInterceptor加入到okhttpClient中:

 OkHttpClient okHttpClient = new OkHttpClient();

OkHttpClient newClient = okHttpClient.newBuilder()
        .addNetworkInterceptor(new CacheInterceptor())
        .cache(cache)
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .build();

這樣我們就可以在服務器不支持緩存的情況下使用緩存了。
[參考文章][5]


  • 強制使用網絡響應
       Request request = new Request.Builder()
       .header("Cache-Control", "no-cache") // 刷新數據
       .url("http://publicobject.com/helloworld.txt")
       .build();
  • 通過服務器驗證緩存數據是否有效
Request request = new Request.Builder()
        .header("Cache-Control", "max-age=0")
        .url("http://publicobject.com/helloworld.txt")
        .build();
  • 強制使用緩存響應
Request request = new Request.Builder()
        .header("Cache-Control", "only-if-cached")
        .url("http://publicobject.com/helloworld.txt")
        .build();
  • 指定緩存數據過時的時間
int maxStale = 60 * 60 * 24 * 28; //4周
Request request = new Request.Builder()
        .header("Cache-Control", "max-stale=" + maxStale)
        .url("http://publicobject.com/helloworld.txt")
        .build();

[參考文章][6]


okhttp框架獲取響應數據有三種方法:

  • 返回網絡上的數據。如果沒有使用網絡,則返回null。

public Response networkResponse();//從網絡返回的response取數據

  • 返回緩存中的數據。如果不使用緩存,則返回null。對應發送的GET請求,緩存響應和網絡響應 有可都非空。
public Response priorResponse()```

networkResponse()與cacheResponse()是互斥的,要是networkResponse()返回的不為空null,那么cacheResponse()就會返回為nul;反之亦然。具體為什么我還沒搞清楚??????

所以在我們用request去請求網絡數據的步驟,可以這樣寫:

if (null != response.cacheResponse()) {
String str = response.cacheResponse().toString();
//這里有一個問題就是為什么要用一個reponse對象去拿緩存呢?不是應該要用request,像volley一樣嗎???????
Log.i("wangshu", "cache---" + str);
} else {
response.body().string();
String str=response.networkResponse().toString();
Log.i("wangshu", "network---" + str);
}


---
下面是強制情況:
但有時候即使在有緩存的情況下我們依然需要去后臺請求最新的資源(比如資源更新了)這個時候可以使用強制走網絡來要求必須請求網絡數據

request = request.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build();

同樣的我們可以使用 FORCE_CACHE 強制只要使用緩存的數據

request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();

注意:
如果開啟了緩存。但是要強制去網絡更新即配置了CacheControl.FORCE_NETWORK的話,程序不會報錯。這時對應的緩存會清掉(注意不是所有的緩存喔!)即cacheResponse()返回為空,等同于不使用緩存;
但如果請求必須從網絡獲取才有數據,卻配置了CacheControl.FORCE_CACHE的話就會返回504錯誤,
綜上所述,可以這樣理解:FORCE_CACHE是第一道門,而FORCE_NETWORK是第二道門。如果使用了第一道門,即配置了CacheControl.FORCE_CACHE,所有的請求都會從cache緩存里面取,這時要請求網絡的話是過不了第一道門的;但是如果使用的是第二道門的話,這時是可以過第一道門即繞過緩存,而且會把對應的緩存清掉,然后從網絡取數據


關于OkHttp的封裝請看鴻洋的博客
[ Android OkHttp完全解析 是時候來了解OkHttp了][7] //這篇文章我還分析完》》》未完待續

---



[參考博客][8]


  ---
  
#源碼分析篇#


##更新補充##
推薦在分析之前先看看這篇文章
一片分析OkHttp不錯的文章:[拆輪子系列:拆 OkHttp][18]
##博客一##
[參考博客][9]
![總體設計圖][10]

上面是OKHttp總體設計圖,主要是通過Diapatcher不斷從RequestQueue中取出請求(Call),根據是否已緩存調用Cache或Network這兩類數據獲取接口之一,從內存緩存或是服務器取得請求的數據。該引擎有同步和異步請求,一個是普通的同步單線程;另一種是使用了隊列進行并發任務的分發(Dispatch)與回調。同步請求通過Call.execute()直接返回當前的Response,而異步請求會把當前的請求Call.enqueue添加(AsyncCall)到請求隊列中,并通過回調(Callback)的方式來獲取最后結果。

注意:雖然在使用的沒有那么明顯,但是okhttp也有一個RequestQueue

![請求流程圖][11]


![類設計圖][12]
  從OkHttpClient類的整體設計來看,它采用```***門面模式***```來。client知曉子模塊的所有配置以及提供需要的參數。client會將所有從客戶端發來的請求委派到相應的子系統去。
  在該系統中,有多個子系統、類或者類的集合。例如上面的cache、連接以及連接池相關類的集合、網絡配置相關類集合等等。每個子系統都可以被客戶端直接調用,或者被門面角色調用。子系統并不知道門面的存在,對于子系統而言,門面僅僅是另外一個客戶端而已。同時,OkHttpClient可以看作是整個框架的上下文。
  通過類圖,其實很明顯反應了該框架的幾大核心子系統;路由、連接協議、攔截器、代理、安全性認證、連接池以及網絡適配。從client大大降低了開發者使用難度。同時非常明了的展示了該框架在所有需要的配置以及獲取結果的方式。

##同步與異步的實現##
在發起請求時,整個框架主要通過Call來封裝每一次的請求。同時Call持有OkHttpClient和一份HttpEngine。而每一次的同步或者異步請求都會有Dispatcher的參與,不同的是:
同步


  Dispatcher會在同步執行任務隊列中記錄當前被執行過得任務Call,同時在當前線程中去執行Call的getResponseWithInterceptorChain()方法,直接獲取當前的返回數據Response;
異步

首先來說一下Dispatcher,Dispatcher內部實現了懶加載無邊界限制的線程池方式,同時該線程池采用了SynchronousQueue這種阻塞隊列。SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此此隊列內部其 實沒有任何一個元素,或者說容量是0,嚴格說并不是一種容器。由于隊列沒有容量,因此不能調用peek操作,因為只有移除元素時才有元素。顯然這是一種快速傳遞元素的方式,也就是說在這種情況下元素總是以最快的方式從插入者(生產者)傳遞給移除者(消費者),這在多任務隊列中是最快處理任務的方式。對于高頻繁請求的場景,無疑是最適合的。

異步執行是通過Call.enqueue(Callback responseCallback)來執行,在Dispatcher中添加一個封裝了Callback的Call的匿名內部類Runnable來執行當前的Call。這里一定要注意的地方這個AsyncCall是Call的匿名內部類。AsyncCall的execute方法仍然會回調到Call的getResponseWithInterceptorChain方法來完成請求,同時將返回數據或者狀態通過Callback來完成。

##攔截器有什么作用##
先來看看Interceptor本身的文檔解釋:觀察,修改以及可能短路的請求輸出和響應請求的回來。通常情況下攔截器用來添加,移除或者轉換請求或者回應的頭部信息。
從這里的執行來看,攔截器主要是針對Request和Response的切面處理。

在這里再多說一下關于Call這個類的作用,在Call中持有一個HttpEngine。每一個不同的Call都有自己獨立的HttpEngine。在HttpEngine中主要是各種鏈路和地址的選擇,還有一個Transport比較重要


##緩存策略##
在OkHttpClient內部暴露了有Cache和InternalCache。而InternalCache不應該手動去創建,所以作為開發使用者來說,一般用法

public final class CacheResponse {
private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
logger.info(String.format("Cache file path %s",cacheDirectory.getAbsoluteFile()));
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);

client = new OkHttpClient();
client.setCache(cache);

}

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

String response1Body = response1.body().string();
System.out.println("Response 1 response:          " + response1);
System.out.println("Response 1 cache response:    " + response1.cacheResponse());
System.out.println("Response 1 network response:  " + response1.networkResponse());

Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

String response2Body = response2.body().string();
System.out.println("Response 2 response:          " + response2);
System.out.println("Response 2 cache response:    " + response2.cacheResponse());
System.out.println("Response 2 network response:  " + response2.networkResponse());

System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));

}

public static void main(String... args) throws Exception {
new CacheResponse(new File("CacheResponse.tmp")).run();
}
}
第一次是來至網絡數據,第二次來至緩存

從Call.getResponse(Request request, boolean forWebSocket)執行Engine.sendRequest()和Engine.readResponse()來詳細說明一下。
具體的sendRequest()和readResponse()請看博客
sendRequest()
此方法是對可能的Response資源進行一個預判,如果需要就會開啟一個socket來獲取資源。如果請求存在那么就會為當前request添加請求頭部并且準備開始寫入request body。
readResponse()
此方法發起刷新請求頭部和請求體,解析HTTP回應頭部,并且如果HTTP回應體存在的話就開始讀取當前回應頭。在這里有發起返回存入緩存系統,也有返回和緩存系統進行一個對比的過程。

##HTTP連接的實現方式(說說連接池)##
外部網絡請求的入口都是通過Transport接口來完成。該類采用了```橋接模式```將HttpEngine和HttpConnection來連接起來。因為HttpEngine只是一個邏輯處理器,同時它也充當了請求配置的提供引擎,而HttpConnection是對底層處理Connection的封裝。
HttpConnection(一個用于發送HTTP/1.1信息的socket連接)這里。主要有如下的生命周期:

1、發送請求頭;
2、打開一個sink(io中有固定長度的或者塊結構chunked方式的)去寫入請求body;
3、寫入并且關閉sink;
4、讀取Response頭部;
5、打開一個source(對應到第2步的sink方式)去讀取Response的body;
6、讀取并關閉source;

![連接執行時序圖][13]

1、連接池是暴露在client下的,它貫穿了Transport、HttpEngine、Connection、HttpConnection和SpdyConnection;在這里目前默認討論HttpConnection;
2、ConnectionPool有兩個構建參數是maxIdleConnections(最大空閑連接數)和keepAliveDurationNs(存活時間),另外連接池默認的線程池采用了Single的模式(源碼解釋是:一個用于清理過期的多個連接的后臺線程,最多一個單線程去運行每一個連接池);
3、發起請求是在Connection.connect()這里,實際執行是在HttpConnection.flush()這里進行一個刷入。這里重點應該關注一下sink和source,他們創建的默認方式都是依托于同一個socket:
this.source = Okio.buffer(Okio.source(socket));
this.sink = Okio.buffer(Okio.sink(socket));
如果再進一步看一下io的源碼就能看到:
Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout);
Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
source負責讀取,sink負責寫入


---

##簡單總結##
1、從整體結構和類內部域中都可以看到OkHttpClient,有點類似與安卓的ApplicationContext。看起來更像一個單例的類,這樣使用好處是統一。但是如果你不是高手,建議別這么用,原因很簡單:邏輯牽連太深,如果出現問題要去追蹤你會有深深地罪惡感的;

##自己理解##

1.首先Okhttp有自己的線程池,所以可以有效管理線程,OkHttpClient自帶并發光環,雖然Volley相比Okhttp是高級的封裝庫,但是Volley沒有線程池,而且工作線程是自己維護的,那么就有可能存在線程由于異常退出之后,沒有下一個工作線程補充的風險(線程池可以彌補這個缺陷),相對底層的Okhttp卻沒有這個風險。

  ---
  
  
##博客二##
[參考博客][14]
###***因為Okhttp不同于Volley這樣的高層庫,它是類似于UrlHttpConnection的底層庫,所以要涉及到一些數據傳輸的知識,類似socket***###

##第一部分:請求的分發和任務隊列##


##主要對象##
Connections: 對JDK中的socket進行了封裝,用來控制socket連接
Streams: 維護HTTP的流,用來對Requset/Response進行IO操作
Calls: HTTP請求任務封裝
StreamAllocation: 用來控制Connections/Streams的資源分配與釋放


##請求的分發##
當我們用OkHttpClient.newCall(request)進行execute/enenqueue時,實際是將請求Call放到了Dispatcher中,okhttp使用Dispatcher進行線程分發,它有兩種方法,一個是普通的同步單線程;另一種是使用了隊列進行并發任務的分發(Dispatch)與回調,我們下面主要分析第二種,也就是隊列這種情況,這也是okhttp能夠競爭過其它庫的核心功能之一


##Dispatcher的結構:##
maxRequests = 64: 最大并發請求數為64
maxRequestsPerHost = 5: 每個主機最大請求數為5
Dispatcher: 分發者,也就是生產者(默認在主線程)
AsyncCall: 隊列中需要處理的Runnable(包裝了異步回調接口)
ExecutorService:消費者池(也就是線程池)
Deque<readyAsyncCalls>:緩存(用數組實現,可自動擴容,無大小限制)
Deque<runningAsyncCalls>:正在運行的任務,僅僅是用來引用正在運行的任務以判斷并發量,注意它并不是消費者緩存
  根據生產者消費者模型的模型理論,當入隊(enqueue)請求時,如果滿足(runningRequests<64 && runningRequestsPerHost<5),那么就直接把AsyncCall直接加到runningCalls的隊列中,并在線程池中執行。如果消費者緩存滿了,就放入readyAsyncCalls進行緩存等待。
  當任務執行完成后,調用finished的promoteCalls()函數,手動移動緩存區(這里是主動清理的,而不會發生死鎖)
  
##OkHttp配置的線程池##
在OkHttp,使用如下構造了單例線程池

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

可以看出,在Okhttp中,構建了一個閥值為[0, Integer.MAX_VALUE]的線程池,它不保留任何最小線程數,隨時創建更多的線程數,當線程空閑時只能活60秒,它使用了一個不存儲元素的阻塞工作隊列,一個叫做"OkHttp Dispatcher"的線程工廠。

這里解釋一下不存儲元素的阻塞工作隊列SynchronousQueue,Dispatcher內部實現了懶加載無邊界限制的線程池方式,同時該線程池采用了SynchronousQueue這種阻塞隊列。SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此此隊列內部其 實沒有任何一個元素,或者說容量是0,嚴格說并不是一種容器。由于隊列沒有容量,因此不能調用peek操作,因為只有移除元素時才有元素。顯然這是一種快速傳遞元素的方式,也就是說在這種情況下元素總是以最快的方式從插入者(生產者)傳遞給移除者(消費者),這在多任務隊列中是最快處理任務的方式。對于高頻繁請求的場景,無疑是最適合的。


也就是說,在實際運行中,當收到10個并發請求時,線程池會創建十個線程,當工作全部完成后,線程池會在60s后相繼關閉所有線程。

---
##自己對于為什么Okhttp的線程管理優越于Volley的一些理解##
首先是Volley中的工作線程是自己維護的,那么就有可能存在線程由于異常退出之后,沒有下一個工作線程補充的風險(線程池可以彌補這個缺陷),那okhtyp是怎樣避免volley的這個缺陷呢?首先是okhttp是有線程池的,其次這個線程池采用了SynchronousQueue這種阻塞隊列。(SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。)所以當一個線程發生異常要退出,但是因為在SynchronousQueue的管理下,所以不會馬上退出,還要等新的線程加入后才會退出,所以避免了線程異常退出后沒有線程補充的問題
然后是線程池會自動關閉空閑的線程,注意這里是關閉而已,不是銷毀,所以線程池還是會有線程的

---

##反向代理模型##
在OkHttp中,使用了與Nginx類似的反向代理與分發技術,這是典型的單生產者多消費者問題。
我們知道在Nginx中,用戶通過HTTP(Socket)訪問前置的服務器,服務器會自動轉發請求給后端,并返回后端數據給用戶。通過將工作分配給多個后臺服務器,可以提高服務的負載均衡能力,實現非阻塞、高并發連接,避免資源全部放到一臺服務器而帶來的負載,速度,在線率等影響。
![此處輸入圖片的描述][15]
而在OkHttp中,非常類似于上述場景,它使用Dispatcher作為任務的轉發器,線程池對應多臺后置服務器,用AsyncCall對應Socket請求,用Deque<readyAsyncCalls>對應Nginx的內部緩存
![此處輸入圖片的描述][16]

可以發現請求是否進入緩存的條件如下:
```(runningRequests<64 && runningRequestsPerHost<5)```
如果滿足條件,那么就直接把AsyncCall直接加到runningCalls的隊列中,并在線程池中執行(線程池會根據當前負載自動創建,銷毀,緩存相應的線程)。反之就放入readyAsyncCalls進行緩存等待。

當任務執行完成后,無論是否有異常,finally代碼段總會被執行,也就是會調用Dispatcher的finished函數,打開源碼,發現它將runningAsyncCalls中對應的Call移除后,接著執行promoteCalls()函數

##第一部分:Summary請求的分發和任務隊列##
通過上述的分析,我們知道了:

OkHttp采用Dispatcher技術,類似于Nginx,與線程池配合實現了高并發,低阻塞的運行
Okhttp采用Deque作為緩存,按照入隊的順序先進先出
OkHttp最出彩的地方就是在try/finally中調用了finished函數,可以主動控制隊列的移動,而不是采用鎖,極大減少了編碼復雜性

---

##第二部分:Socket管理##
###科普###
通常我們進行http連接時,首先進行tcp握手,然后傳輸數據,最后釋放
這種方法的確簡單,但是在復雜的網絡內容中就不夠用了,創建socket需要進行3次握手,而釋放socket需要2次握手(或者是4次)。重復的連接與釋放tcp連接就像每次僅僅擠1mm的牙膏就合上牙膏蓋子接著再打開接著擠一樣。而每次連接大概是TTL一次的時間(也就是ping一次),甚至在TLS環境下消耗的時間就更多了。很明顯,當訪問復雜網絡時,延時(而不是帶寬)將成為非常重要的因素。
當然,上面的問題早已經解決了,在http中有一種叫做keepalive connections的機制,它可以在傳輸數據后仍然保持連接,當客戶端需要再次獲取數據時,直接使用剛剛空閑下來的連接而不需要再次握手
當然keepalive也有缺點,在提高了單個客戶端性能的同時,復用卻阻礙了其他客戶端的鏈路速度

---
在okhttp中,socket連接池對用戶,甚至開發者都是透明的。它自動創建socket連接池,自動進行泄漏連接回收,自動幫你管理線程池,提供了put/get/clear的接口,甚至調用都幫你寫好了。
我們知道在socket連接中,也就是Connection中,本質是封裝好的流操作,除非手動close,基本不會被gc掉,非常容易引發內存泄露。但是Okhttp通過引用計數法,實現將沒有被使用的Socket關閉。java內部有垃圾回收GC,okhttp有socket回收SocketClean;垃圾回收是根據對象的引用樹實現的,而okhttp是根據RealConnection的虛引用StreamAllocation引用計數是否為0實現的。
##總結##
通過上面的分析,我們可以總結,okhttp使用了類似于引用計數法與標記擦除法的混合使用,當連接空閑或者釋放時,StreamAllocation的數量會漸漸變成0,從而被線程池監測到并回收,這樣就可以保持多個健康的keep-alive連接,Okhttp的黑科技就是這樣實現的。

---
##第三部分:HTTP請求序列化/反序列化##
1. 獲得HTTP流(httpStream)
我們已經在上文第二部分的RealConnection通過connectSocket()構造HttpStream對象并建立套接字連接(完成三次握手)
```httpStream = connect();```
在connect()有非常重要的一步,它通過***okio庫***與遠程socket建立了I/O連接,為了更好的理解,我們可以把它看成管道

/source 用于獲取response
source = Okio.buffer(Okio.source(rawSocket));
//sink 用于write buffer 到server
sink = Okio.buffer(Okio.sink(rawSocket));

***Okhttp的I/O使用的是Okio庫,它是java中最好用的I/O API***

2. 拼裝Raw請求與Headers(writeRequestHeaders)
我們通過Request.Builder構建了簡陋的請求后,可能需要進行一些修飾,這時需要使用Interceptors對Request進行進一步的拼裝了。

***攔截器***是okhttp中強大的流程裝置,它可以用來監控log,修改請求,修改結果,甚至是對用戶透明的GZIP壓縮。類似于函數式編程中的flatmap操作。在okhttp中,內部維護了一個Interceptors的List,通過InterceptorChain進行多次攔截修改操作。
![此處輸入圖片的描述][17]

---
##更新補充##
推薦在分析之前先看看這篇文章
一片分析OkHttp不錯的文章:[拆輪子系列:拆 OkHttp][18]


  [1]: http://codecloud.net/android-okhttp-6425.html
  [2]: http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html
  [3]: http://blog.csdn.net/lmj623565791/article/details/47911083
  [4]: http://gold.xitu.io/entry/5728441d128fe1006058b6b9
  [5]: http://mushuichuan.com/2016/03/01/okhttpcache/
  [6]: http://aiwoapp.blog.51cto.com/8677066/1619654
  [7]: http://blog.csdn.net/lmj623565791/article/details/47911083
  [8]: http://blog.csdn.net/chenzujie/article/details/46994073
  [9]: http://frodoking.github.io/2015/03/12/android-okhttp/
  [10]: http://o6uwc0k25.bkt.clouddn.com/%E6%80%BB%E4%BD%93%E8%AE%BE%E8%AE%A1.jpg
  [11]: http://o6uwc0k25.bkt.clouddn.com/%E8%AF%B7%E6%B1%82%E6%B5%81%E7%A8%8B%E5%9B%BE.jpg
  [12]: http://o6uwc0k25.bkt.clouddn.com/%E7%B1%BB%E8%AE%BE%E8%AE%A1%E5%9B%BE.jpg
  [13]: http://o6uwc0k25.bkt.clouddn.com/%E8%BF%9E%E6%8E%A5%E6%89%A7%E8%A1%8C%E6%97%B6%E5%BA%8F%E5%9B%BE.jpg
  [14]: http://www.lxweimin.com/p/aad5aacd79bf
  [15]: http://o6uwc0k25.bkt.clouddn.com/%E5%8D%95%E7%94%9F%E4%BA%A7%E8%80%85%E5%A4%9A%E6%B6%88%E8%B4%B9%E8%80%85.jpg
  [16]: http://o6uwc0k25.bkt.clouddn.com/diapatcher.jpg
  [17]: http://o6uwc0k25.bkt.clouddn.com/%E6%8B%A6%E6%88%AA%E5%99%A8.jpg
  [18]: http://mp.weixin.qq.com/s?__biz=MzA4MjU5NTY0NA==&mid=2653419018&idx=1&sn=932eec802048861e616a10fb8aca083b&scene=1&srcid=0818NBtIt2B6T6VtfUcQOb85#rd
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容

  • 參考資源 官網 國內博客 GitHub官網 鑒于一些關于OKHttp3源碼的解析文檔過于碎片化,本文系統的,由淺入...
    風骨依存閱讀 12,549評論 11 82
  • 1 介紹 在我們所處的互聯網世界中,HTTP協議算得上是使用最廣泛的網絡協議。OKHttp是一款高效的HTTP客戶...
    天才木木閱讀 5,750評論 7 53
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,799評論 18 139
  • 用OkHttp很久了,也看了很多人寫的源碼分析,在這里結合自己的感悟,記錄一下對OkHttp源碼理解的幾點心得。 ...
    藍灰_q閱讀 4,332評論 4 34
  • 看到這篇信的主題,便異常的喜歡。 想起莊周夢蝶的詩句 李商隱《錦瑟》 錦瑟無端五十弦,一弦一柱思華年。 莊生曉夢迷...
    淇淇18閱讀 651評論 0 3