[搞定開源] 第一篇 okhttp 3.10原理

weixin_34234823發表於2018-07-06

okhttp是Android攻城獅必須掌握的網路庫,很多其他開源庫用到也是它,第一篇介紹okhttp原理最合適不過。

review keyword:請求分發、責任鏈

okhttp的使用

作為網路庫,okhttp的基本功能就是發出request請求,得到response響應。OkHttpClient的構造方法,三種形式,使用構造者模式,裡面有幾十個引數,重要引數後面逐漸會講到。

OkHttpClient okHttpClient1 = new OkHttpClient();
OkHttpClient okHttpClient2 = new OkHttpClient.Builder().build();
OkHttpClient okHttpClient3 = okHttpClient2.newBuilder().build();

同步請求和非同步請求的例子:

private static void sync(OkHttpClient client, String url) {
    Request request = new Request.Builder().url(url).build();
    Response response = null;
    try {
        response = client.newCall(request).execute();
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        System.out.print(response.body().string());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private static void async(OkHttpClient client, String url) {
    Request request = new Request.Builder().url(url).build();
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            System.out.println("onFailure");
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            System.out.println("onResponse:" + response.body().string());
        }
    });
}

使用很簡單不多說,速度進入原始碼,先放張okhttp結構圖,涉及到的重要類都在上面了。

3294095-0dbbbb5e10d3ed6f.png

請求封裝Call

請求request需要封裝到Call物件,呼叫的是OkHttpClinet的newCall方法。Call定義為介面,具體實現類區分同步和非同步:

  • 同步請求:RealCall
  • 非同步請求:AsyncCall

非同步請求需要線上程池裡執行,所以AsyncCall繼承了Runnable。Call提供了cancel方法,所以網路請求是可以中止的。

封裝request到Call後,就交由Dispatcher執行請求分發。

請求分發Dispatcher

同步請求直接執行,非同步請求提交到執行緒池裡執行,Dispatcher裡執行緒池的構建引數如下:

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;
}
  • 執行緒數量範圍:0到無窮大;
  • 空閒執行緒的回收時間是60秒;
  • 阻塞佇列是SynchronousQueue,不存放元素;
  • 自定義執行緒工廠:名稱直接硬編碼,非守護。

沒有新鮮的,執行緒池引數應該要爛熟於胸。


Dispatcher裡有三個Deque,存放什麼名稱寫得很清楚。

private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
  • readyAsyncCalls:待執行的非同步請求
  • runningAsyncCalls:正在執行的非同步請求
  • runningSyncCalls:正在執行的同步請求

同步請求呼叫execute方法,非同步請求呼叫enqueue方法,本質就是將call放入對應的佇列。

同步請求

@Override
public Response execute() throws IOException {
    synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
        client.dispatcher().executed(this);
        Response result = getResponseWithInterceptorChain();
        if (result == null) throw new IOException("Canceled");
        return result;
    } catch (IOException e) {
        eventListener.callFailed(this, e);
        throw e;
    } finally {
        client.dispatcher().finished(this);
    }
}

RealCall的execute方法,裡面是一些狀態判斷和監聽,最重要的是呼叫Dispatcher的executed:

synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
}

很簡單地,將Call加入runningSyncCalls。每個OkHttpClient物件只建立一個Dispatcher,所以操作佇列時,需要同步。

具體的執行過程呼叫getResponseWithInterceptorChain,後文很快會說。當Call執行完成得到response時,在finally裡呼叫Dispatcher的finished。

非同步請求

@Override
public void enqueue(Callback responseCallback) {
    synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

非同步請求的enqueue,呼叫Dispatcher同名的enqueue,這時候傳入的Call為AsyncCall。

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

到達Dispatcher進行分發,enqueue也是操作Call入隊。請求的數量有限制(maxRequests=64 && maxRequestsPerHost=5),範圍內加入runningAsyncCalls並提交執行緒池執行;否則加入readyAsyncCalls等待。

@Override
protected void execute() {
    boolean signalledCallback = false;
    try {
        Response response = getResponseWithInterceptorChain();
       //省略callback處理部分
    } catch (IOException e) {
       //
    } finally {
        client.dispatcher().finished(this);
    }
}

具體執行非同步請求在execute方法,callback裡的onResponse和onFailure很簡單,程式碼略掉。execute核心同樣呼叫getResponseWithInterceptorChain得到response,最後也是呼叫Dispatcher的finished。

請求完成後

請求執行完成的善後是Dispatcher.finished,功能是將完成的Call移出佇列。

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
  int runningCallsCount;
  Runnable idleCallback;
  synchronized (this) {
    if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
    if (promoteCalls) promoteCalls();
    runningCallsCount = runningCallsCount();
    idleCallback = this.idleCallback;
  }

  if (runningCallsCount == 0 && idleCallback != null) {
    idleCallback.run();
  }
}

如果是非同步請求,需要增加呼叫promoteCalls,看看readyAsyncCalls裡的Call能不能放入runningAsyncCalls參與執行。這裡用synchronized鎖住Dispatcher,和“生產者-消費者”有點像。(回憶wait/notify的寫法)

看到個擴充套件點,當Dispatcher裡沒有Call可執行時,可以設定一個idleCallback跑一些東西。

攔截器Interceptor

重頭戲是方法getResponseWithInterceptorChain。

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

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
            originalRequest, this, eventListener, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
}

getResponseWithInterceptorChain首先組建攔截器列表,包括okhttp自帶的攔截器還有使用者自定義的攔截器。這是責任鏈設計模式,每個request都需要按序通過攔截器,最終發出到伺服器得到response,再反序依次通過攔截器。

  • RetryAndFollowUpInterceptor
  • BridgeInterceptor
  • CacheInterceptor
  • ConnectInterceptor
  • CallServerInterceptor

okhttp的核心實現就是這幾個攔截器,後面會逐個分析它們的功能。


補充說明自定義攔截器,有兩種選擇,application interceptors和network interceptors,區別在wiki寫得很清楚,圖直接搬過來。

3294095-893ea5bf62d7a825
自定義攔截器

okhttp支援WebSocket,在程式碼裡看到如果是WebSocket,則不支援network interceptors。

WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動傳送資訊給客戶端。


定義完攔截器後,由RealInterceptorChain將攔截器串聯,呼叫proceed方法,傳入originalRequest。

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);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    //...

    return response;
  }

proceed的核心是建立下一個RealInterceptorChain,並傳入index+1,表示獲取下一個攔截器,然後執行當前攔截器的intercept,最後返回response。

不得不說,攔截器的設計非常美,每一層都各司其職,互不相干,但又配合著處理request和response,最終完成http整個流程。

連線和流

RealInterceptorChain的proceed有四個入參:

Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection

RealConnection封裝了Socket,是請求傳送的通道;HttpCodec描述了request和response的輸入輸出,用的是okio;StreamAllocation是管理連線和流的橋樑。為了複用連線,okhttp使用ConnectionPool對連線進行管理。

對上面幾個類的介紹,另外放在okhttp 3.10連線複用原理

RetryAndFollowUpInterceptor

真的開始跟著請求過攔截器了。RetryAndFollowUpInterceptor是請求通過的第一個自帶攔截器,負責處理請求失敗的重試和伺服器的重定向。

intercept的程式碼比較長,我們分開幾部分來看。

StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;

第一步就是建立StreamAllocation。

while(true){
    //1、請求是否取消
    //2、request交給下一個攔截器,得到response
    //3、是否重定向
}

然後進入無限迴圈,邏輯很明確,檢查response是否是重定向,是的話一直迴圈請求新url。當然,重定向次數有限制,最大到MAX_FOLLOW_UPS=20。

mark1

if (canceled) {
  streamAllocation.release();
  throw new IOException("Canceled");
}

首先判斷canceled狀態,哪裡改變狀態的呢?回看RealCall的cancel。

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

只有一句話,執行retryAndFollowUpInterceptor的cancel。

public void cancel() {
  canceled = true;
  StreamAllocation streamAllocation = this.streamAllocation;
  if (streamAllocation != null) streamAllocation.cancel();
}

設定當前canceled=true,停止retryAndFollowUpInterceptor的無限迴圈,同時呼叫StreamAllocation的cancel,裡面繼續呼叫連線和流的cancel,將能停的東西都停了。

mark2

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();
  }
}

這部分將請求傳遞到下一個攔截器,並捕獲處理各種網路請求異常。失敗的原因很多,裡面的異常處理呼叫recover方法判斷是否能夠重試。recover裡檢查配置、協議、exception型別。只要能重試,會保留連線。

mark3

//...

Request followUp = followUpRequest(response, streamAllocation.route());

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

//...

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;

這部分最重要的是呼叫followUpRequest檢查是否重定向,followUpRequest裡是swtich-case一系列http狀態碼,對我們學習各個狀態碼的處理是不可多得的資料。

最後還需要判斷重定向的目標是否sameConnection,不是的話需要重新建立StreamAllocation。

BridgeInterceptor

使用request時,使用者一般只會傳入method和url。http header才不止這麼少引數,填充預設引數、處理cookie、gzip等是BridgeInterceptor的工作。

CacheInterceptor

okhttp對http的快取策略全部在CacheInterceptor中完成,另見okhttp 3.10快取原理。request到達CacheInterceptor時,如果快取命中,直接返回快取response。當下層response返回到CacheInterceptor時,它可以將結果快取起來。

ConnectInterceptor

@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Request request = realChain.request();
  StreamAllocation streamAllocation = realChain.streamAllocation();

  boolean doExtensiveHealthChecks = !request.method().equals("GET");
  HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
  RealConnection connection = streamAllocation.connection();

  return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

ConnectInterceptor的程式碼很短,核心功能是獲取能用的流和連線,也就是HttpCodec和RealConnection,streamAllocation.newStream對應這兩步:

public HttpCodec newStream(...) {
  //呼叫findHealthyConnection得到RealConnection
  //呼叫RealConnection.newCodec得到HttpCodec
}

private RealConnection findHealthyConnection(...) throws IOException {
  while (true) {
     //呼叫findConnection獲取RealConnection
  }
}

findHealthyConnection,顧名思義找到一個能用的連線,裡面是個無限迴圈呼叫findConnection,直到獲得RealConnection。

連線可能是複用舊有的,也可能是新建立的,findConnection方法比較長,分析下來就是對應這兩步:

  • 從ConnectionPool中取出連線複用;
  • 建立新連線,放回ConnectionPool管理。

連線過程是tcp握手、ssl握手等協議過程,不細說。

得到連線後,就可以建立流。在RealCollection中,newCodec在當前連線上建立一個流,http1使用了okio的source和sink讀寫資料。

private BufferedSource source;
private BufferedSink sink;

public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
    StreamAllocation streamAllocation) throws SocketException {
  if (http2Connection != null) {
    return new Http2Codec(client, chain, streamAllocation, http2Connection);
  } else {
    socket.setSoTimeout(chain.readTimeoutMillis());
    source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
    sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
    return new Http1Codec(client, streamAllocation, source, sink);
  }
}

至此,請求傳送需要的連線和流準備完畢。

CallServerInterceptor

最後執行的攔截器CallServerInterceptor,核心是通過okio在socket上寫入request,讀取response。

後記

okhttp是大師的作品,贊。

我和大師的距離就差那麼一點,就像考北大差那麼一點分(100分),哭笑。

相關文章