OkHttp源碼解析——HTTP請求的邏輯流程

1 介紹

在我們所處的互聯網世界中,HTTP協議算得上是使用最廣泛的網絡協議。
OKHttp是一款高效的HTTP客戶端,支持同一地址的鏈接共享同一個socket,通過連接池來減小響應延遲,還有透明的GZIP壓縮,請求緩存等優勢。
如果您的服務器配置了多個IP地址,當第一個IP連接失敗的時候,OkHttp會自動嘗試下一個IP。OkHttp還處理了代理服務器問題和SSL握手失敗問題。

值得一提的是:Android4.4原生的HttpUrlConnection底層已經替換成了okhttp實現了。

public final class URL implements Serializable {
...
    public URLConnection openConnection() throws IOException {
            return this.handler.openConnection(this);
        }
}

這個handler,在源碼中判斷到如果是HTTP協議,就會創建HtppHandler:

public final class HttpHandler extends URLStreamHandler {
    @Override protected URLConnection openConnection(URL url) throws IOException {
        // 調用了OKHttpClient()的方法
        return new OkHttpClient().open(url);
    }
    @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
        if (url == null || proxy == null) {
            throw new IllegalArgumentException("url == null || proxy == null");
        }
        return new OkHttpClient().setProxy(proxy).open(url);
    }
    @Override protected int getDefaultPort() {
        return 80;
    }
}

2 基本使用方式

在OKHttp,每次網絡請求就是一個Request,我們在Request里填寫我們需要的url,header等其他參數,再通過Request構造出Call,Call內部去請求服務器,得到回復,并將結果告訴調用者。同時okhttp提供了同步異步兩種方式進行網絡操作。

2.1 同步

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

直接execute執行得到Response,通過Response可以得到code,message等信息。android本身是不允許在UI線程做網絡請求操作,需要在子線程中執行。

2.2 異步

  Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {

            }

            @Override
            public void onResponse(Response response) throws IOException {
                //NOT UI Thread
                if(response.isSuccessful()){
                    System.out.println(response.code());
                    System.out.println(response.body().string());
                }
            }
        });

在同步的基礎上講execute改成enqueue,并且傳入回調接口,但接口回調回來的代碼是在非UI線程的,因此如果有更新UI的操作必須切到主線程。

3 整體結構

3.1 處理網絡響應的攔截器機制

無論是同步的call.execute()還是異步的call.enqueue(),最后都是殊途同歸地走到call.getResponseWithInterceptorChain(boolean forWebSocket)方法。

private 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 (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));

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

可以發現okhttp在處理網絡響應時采用的是攔截器機制。okhttp用ArrayList對interceptors進行管理,interceptors將依次被調用。

okhttp_interceptors.png

如上圖:

  1. 橙色框內是okhttp自帶的Interceptors的實現類,它們都是在call.getResponseWithInterceptorChain()中被添加入 InterceptorChain中,實際上這幾個Interceptor都是在okhttp3后才被引入,它們非常重要,負責了重連、組裝請求頭部、讀/寫緩存、建立socket連接、向服務器發送請求/接收響應的全部過程

  2. 在okhttp3之前,這些行為都封裝在HttpEngine類中。okhttp3之后,HttpEngine已經被刪去,取而代之的是這5個Interceptor,可以說一次網絡請求中的細節被解耦放在不同的Interceptor中,不同Interceptor只負責自己的那一環節工作(對Request或者Response進行獲取/處理),使得攔截器模式完全貫穿整個網絡請求。

  3. 用戶可以添加自定義的Interceptor,okhttp把攔截器分為應用攔截器和網絡攔截器

    public class OkHttpClient implements Cloneable, Call.Factory {
     final List<Interceptor> interceptors;
     final List<Interceptor> networkInterceptors;
     ......
     }
    
    1. 調用OkHttpClient.Builder的addInterceptor()可以添加應用攔截器,只會被調用一次,可以處理網絡請求回來的最終Response
    2. 調用addNetworkInterceptor()可以添加network攔截器,處理所有的網絡響應(一次請求如果發生了redirect ,那么這個攔截器的邏輯可能會被調用兩次)

Interceptor解析

由上面的分析可以知道,okhttp框架內自帶了5個Interceptor的實現:

  1. RetryAndFollowUpInterceptor,重試那些失敗或者redirect的請求。
  2. BridgeInterceptor,請求之前對響應頭做了一些檢查,并添加一些頭,然后在請求之后對響應做一些處理(gzip解壓or設置cookie)。
  3. CacheInterceptor,根據用戶是否有設置cache,如果有的話,則從用戶的cache中獲取當前請求的緩存。
  4. ConnectInterceptor,復用連接池中的連接,如果沒有就與服務器建立新的socket連接。
  5. CallServerInterceptor,負責發送請求和獲取響應。

下圖是在Interceptor Chain中的數據流:


Interceptor_flow.png

官方文檔關于Interceptor的解釋是:

Observes, modifies, and potentially short-circuits requests going out and the corresponding responses coming back in. Typically interceptors add, remove, or transform headers on the request or response.
通過Interceptors可以 觀察,修改或者攔截請求/響應。一般攔截器添加,刪除或修改 請求/響應的header。

Interceptor是一個接口,里面只有一個方法:

public interface Interceptor {
  Response intercept(Chain chain) throws IOException;
}

實現Interceptor需要注意兩點(包括源碼內置的Interceptor也是嚴格遵循以下兩點):

  1. 通過intercept()方法里的Chain參數可以拿到request,這樣子就可以對request進行統一的修改(例如BridgeInterceptor對所有request的頭部進行了設置),或者根據request去做一些事情。
  2. 在intercept()方法中通過chain.proceed(request)得到Response,從而攔截了網絡響應進行修改,或者根據response去做一些事情。

4 關鍵代碼

以下是HTTP客戶端向服務器發送報文的過程:

  1. 從URL中解析出服務器的IP地址和端口號
  2. 在客戶端和服務器之間建立一條TCP/IP連接
  3. 開始傳輸HTTP報文

HTTP是個應用層協議。HTTP無需操心網絡通信的具體細節;它把聯網的細節都交給了通用、可靠的因特網傳輸協議TCP/IP。TCP/IP隱藏了各種網絡和硬件的特點及弱點,使各種類型的計算機和網絡都能夠進行可靠的通信。
簡單來說,HTTP協議位于TCP的上層。HTTP使用TCP來傳輸其報文數據。

如果你使用okhttp請求一個URL,具體的工作如下:

  1. 框架使用URL和配置好的OkHttpClient創建一個address。此地址指定我們將如何連接到網絡服務器。
  2. 框架通過address從連接池中取回一個連接。
  3. 如果沒有在池中找到連接,ok會選擇一個route嘗試連接。這通常意味著使用一個DNS請求, 以獲取服務器的IP地址。如果需要,ok還會選擇一個TLS版本和代理服務器。
  4. 如果獲取到一個新的route,它會與服務器建立一個直接的socket連接、使用TLS安全通道(基于HTTP代理的HTTPS),或直接TLS連接。它的TLS握手是必要的。
  5. 開始發送HTTP請求并讀取響應。

如果有連接出現問題,OkHttp將選擇另一條route,然后再試一次。這樣的好處是當服務器地址的一個子集不可達時,OkHttp能夠自動恢復。而且當連接池過期或者TLS版本不受支持時,這種方式非常有用。
一旦響應已經被接收到,該連接將被返回到池中,以便它可以在將來的請求中被重用。連接在池中閑置一段時間后,它會被趕出。

下面就說說這五個步驟的關鍵代碼:

4.1 建立連接 —— ConnectInterceptor

上面所述前四個步驟都在ConnectInterceptor中。
HTTP是建立在TCP協議之上,HTTP協議的瓶頸及其優化技巧都是基于TCP協議本身的特性。比如TCP建立連接時也要在第三次握手時才能捎帶 HTTP 請求報文,達到真正的建立連接,但是這些連接無法復用會導致每次請求都經歷三次握手和慢啟動。
正是由于TCP在建立連接的初期有慢啟動(slow start)的特性,所以連接的重用總是比新建連接性能要好。

而okhttp的一大特點就是通過連接池來減小響應延遲。如果連接池中沒有可用的連接,則會與服務器建立連接,并將socket的io封裝到HttpStream(發送請求和接收response)中,這些都在ConnectInterceptor中完成。
具體在StreamAllocation.findConnection()方法中,下面是具體邏輯:

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      ......
      // Attempt to get a connection from the pool.
      RealConnection pooledConnection = 
          Internal.instance.get(connectionPool, address, this);// 1
      ......
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();//2
      ......
    }

    RealConnection newConnection = new RealConnection(selectedRoute);//3
    ......
    synchronized (connectionPool) {//4
      Internal.instance.put(connectionPool, newConnection);
      this.connection = newConnection;
      if (canceled) throw new IOException("Canceled");
    }

    newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
        connectionRetryEnabled);//5

    return newConnection;
  }

下面具體說說每一步做了什么:

  1. 線程池中取得連接RealConnection pooledConnection = pool.get(address, streamAllocation)

    //StreamAllocation.java
      RealConnection get(Address address, StreamAllocation streamAllocation) {3
        for (RealConnection connection : connections) {
          if (connection.allocations.size() < connection.allocationLimit
              && address.equals(connection.route().address)//根據url來命中connection
              && !connection.noNewStreams) {
            streamAllocation.acquire(connection);//將可用的連接放入
            return connection;
          }
        }
        return null;
      }
    
  2. 如果selectedRoute為空,則選擇下一條路由Route selectedRoute = routeSelector.next();

    //RouteSelector.java
    public final class RouteSelector {
        public Route next() throws IOException {  
           // Compute the next route to attempt.  
           if (!hasNextInetSocketAddress()) {  
             if (!hasNextProxy()) {  
               if (!hasNextPostponed()) {  
                 throw new NoSuchElementException();  
               }  
               return nextPostponed();  
             }  
             lastProxy = nextProxy();  
           }  
           lastInetSocketAddress = nextInetSocketAddress();  //
          
           Route route = new Route(address, lastProxy, lastInetSocketAddress);  
           if (routeDatabase.shouldPostpone(route)) {  
             postponedRoutes.add(route);  
             // We will only recurse in order to skip previously failed routes. They will be tried last.  
             return next();  
           }  
          
           return route;  
           }  
    
        private Proxy nextProxy() throws IOException {  
            if (!hasNextProxy()) {  
              throw new SocketException("No route to " + address.url().host()  
                  + "; exhausted proxy configurations: " + proxies);  
            }  
            Proxy result = proxies.get(nextProxyIndex++);  
            resetNextInetSocketAddress(result);  
            return result;  
        }  
    
        private void resetNextInetSocketAddress(Proxy proxy) throws IOException {  
        ......
        List<InetAddress> addresses = address.dns().lookup(socketHost); //調用dns查詢域名對應的ip 
        ...
        }
    }
    

    瀏覽器需要知道目標服務器的 IP地址和端口號 才能建立連接。將域名解析為 IP地址 的這個系統就是 DNS。

    debug_dns.png
  3. 以前面創建的route為參數新建一個RealConnectionRealConnection newConnection = new RealConnection(selectedRoute);

    public RealConnection(Route route) {  
    this.route = route;  
    }  
    
  4. 添加到連接池

public final class ConnectionPool {
      void put(RealConnection connection) {  
      assert (Thread.holdsLock(this));  
      if (!cleanupRunning) {  
        cleanupRunning = true;  
        executor.execute(cleanupRunnable); 
     //這里很重要,把閑置超過keepAliveDurationNs時間的connection從連接池中移除。
    //具體細節看ConnectionPool 的cleanupRunnable里的run()邏輯
      }  
      connections.add(connection);  
      } 
}
  1. 調用RealConnection的connect()方法,實際上是buildConnection()構建連接。
//RealConnection.java
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,  
  ConnectionSpecSelector connectionSpecSelector) throws IOException {  
connectSocket(connectTimeout, readTimeout);  //建立socket連接
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);  
}

調用connectSocket連接socket。
調用establishProtocol根據HTTP協議版本做一些不同的事情:SSL握手等等。

重點來了! connectSocket(connectTimeout, readTimeout); 里的邏輯實際上是:

public final class RealConnection extends FramedConnection.Listener implements Connection {

    public void connectSocket(Socket socket, InetSocketAddress address,  
        int connectTimeout) throws IOException {  
      socket.connect(address, connectTimeout);  //Http是基于TCP的,自然底層也是建立了socket連接
      ...
      source = Okio.buffer(Okio.source(rawSocket));  
      sink = Okio.buffer(Okio.sink(rawSocket));  //用Okio封裝了socket的輸入和輸出流
    }  
  public final class Okio {

      public static Source source(Socket socket) throws IOException {
          if(socket == null) {
              throw new IllegalArgumentException("socket == null");
          } else {
              AsyncTimeout timeout = timeout(socket);
              Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout);
              return timeout.source(source);
          }
      }

      public static Sink sink(Socket socket) throws IOException {
          if(socket == null) {
              throw new IllegalArgumentException("socket == null");
          } else {
              AsyncTimeout timeout = timeout(socket);
              Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
              return timeout.sink(sink);
          }
      }
  }
  1. 構建HttpStream
resultConnection.socket().setSoTimeout(readTimeout);  
     resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);  
     resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);  
     resultStream = new Http1xStream(  
         client, this, resultConnection.source, resultConnection.sink);  

至此,HttpStream就構建好了,通過它可以發送請求和接收response。

4.2 發送request/接收Response —— CallServerInterceptor

CallServerInterceptor的intercept()方法里 負責發送請求和獲取響應,實際上都是由HttpStream類去完成具體的工作。

Http1XStream

一個socket連接用來發送HTTP/1.1消息,這個類嚴格按照以下生命周期:

  1. writeRequestHeaders()發送request header
  2. 打開一個sink來寫request body,然后關閉sink
  3. readResponseHeaders()讀取response頭部
  4. 打開一個source來讀取response body,然后關閉source

4.2.1 writeRequest

HTTP報文是由一行一行的簡單字符串組成的,都是純文本,不是二進制代碼,可以很方便地進行讀寫。

public final class Http1xStream implements HttpStream {
  /** Returns bytes of a request header for sending on an HTTP transport. */
  public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
  }
}


public final class Headers {
  private final String[] namesAndValues;

    /** Returns the field at {@code position}. */
  public String name(int index) {
    return namesAndValues[index * 2];
  }

  /** Returns the value at {@code index}. */
  public String value(int index) {
    return namesAndValues[index * 2 + 1];
  }
}
debug_write_request.png

4.2.2 readResponse

public final class Http1xStream implements HttpStream {

//讀取Response Header
  public Response.Builder readResponse() throws IOException {
  ......
   while (true) {
        StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());//1 從InputStream上讀入一行數據

        Response.Builder responseBuilder = new Response.Builder()
            .protocol(statusLine.protocol)
            .code(statusLine.code)
            .message(statusLine.message)
            .headers(readHeaders());

        if (statusLine.code != HTTP_CONTINUE) {
          state = STATE_OPEN_RESPONSE_BODY;
          return responseBuilder;
        }
      }
    }

//讀取Response Body,獲得
    @Override public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }
}
  1. 解析HTTP報文,得到HTTP協議版本。

    public final class StatusLine {
    
      public static StatusLine parse(String statusLine/*HTTP/1.1 200 OK*/) throws IOException {
        // H T T P / 1 . 1   2 0 0   T e m p o r a r y   R e d i r e c t
        // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
    
        // Parse protocol like "HTTP/1.1" followed by a space.
        int codeStart;
        Protocol protocol;
        if (statusLine.startsWith("HTTP/1.")) {
          .......
    
debug_status_line.png
  1. 讀取ResponseHeader

    /** Reads headers or trailers. */
    public Headers readHeaders() throws IOException {
        Headers.Builder headers = new Headers.Builder();
        // parse the result headers until the first blank line
        for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
          Internal.instance.addLenient(headers, line);
        }
        return headers.build();
    }
    
debug_read_response_header.png
  1. 讀取ResponseBody,讀取InputStream獲得byte數組,至此就完全得到了客戶端請求服務端接口 的響應內容。

    public abstract class ResponseBody implements Closeable {
      public final byte[] bytes() throws IOException {
      ......
        try {
          bytes = source.readByteArray();
        } finally {
          Util.closeQuietly(source);
        }
    ......
        return bytes;
    }
    
      /**
       * Returns the response as a string decoded with the charset of the Content-Type header. If that
       * header is either absent or lacks a charset, this will attempt to decode the response body as
       * UTF-8.
       */
      public final String string() throws IOException {
        return new String(bytes(), charset().name());
      }
    
debug_result.png

5 總結

從上面關于okhttp發送網絡請求及接受網絡響應的過程的分析,可以發現 okhttp并不是Volley和Retrofit這種二次封裝的網絡框架,而是基于最原始的java socket連接自己去實現了HTTP協議,就連Android源碼也將其收錄在內,堪稱網絡編程的典范。結合HTTP協議相關書籍與okhttp的源碼實踐相結合進行學習,相信可以對HTTP協議有具體且深入的掌握。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容