OKHttp源碼解析之網絡請求

OKHttp是square公司的開源項目,當前android開發中最經常使用的輕量級框架。本文中主要是解析OKHttp是如何創建網絡鏈接,即HttpEngine,Connection中的部分代碼。(注:解析的版本是2.5.0版本)java

在開始前咱們先要肯定如下幾個問題,這將助於對源碼的理解(若是已經清楚的大神能夠跳過),問題以下:android

1.http同tcp有什麼關係?git

http是應用層協議,依賴於傳輸層的tcp協議。通俗的講http就是一個tcp鏈接,只不過它是以一種「短鏈接」的形式存在。github

 

2.https 的具體過程是怎麼樣的?web

https是由兩部分組成: http + ssl/tls。 即在http的基礎上一層處理加密信息的模塊,使客戶端與服務端的通信內容進行加密。如下是https身份認證的過程。緩存

 

3.HTTP 1.1,SPDY,HTTP 2.0有什麼區別服務器

HTTP 1.1 是互聯網中主要的協議,但隨着科技的發展,原有的HTTP 1.1已不能知足要求。在2012年Google 推出協議 SPDY,解決 HTTP 1.1 中廣爲人知的性能問題。再到2015年,基於SPDY的HTTP 2.0正式發佈.websocket

相對 HTTP 1.1,HTTP 2.0 主要有如下主要變化:網絡

  1. 二進制分幀:請求和響應等,消息由一個或多個幀組成,並採用二進制格式傳輸數據,而非 HTTP 1.x 的文本格式,二進制協議解析起來更高效
  2. 多路複用:HTTP 1.1 中,若是想併發多個請求,必須使用多個 TCP 連接,但在 HTTP 2.0 中一個tcp鏈接能夠被多個請求複用
  3. 頭部壓縮:將http請求中的header進行壓縮傳輸,可以節省消息頭佔用的網絡的流量

而HTTP 1.1相對SPDY,在總體上與HTTP 2.0上沒有太大區別,但優點更加明顯:併發

  • HTTP/2採用二進制格式傳輸數據,其在協議的解析和優化擴展上帶來更多的優點和可能
  • HTTP/2對消息頭採用HPACK進行壓縮傳輸,可以節省消息頭佔用的網絡的流量
  • Server Push的服務端可以更快地把資源推送給客戶端。

 

4.一次完整的http請求須要通過哪些步驟?

  1. DNS 解析
  2. 與服務端創建tcp鏈接
  3. https會有ssl/tls認證
  4. 發送請求內容
  5. 等待服務器響應
  6. 接收服務器響應內容
  7. 關閉tcp鏈接

 

 

說了那麼多廢話,讓咱們回到源碼解析的正題上,在此先上一發圖

 

該圖是OKHttp如何執行一個網絡請求的代碼調用過程,注意紅色下劃線的部分,它的代碼執行順序是與前面提到網絡請求須要通過的步驟裏面說的是一致的,只要理解這個過程,OKHttp無論內部代碼若是變化,它的調用順序依然是要圍繞這個變化。圖沒看懂不要緊,讓咱們再結合源碼進行分析。

 

首先看一下開始部分

/**
   * Figures out what the response source will be, and opens a socket to that
   * source if necessary. Prepares the request headers and gets ready to start
   * writing the request body if it exists.
   *
   * @throws RequestException if there was a problem with request setup. Unrecoverable.
   * @throws RouteException if the was a problem during connection via a specific route. Sometimes
   *     recoverable. See {@link #recover(RouteException)}.
   * @throws IOException if there was a problem while making a request. Sometimes recoverable. See
   *     {@link #recover(IOException)}.
   *
   */
  public void sendRequest() throws RequestException, RouteException, IOException {
    //...省略部分代碼

    //初始化request
    Request request = networkRequest(userRequest);
InternalCache responseCache = Internal.instance.internalCache(client);
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

    //查詢緩存記錄
    long now = System.currentTimeMillis();
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;

    if (responseCache != null) {
      responseCache.trackResponse(cacheStrategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // 判斷是否有緩存
    if (networkRequest != null) {
      // Open a connection unless we inherited one from a redirect.
      if (connection == null) {
     //此處開始進行幹正事 connect(); } transport
= Internal.instance.newTransport(connection, this); // //因爲看源碼中 callerWritesRequestBody 老是爲false,因此如下if代碼塊不會執行 if (callerWritesRequestBody && permitsRequestBody() && requestBodyOut == null) { long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // Buffer a request body of a known length. transport.writeRequestHeaders(networkRequest); requestBodyOut = new RetryableSink((int) contentLength); } else { // Buffer a request body of an unknown length. Don't write request // headers until the entire body is ready; otherwise we can't set the // Content-Length header correctly. requestBodyOut = new RetryableSink(); } } else { transport.writeRequestHeaders(networkRequest); requestBodyOut = transport.createRequestBody(networkRequest, contentLength); } } } else { //...省略對http緩存的加載 } }

注:Okhttp 2.5的源碼中,建立HttpEngine時老是對 callerWritesRequestBody變量置爲false,而爲true的狀況同false在總體上的區別不大,這裏就不分析另外部分代碼

以上的代碼是HttpEngine的sendRequest方法,主要對request的初始化,同時若是以前有緩存內容,則會優先加載緩存內容。因爲本文的主要內容請求的執行部分,Cache等其它部分就忽略過了,再看看如下connect裏面的代碼

 

 /** Connect to the origin server either directly or via a proxy. */
  private void connect() throws RequestException, RouteException {
    if (connection != null) throw new IllegalStateException();
    
    if (routeSelector == null) {
      //該部分只是建立address對象,並經過routeSelector對象建立RouteSelector對象
      address = createAddress(client, networkRequest);
      try {
        routeSelector = RouteSelector.get(address, networkRequest, client);
      } catch (IOException e) {
        throw new RequestException(e);
      }
    }

    connection = createNextConnection();
    //獲取到了Connection對象,就即將調用Connection.connectAndSetOwner裏創建鏈接
    Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
    route = connection.getRoute();
  }


  private Connection createNextConnection() throws RouteException {
    ConnectionPool pool = client.getConnectionPool();

     
    // Always prefer pooled connections over new connections.
    for (Connection pooled; (pooled = pool.get(address)) != null; ) {
      if (networkRequest.method().equals("GET") || Internal.instance.isReadable(pooled)) {
       //針對address一致,Connection還alive的狀況,則可複用Connection
        return pooled;
      }
      closeQuietly(pooled.getSocket());
    }

    try {
      //RouteSelector.next是負責dns解析,而且選取合適的proxy服務(此proxy服務也多是直接鏈接服務端的配置)
      Route route = routeSelector.next();
      return new Connection(pool, route);
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }    

  private static Address createAddress(OkHttpClient client, Request request) {
    SSLSocketFactory sslSocketFactory = null;
    HostnameVerifier hostnameVerifier = null;
    CertificatePinner certificatePinner = null;
    if (request.isHttps()) {
      sslSocketFactory = client.getSslSocketFactory();
      hostnameVerifier = client.getHostnameVerifier();
      certificatePinner = client.getCertificatePinner();
    }

    return new Address(request.httpUrl().host(), request.httpUrl().port(),
        client.getSocketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner,
        client.getAuthenticator(), client.getProxy(), client.getProtocols(),
        client.getConnectionSpecs(), client.getProxySelector());
  }

          

 該段最主要的代碼是 connect方法中的createNextConnection,前面的routeSelector和address建立只是在這裏爲它作鋪墊。且createNextConnection的調用主要是獲取到Connection對象,該對象能夠理接爲就是用於傳遞請求和接收內容的鏈路。Internal.instance.connectAndSetOwner()的解發實際就是調用Connection.connectAndSetOwner()

  /**
   * Connects this connection if it isn't already. This creates tunnels, shares
   * the connection with the connection pool, and configures timeouts.
   */
  void connectAndSetOwner(OkHttpClient client, Object owner, Request request)
      throws RouteException {
    //標記當前Connection對象是由誰建立
    setOwner(owner);
    
    //如果沒有鏈接,則須要先進行鏈接
    if (!isConnected()) {
      List<ConnectionSpec> connectionSpecs = route.address.getConnectionSpecs();
     //connect 的內容請看下面方法
      connect(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(),
          request, connectionSpecs, client.getRetryOnConnectionFailure());
      if (isFramed()) {
       //若是是SPDY或者HTTP2.0,則分享此鏈接
        client.getConnectionPool().share(this);
      }
      client.routeDatabase().connected(getRoute());
    }

    setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
  }


  
  void connect(int connectTimeout, int readTimeout, int writeTimeout, Request request,
      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
    if (connected) throw new IllegalStateException("already connected");
    
   //創建鏈接須要用的配置整整齊齊的召喚出來而已
    RouteException routeException = null;
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    Proxy proxy = route.getProxy();
    Address address = route.getAddress();

    //如果爲https的狀況則另外須要知足它的配置要求
    if (route.address.getSslSocketFactory() == null
        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
      throw new RouteException(new UnknownServiceException(
          "CLEARTEXT communication not supported: " + connectionSpecs));
    }

    while (!connected) {
      try {
        //前面說過http 本質上就是一個tcp,那麼tcp鏈接確定須要創建一個socket對象,如今終於有socket對象的建立,就說明要開始鏈接的操做不遠了。
        socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.getSocketFactory().createSocket()
            : new Socket(proxy);
        //請看下面方法
        connectSocket(connectTimeout, readTimeout, writeTimeout, request,
            connectionSpecSelector);
        connected = true; // Success!
      } catch (IOException e) {
        //...忽略部分代碼
      }
    }
  }    


/** 
這裏就是真正發起tcp鏈接的地方!!!!!!
Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
      Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException {
    socket.setSoTimeout(readTimeout);
    //這裏就是socket.connect調用的地方!!!若是address沒問題,那麼是可以鏈接上的
    Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout);
     
    if (route.address.getSslSocketFactory() != null) {
     //connectTls是對https作證書認證,這裏不作分析,有興趣的同窗能夠了解SSLSocket的使用
      connectTls(readTimeout, writeTimeout, request, connectionSpecSelector);
    }

    /***到這裏來講明已經和服務器鏈接上了,而這裏會根據http協議的狀況進行又建立不一樣的Connection對象,
前面提到,spdy,http2.0在協議上與http1.1有所不一樣,如頭部壓縮的特性,因此該Connection是對通信協議的封裝**
*/ if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) { socket.setSoTimeout(0); // Framed connection timeouts are set per-stream. framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket) .protocol(protocol).build(); framedConnection.sendConnectionPreface(); } else { httpConnection = new HttpConnection(pool, this, socket); } }

 以上代碼Connection對象裏鏈接所執行的操做,這裏主要強調就是connectSocket()方法中執行的幾步重要的步驟:

Platform.get().connectSocket():本質就是Socket.connect()方法的調用,與服務器創建tcp鏈接,打開外面世界的大門。

connectTls():進行 ssl/tls 證書認證,但有興趣的同窗能夠去了解一下SSLSocket.java這個類

HttpConnection或FramedConnection的建立:該對象內部包含了http通信協議的封裝/解析,幫助後面發/收內容            



上部分都是爲了與服務端創建鏈接而已,以後發送request及接收response的內容就從HttpEngine方法裏的readResponse()開始

/**
   * Flushes the remaining request header and body, parses the HTTP response
   * headers and starts reading the HTTP response body if it exists.
   */
  public void readResponse() throws IOException {
    //...忽略部分代碼

    Response networkResponse;

    if (forWebSocket) {
      //...websocket不在討論範圍內,暫時忽略

    } else if (!callerWritesRequestBody) {
      //此處callerWritesRequestBody總爲false,因此老是會執行到這裏來
      networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);

    } else {
       //...忽略這裏的代碼
    }
    
   //到這裏來講明已經通信完畢了,此處是對reponse作一個漂亮的封裝
    userResponse = networkResponse.newBuilder()
        .request(userRequest)
        .priorResponse(stripBody(priorResponse))
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (hasBody(userResponse)) {
     //對能夠須要且能夠緩存的內容進行緩存
      maybeCache();
     //對通過壓縮的內容進行解壓
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
  }

 readResponse的底下部分是已經獲取到服務端響應內容的時候了,只是對response作點加工 ,主要的讀寫操做是在NetworkInterceptorChain.proceed()中,因此繼續往下看

class NetworkInterceptorChain implements Interceptor.Chain {
    private final int index;
    private final Request request;
    private int calls;

   //...忽略部分代碼   

    @Override public Response proceed(Request request) throws IOException {
      //...忽略部分代碼

      //寫入http的request的頭部
      transport.writeRequestHeaders(request);

      //Update the networkRequest with the possibly updated interceptor request.
      networkRequest = request;
      
      if (permitsRequestBody() && request.body() != null) {
        //針對相似post請求,有body的部分,須要將body也寫入
        Sink requestBodyOut = transport.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      }
      
      //讀取服務響應內容
      Response response = readNetworkResponse();

      int code = response.code();
      if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
        throw new ProtocolException(
            "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
      }

      return response;
    }
  }

  private Response readNetworkResponse() throws IOException {
    //以前的header與body的寫入還並無發送給服務端,當調用finishRequest才正式發送
    transport.finishRequest();

    //等待服務器響應而且解析內容
    Response networkResponse = transport.readResponseHeaders()
        .request(networkRequest)
        .handshake(connection.getHandshake())
        .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
        .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
        .build();

    if (!forWebSocket) {
       //...略過部分代碼
    }

    Internal.instance.setProtocol(connection, networkResponse.protocol());
    return networkResponse;
  }

此部分中 transport 是Transport接口的實現對象,內部是對前面提到的HttpConnection和FrameConnection的封裝調用。而在HttpConnection或FrameConnection中的讀寫實現,除了協議的封裝部分,剩下就是對數據流的讀寫操做(代碼提到的Source或Sink對象就是對Stream的輸入輸出流封裝,見okio)。讀/寫操做自己只要理解了協議內容自己就不是什麼難點,至於我才疏學淺就不深刻到協議自己去了。

到了這裏,整個請求的過程已經完整了,剩下的是對socket的鏈接釋放就再也不多說了。總的來講okHttp網絡請求的調用部分代碼不復雜,只要理解http請求所須要的步驟,跟着順序走下來,就能很好的理解。

本人當前也是在學習階段,若是有沒有寫的不對的地方,但願各位大牛能幫忙指出,謝謝

參考資源:

一文讀懂http/2 http://support.upyun.com/hc/kb/article/1048799/

Https的通信原理:https://juejin.im/entry/5a742ff5f265da4e82631c95

HTTP與TCP的區別和聯繫 https://blog.csdn.net/u013485792/article/details/52100533

相關文章
相關標籤/搜索