帶你學(xué)開源項目:OkHttp--自己動手實現(xiàn)OkHttp | wingjay

版權(quán)聲明:本文原創(chuàng)發(fā)布于公眾號 wingjay,轉(zhuǎn)載請務(wù)必注明出處! http://www.lxweimin.com/p/27bf1057f5aa

大幅提高自身技術(shù)實力最有效的途徑之一就是學(xué)習(xí)世界級優(yōu)秀開源項目的精髓,而本人的《帶你學(xué)開源項目》系列文章將持續(xù)更新,對當(dāng)前Android開發(fā)界最優(yōu)秀的開源項目進(jìn)行深入分析。

一、開源項目 OkHttp

在Android、Java開發(fā)領(lǐng)域中,相信大家都聽過或者在使用Square家大名鼎鼎的網(wǎng)絡(luò)請求庫: OkHttp https://github.com/square/okhttp ,當(dāng)前多數(shù)著名的開源項目如 FrescoGlidePicassoRetrofit都在使用OkHttp,這足以說明其質(zhì)量,而且該項目仍處在不斷維護(hù)中

二、問題

在分析okhttp源碼之前,我想先提出一個問題,如果我們自己來設(shè)計一個網(wǎng)絡(luò)請求庫,這個庫應(yīng)該長什么樣子?大致是什么結(jié)構(gòu)呢?

下面我和大家一起來構(gòu)建一個網(wǎng)絡(luò)請求庫,并在其中融入okhttp中核心的設(shè)計思想,希望借此讓讀者感受并學(xué)習(xí)到okhttp中的精華之處,而非僅限于了解其實現(xiàn)。

筆者相信,如果你能耐心閱讀完本篇,不僅能對http協(xié)議有進(jìn)一步理解,更能夠?qū)W習(xí)到世界級項目的思維精華,提高自身思維方式。

三、思考

首先,我們假設(shè)要構(gòu)建的的網(wǎng)絡(luò)請求庫叫做WingjayHttpClient,那么,作為一個網(wǎng)絡(luò)請求庫,它最基本功能是什么呢?

在我看來應(yīng)該是:接收用戶的請求 -> 發(fā)出請求 -> 接收響應(yīng)結(jié)果并返回給用戶。

那么從使用者角度而言,需要做的事是:

  1. 創(chuàng)建一個Request:在里面設(shè)置好目標(biāo)URL;請求method如GET/POST等;一些header如Host、User-Agent等;如果你在POST上傳一個表單,那么還需要body。
  2. 將創(chuàng)建好的Request傳遞給WingjayHttpClient
  3. WingjayHttpClient去執(zhí)行Request,并把返回結(jié)果封裝成一個Response給用戶。而一個Response里應(yīng)該包括statusCode如200,一些header如content-type等,可能還有body

到此即為一次完整請求的雛形。那么下面我們來具體實現(xiàn)這三步。

四、雛形實現(xiàn)

下面我們先來實現(xiàn)一個httpClient的雛形,只具備最基本的功能。

1. 創(chuàng)建Request

首先,我們要建立一個Request類,利用Request類用戶可以把自己需要的參數(shù)傳入進(jìn)去,基本形式如下:

class Request {
    String url;
    String method;
    Headers headers;
    Body requestBody;

    public Request(String url, String method, @Nullable Headers headers, @Nullable Body body) {
        this.url = url;
        ...
    }
}

2. 將Request對象傳遞給WingjayHttpClient

我們可以設(shè)計WingjayHttpClient如下:

class WingjayHttpClient {
    public Response sendRequest(Request request) {
        return executeRequest(request);
    }
}

3. 執(zhí)行Request,并把返回結(jié)果封裝成一個Response返回

class WingjayHttpClient {
    ...
    private Response executeRequest(Request request) {
        //使用socket來進(jìn)行訪問
        Socket socket = new Socket(request.getUrl(), 80);
        ResponseData data = socket.connect().getResponseData();
        return new Response(data);
    }
    ...
}

class Response {
    int statusCode;
    Headers headers;
    Body responseBody
    ...
}

五、功能擴展

利用上面的雛形,可以得到其使用方法如下:

Request request = new Request("https://wingjay.com");
WingjayHttpClient client = new WingjayHttpClient();
Response response = client.sendRequest(request);
handle(response);

然而,上面的雛形是遠(yuǎn)遠(yuǎn)不能勝任常規(guī)的應(yīng)用需求的,因此,下面再來對它添加一些常用的功能模塊。

1. 重新把簡陋的user Request組裝成一個規(guī)范的http request

一般的request中,往往用戶只會指定一個URL和method,這個簡單的user request是不足以成為一個http request,我們還需要為它添加一些header,如Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type,如果這個request使用了cookie,那我們還要將cookie添加到這個request中。

我們可以擴展上面的sendRequest(request)方法:

[class WingjayHttpClient]

public Response sendRequest(Request userRequest) {
    Request httpRequest = expandHeaders(userRequest);
    return executeRequest(httpRequest);
}

private Request expandHeaders(Request userRequest) {
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }
    
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    ...
}   

2. 支持自動重定向

有時我們請求的URL已經(jīng)被移走了,此時server會返回301狀態(tài)碼和一個重定向的新URL,此時我們要能夠支持自動訪問新URL而不是向用戶報錯。

對于重定向這里有一個測試性URL:http://www.publicobject.com/helloworld.txt ,通過訪問并抓包,可以看到如下信息:

redirect.png

因此,我們在接收到Response后要根據(jù)status_code是否為重定向,如果是,則要從Response Header里解析出新的URL-Location并自動請求新URL。那么,我們可以繼續(xù)改寫sendRequest(request)方法:

[class WingjayHttpClient]

private boolean allowRedirect = true;
// user can set redirect status when building WingjayHttpClient
public void setAllowRedirect(boolean allowRedirect) {
    this.allowRedirect = allowRedirect;
}

public Response sendRequest(Request userRequest) {
        Request httpRequest = expandHeaders(userRequest);
        Response response = executeRequest(httpRequest);
        switch (response.statusCode()) {
            // 300: multi choice; 301: moven permanently; 
            // 302: moved temporarily; 303: see other; 
            // 307: redirect temporarily; 308: redirect permanently
            case 300:
            case 301:
            case 302:
            case 303:
            case 307:
            case 308:
                return handleRedirect(response);
            default:
                return response;
        }
        
}
// the max times of followup request
private static final int MAX_FOLLOW_UPS = 20;
private int followupCount = 0;

private Response handleRedirect(Response response) {
    // Does the WingjayHttpClient allow redirect?
    if (!client.allowRedirect()) {
        return null;
    }

    // Get the redirecting url
    String nextUrl = response.header("Location");

    // Construct a redirecting request
    Request followup = new Request(nextUrl);

    // check the max followupCount
    if (++followupCount > MAX_FOLLOW_UPS) {
        throw new Exception("Too many follow-up requests: " + followUpCount);
    }

    // not reach the max followup times, send followup request then.
    return sendRequest(followup);
}

利用上面的代碼,我們通過獲取原始userRequest的返回結(jié)果,判斷結(jié)果是否為重定向,并做出自動followup處理。

一些常用的狀態(tài)碼
100~199:指示信息,表示請求已接收,繼續(xù)處理
200~299:請求成功,表示請求已被成功接收、理解、接受
300~399:重定向,要完成請求必須進(jìn)行更進(jìn)一步的操作
400~499:客戶端錯誤,請求有語法錯誤或請求無法實現(xiàn)
500~599:服務(wù)器端錯誤,服務(wù)器未能實現(xiàn)合法的請求

3. 支持重試機制

所謂重試,和重定向非常類似,即通過判斷Response狀態(tài),如果連接服務(wù)器失敗等,那么可以嘗試獲取一個新的路徑進(jìn)行重新連接,大致的實現(xiàn)和重定向非常類似,此不贅述。

4. Request & Response 攔截機制

這是非常核心的部分。

通過上面的重新組裝request和重定向機制,我們可以感受的,一個request從user創(chuàng)建出來后,會經(jīng)過層層處理后,才真正發(fā)出去,而一個response,也會經(jīng)過各種處理,最終返回給用戶。

筆者認(rèn)為這和網(wǎng)絡(luò)協(xié)議棧非常相似,用戶在應(yīng)用層發(fā)出簡單的數(shù)據(jù),然后經(jīng)過傳輸層、網(wǎng)絡(luò)層等,層層封裝后真正把請求從物理層發(fā)出去,當(dāng)請求結(jié)果回來后又層層解析,最終把最直接的結(jié)果返回給用戶使用。

最重要的是,每一層都是抽象的,互不相關(guān)的!

因此在我們設(shè)計時,也可以借鑒這個思想,通過設(shè)置攔截器Interceptor,每個攔截器會做兩件事情:

  1. 接收上一層攔截器封裝后的request,然后自身對這個request進(jìn)行處理,例如添加一些header,處理后向下傳遞;
  2. 接收下一層攔截器傳遞回來的response,然后自身對response進(jìn)行處理,例如判斷返回的statusCode,然后進(jìn)一步處理。

那么,我們可以為攔截器定義一個抽象接口,然后去實現(xiàn)具體的攔截器。

interface Interceptor {
    Response intercept(Request request);
}

大家可以看下上面這個攔截器設(shè)計是否有問題?

我們想象這個攔截器能夠接收一個request,進(jìn)行攔截處理,并返回結(jié)果。

但實際上,它無法返回結(jié)果,而且它在處理request后,并不能繼續(xù)向下傳遞,因為它并不知道下一個Interceptor在哪里,也就無法繼續(xù)向下傳遞。

那么,如何解決才能把所有Interceptor串在一起,并能夠依次傳遞下去。

public interface Interceptor {
  Response intercept(Chain chain);

  interface Chain {
    Request request();

    Response proceed(Request request);
  }
}

使用方法如下:假如我們現(xiàn)在有三個Interceptor需要依次攔截:

// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new MyInterceptor1());
interceptors.add(new MyInterceptor2());
interceptors.add(new MyInterceptor3());

Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, 0, originalRequest);
chain.proceed(originalRequest);        

里面的RealInterceptorChain的基本思想是:我們把所有interceptors傳進(jìn)去,然后chain去依次把request傳入到每一個interceptors進(jìn)行攔截即可。

通過下面的示意圖可以明確看出攔截流程:

interceptor_preview

其中,RetryAndFollowupInterceptor是用來做自動重試和自動重定向的攔截器;BridgeInterceptor是用來擴展requestheader的攔截器。這兩個攔截器存在于okhttp里,實際上在okhttp里還有好幾個攔截器,這里暫時不做深入分析。

interceptor
  1. CacheInterceptor
    這是用來攔截請求并提供緩存的,當(dāng)request進(jìn)入這一層,它會自動去檢查緩存,如果有,就直接返回緩存結(jié)果;否則的話才將request繼續(xù)向下傳遞。而且,當(dāng)下層把response返回到這一層,它會根據(jù)需求進(jìn)行緩存處理;

  2. ConnectInterceptor
    這一層是用來與目標(biāo)服務(wù)器建立連接

  3. CallServerInterceptor
    這一層位于最底層,直接向服務(wù)器發(fā)出請求,并接收服務(wù)器返回的response,并向上層層傳遞。

上面幾個都是okhttp自帶的,也就是說需要在WingjayHttpClient自己實現(xiàn)的。除了這幾個功能性的攔截器,我們還要支持用戶自定義攔截器,主要有以下兩種(見圖中非虛線框藍(lán)色字部分):

  1. interceptors
    這里的攔截器是攔截用戶最原始的request。

  2. NetworkInterceptor
    這是最底層的request攔截器。

如何區(qū)分這兩個呢?舉個例子,我創(chuàng)建兩個LoggingInterceptor,分別放在interceptors層和NetworkInterceptor層,然后訪問一個會重定向的URL_1,當(dāng)訪問完URL_1后會再去訪問重定向后的新地址URL_2。對于這個過程,interceptors層的攔截器只會攔截到URL_1的request,而在NetworkInterceptor層的攔截器則會同時攔截到URL_1URL_2兩個request。具體原因可以看上面的圖。

5. 同步、異步 Request池管理機制

這是非常核心的部分。

通過上面的工作,我們修改WingjayHttpClient后得到了下面的樣子:

class WingjayHttpClient {
    public Response sendRequest(Request userRequest) {
        Request httpRequest = expandHeaders(userRequest);
        Response response = executeRequest(httpRequest);
        switch (response.statusCode()) {
            // 300: multi choice; 301: moven permanently; 
            // 302: moved temporarily; 303: see other; 
            // 307: redirect temporarily; 308: redirect permanently
            case 300:
            case 301:
            case 302:
            case 303:
            case 307:
            case 308:
                return handleRedirect(response);
            default:
                return response;
        }
    }

    private Request expandHeaders(Request userRequest) {...}
    private Response executeRequest(Request httpRequest) {...}
    private Response handleRedirect(Response response) {...}
}

也就是說,WingjayHttpClient現(xiàn)在能夠同步地處理單個Request了。

然而,在實際應(yīng)用中,一個WingjayHttpClient可能會被用于同時處理幾十個用戶request,而且這些request里還分成了同步異步兩種不同的請求方式,所以我們顯然不能簡單把一個request直接塞給WingjayHttpClient

我們知道,一個request除了上面定義的http協(xié)議相關(guān)的內(nèi)容,還應(yīng)該要設(shè)置其處理方式同步異步。那這些信息應(yīng)該存在哪里呢?兩種選擇:

  1. 直接放入Request
    從理論上來講是可以的,但是卻違背了初衷。我們最開始是希望用Request來構(gòu)造符合http協(xié)議的一個請求,里面應(yīng)該包含的是請求目標(biāo)網(wǎng)址URL,請求端口,請求方法等等信息,而http協(xié)議是不關(guān)心這個request是同步還是異步之類的信息

  2. 創(chuàng)建一個類,專門來管理Request的狀態(tài)
    這是更為合適的,我們可以更好的拆分職責(zé)。

因此,這里選擇創(chuàng)建兩個類SyncCallAsyncCall,用來區(qū)分同步異步

class SyncCall {
    private Request userRequest;

    public SyncCall(Request userRequest) {
        this.userRequest = userRequest;
    }
}

class AsyncCall {
    private Request userRequest;
    private Callback callback;

    public AsyncCall(Request userRequest, Callback callback) {
        this.userRequest = userRequest;
        this.callback = callback;
    }

    interface Callback {
        void onFailure(Call call, IOException e);
        void onResponse(Call call, Response response) throws IOException;
    }
}

基于上面兩個類,我們的使用場景如下:

WingjayHttpClient client = new WingjayHttpClient();
// Sync
Request syncRequest = new Request("https://wingjay.com");
SyncCall syncCall = new SyncCall(request);
Response response = client.sendSyncCall(syncCall);
handle(response);

// Async
AsyncCall asyncCall = new AsyncCall(request, new CallBack() {
      @Override
      public void onFailure(Call call, IOException e) {}

      @Override
      public void onResponse(Call call, Response response) throws IOException {
        handle(response);
      }
});
client.equeueAsyncCall(asyncCall);

從上面的代碼可以看到,WingjayHttpClient的職責(zé)發(fā)生了變化:以前是response = client.sendRequest(request);,而現(xiàn)在變成了

response = client.sendSyncCall(syncCall);

client.equeueAsyncCall(asyncCall);

那么,我們也需要對WingjayHttpClient進(jìn)行改造,基本思路是在內(nèi)部添加請求池來對所有request進(jìn)行管理。那么這個請求池我們怎么來設(shè)計呢?有兩個方法:

  1. 直接在WingjayHttpClient內(nèi)部創(chuàng)建幾個容器
    同樣,從理論上而言是可行的。當(dāng)用戶把(a)syncCall傳給client后,client自動把call存入對應(yīng)的容器進(jìn)行管理。

  2. 創(chuàng)建一個獨立的類進(jìn)行管理
    顯然這樣可以更好的分配職責(zé)。我們把WingjayHttpClient的職責(zé)定義為,接收一個call,內(nèi)部進(jìn)行處理后返回結(jié)果。這就是WingjayHttpClient的任務(wù),那么具體如何去管理這些request的執(zhí)行順序和生命周期,自然不需要由它來管。

因此,我們創(chuàng)建一個新的類:Dispatcher,這個類的作用是:

  1. 存儲外界不斷傳入的SyncCallAsyncCall,如果用戶想取消則可以遍歷所有的call進(jìn)行cancel操作;
  2. 對于SyncCall,由于它是即時運行的,因此Dispatcher只需要在SyncCall運行前存儲進(jìn)來,在運行結(jié)束后移除即可;
  3. 對于AsyncCallDispatcher首先啟動一個ExecutorService,不斷取出AsyncCall去進(jìn)行執(zhí)行,然后,我們設(shè)置最多執(zhí)行的request數(shù)量為64,如果已經(jīng)有64個request在執(zhí)行中,那么就將這個asyncCall存入等待區(qū)。

根據(jù)設(shè)計可以得到Dispatcher構(gòu)造:

class Dispatcher {
    // sync call
    private final Deque<SyncCall> runningSyncCalls = new ArrayDeque<>();
    // async call
    private int maxRequests = 64;
    private final Deque<AsyncCall> waitingAsyncCalls = new ArrayDeque<>();
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
    private ExecutorService executorService;

    // begin execute Sync call
    public void startSyncCall(SyncCall syncCall) {
        runningSyncCalls.add(syncCall);
    }
    // finish Sync call
    public void finishSyncCall(SyncCall syncCall) {
        runningSyncCalls.remove(syncCall);
    }

    // enqueue a new AsyncCall
    public void enqueue(AsyncCall asyncCall) {
        if (runningAsyncCalls.size() < 64) {
            // run directly
            runningAsyncCalls.add(asyncCall);
            executorService.execute(asyncCall);
        } else {
            readyAsyncCalls.add(asyncCall);
        }
    }
    // finish a AsyncCall
    public void finishAsyncCall(AsyncCall asyncCall) {
        runningAsyncCalls.remove(asyncCall);
    }
}

有了這個Dispatcher,那我們就可以去修改WingjayHttpClient以實現(xiàn)

response = client.sendSyncCall(syncCall);

client.equeueAsyncCall(asyncCall);

這兩個方法了。具體實現(xiàn)如下

[class WingjayHttpClient]

    private Dispatcher dispatcher;

    public Response sendSyncCall(SyncCall syncCall) {
        try {
            // store syncCall into dispatcher;
            dispatcher.startSyncCall(syncCall);
            // execute
            return sendRequest(syncCall.getRequest());
        } finally {
            // remove syncCall from dispatcher
            dispatcher.finishSyncCall(syncCall);
        }
    }

    public void equeueAsyncCall(AsyncCall asyncCall) {
        // store asyncCall into dispatcher;
        dispatcher.enqueue(asyncCall);
        // it will be removed when this asyncCall be executed
    }

基于以上,我們能夠很好的處理同步異步兩種請求,使用場景如下:

WingjayHttpClient client = new WingjayHttpClient();
// Sync
Request syncRequest = new Request("https://wingjay.com");
SyncCall syncCall = new SyncCall(request);
Response response = client.sendSyncCall(syncCall);
handle(response);

// Async
AsyncCall asyncCall = new AsyncCall(request, new CallBack() {
      @Override
      public void onFailure(Call call, IOException e) {}

      @Override
      public void onResponse(Call call, Response response) throws IOException {
        handle(response);
      }
});
client.equeueAsyncCall(asyncCall);

六、總結(jié)

到此,我們基本把okhttp里核心的機制都講解了一遍,相信讀者對于okhttp的整體結(jié)構(gòu)和核心機制都有了較為詳細(xì)的了解。

系列文章
《帶你學(xué)開源項目:RxLifecycle-當(dāng)Activity被destory時自動暫停網(wǎng)絡(luò)請求》
《帶你學(xué)開源項目:Meizhi Android之RxJava & Retrofit最佳實踐》

如果有問題歡迎聯(lián)系我。

謝謝!

wingjay

Android技術(shù)·面試技巧·職業(yè)感悟
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,881評論 18 139
  • 前言 用OkHttp很久了,也看了很多人寫的源碼分析,在這里結(jié)合自己的感悟,記錄一下對OkHttp源碼理解的幾點心...
    Java小鋪閱讀 1,535評論 0 13
  • 用OkHttp很久了,也看了很多人寫的源碼分析,在這里結(jié)合自己的感悟,記錄一下對OkHttp源碼理解的幾點心得。 ...
    藍(lán)灰_q閱讀 4,340評論 4 34
  • 關(guān)于okhttp是一款優(yōu)秀的網(wǎng)絡(luò)請求框架,關(guān)于它的源碼分析文章有很多,這里分享我在學(xué)習(xí)過程中讀到的感覺比較好的文章...
    蕉下孤客閱讀 3,616評論 2 38
  • 每一個人終其一生,都是在找尋另一個自己嗎? 我不曉得。 我只知道有些人看了一眼,便從此留在了心底,總是怕溜...
    雨過天青色的你閱讀 196評論 0 0