從源碼看OkHttp3

主要架構和流程

OKHttpClient、Call

OKHttp3在項目中發起網絡請求的API如下:


okHttpClient.newCall(request).execute();

OKHttpClient類:

  • OKHttpClient 裏面組合了很多的類對象。其實是將OKHttp的很多功能模塊,全部包裝進這個類中,讓這個類單獨提供對外的API,這種設計叫做外觀模式

  • 由於內部功能模塊太多,使用了Builder模式(生成器模式)來構造。

它的方法只有一個:newCall.返回一個Call對象(一個準備好了的可以執行和取消的請求)。

Call接口:


public interface Call {
  
  Request request();
 
  //同步的方法,直接返回Response
  Response execute() throws IOException;
  
  //異步的,傳入回調CallBack即可(接口,提供onFailure和onResponse方法)
  void enqueue(Callback responseCallback);
  
  void cancel();

  boolean isExecuted();

  boolean isCanceled();

  interface Factory {
    Call newCall(Request request);
  }
}

Call接口提供了內部接口Factory(用於將對象的創建延遲到該工廠類的子類中進行,從而實現動態的配置,工廠方法模式)。

實際的源碼中,OKHttpClient實現了Call.Factory接口,返回了一個RealCall對象。


@Override 
public Call newCall(Request request) {
    return new RealCall(this, request);
}

RealCall裏面的兩個關鍵方法是:execute 和 enqueue。分別用於同步和異步得執行網絡請求。後面會詳細介紹。

請求Request、返回數據Response

Request:


    public final class Request {
      //url字符串和端口號信息,默認端口號:http爲80,https爲443.其他自定義信息
      private final HttpUrl url;
      
      //"get","post","head","delete","put"....
      private final String method;
      
      //包含了請求的頭部信息,name和value對。最後的形勢爲:$name1+":"+$value1+"\n"+ $name2+":"+$value2+$name3+":"+$value3...
      private final Headers headers;
      
      //請求的數據內容
      private final RequestBody body;
      
      //請求的附加字段。對資源文件的一種摘要。保存在頭部信息中:ETag: "5694c7ef-24dc"。客戶端可以在二次請求的時候,在requst的頭部添加緩存的tag信息(如If-None-Match:"5694c7ef-24dc"),服務端用改信息來判斷數據是否發生變化。
      private final Object tag;
      
      //各種附值函數和Builder類
      ...
        
     }

其中內部類RequestBody: 請求的數據。抽象類:


    public abstract class RequestBody {
     
      ...
      
      //返回內容類型
      public abstract MediaType contentType();
    
      //返回內容長度
      public long contentLength() throws IOException {
        return -1;
      }
    
      //如何寫入緩衝區。BufferedSink是第三方庫okio對輸入輸出API的一個封裝,不做詳解。
      public abstract void writeTo(BufferedSink sink) throws IOException;

    }

OKHttp3中給出了兩個requestBody的實現FormBody 和 MultipartBody,分別對應了兩種不同的MIME類型:"application/x-www-form-urlencoded"和"multipart/"+xxx.作爲的默認實現。

Response:


    public final class Response implements Closeable {
      //網絡請求的信息
      private final Request request;
      
      //網路協議,OkHttp3支持"http/1.0","http/1.1","h2"和"spdy/3.1"
      private final Protocol protocol;
      
      //返回狀態碼,包括404(Not found),200(OK),504(Gateway timeout)...
      private final int code;
      
      //狀態信息,與狀態碼對應
      private final String message;
      
      //TLS(傳輸層安全協議)的握手信息(包含協議版本,密碼套件(https://en.wikipedia.org/wiki/Cipher_suite),證書列表
      private final Handshake handshake;
      
      //相應的頭信息,格式與請求的頭信息相同。
      private final Headers headers;
      
      //數據內容在ResponseBody中
      private final ResponseBody body;
      
      //網絡返回的原聲數據(如果未使用網絡,則爲null)
      private final Response networkResponse;
      
      //從cache中讀取的網絡原生數據
      private final Response cacheResponse;
      
      //網絡重定向後的,存儲的上一次網絡請求返回的數據。
      private final Response priorResponse;
      
      //發起請求的時間軸
      private final long sentRequestAtMillis;
      
      //收到返回數據時的時間軸
      private final long receivedResponseAtMillis;
    
      //緩存控制指令,由服務端返回數據的中的Header信息指定,或者客戶端發器請求的Header信息指定。key:"Cache-Control"
      //詳見<a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">RFC 2616,14.9</a>
      private volatile CacheControl cacheControl; // Lazily initialized.
        
      //各種附值函數和Builder類型          ...
    }

Note:所有網絡請求的頭部信息的key,不是隨便寫的。都是RFC協議規定的。request的header與response的header的標準都不同。具體的見 List of HTTP header fields。OKHttp的封裝類Request和Response爲了應用程序編程方便,會把一些常用的Header信息專門提取出來,作爲局部變量。比如contentType,contentLength,code,message,cacheControl,tag...它們其實都是以name-value對的形勢,存儲在網絡請求的頭部信息中。

我們使用了retrofit2,它提供了接口converter將自定義的數據對象(各類自定義的request和response)和OKHttp3中網絡請求的數據類型(ReqeustBody和ResponseBody)進行轉換。
而converterFactory是converter的工廠模式,用來構建各種不同類型的converter。

故而可以添加converterFactory由retrofit完成requestBody和responseBody的構造。

這裏對retrofit2不展開討論,後續會出新的文章來詳細討論。僅僅介紹一下converterFacotry,以及它是如何構建OkHttp3中的RequestBody和ResponseBody的。

  • Note: retrofit2中的Response與okhttp3中的response不同,前者是包含了後者。既retrofit2中的response是一層封裝,內部纔是真正的okhttp3種的response。

我們項目中的一個converterFacotry代碼如下:


    public class RsaGsonConverterFactory extends Converter.Factory {
   
    //省略部分代碼
    ...
    
    private final Gson gson;

    private RsaGsonConverterFactory(Gson gson) {
        if (gson == null) throw new NullPointerException("gson == null");
        this.gson = gson;
    } 
    //將返回的response的Type,註釋,和retrofit的傳進來,返回response的轉換器。Gson只需要type就可以將responseBody轉換爲需要的類型。
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new RsaGsonResponseBodyConverter<>(gson, adapter);
    }
    //將request的參數類型,參數註釋,方法註釋和retrofit傳進來,返回request的轉換器。Gson只需要type就可以將request對象轉換爲OKHttp3的reqeustBody類型。
    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new RsaGsonRequestBodyConverter<>(gson, adapter);
    }
    }

該Factory(工廠方法模式,用於動態的創建對象)主要是用來生產response的converter和request的converter。顯然我們使用了Gson作爲數據轉換的橋樑。分別對應如下兩個類:

  • response的converter(之所以命名爲Rsa,是做了一層加解密):

    
      public class RsaGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
          private final Gson gson;
          private final TypeAdapter<T> adapter;
      
          RsaGsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
              this.gson = gson;
              this.adapter = adapter;
          }
      
          @Override public T convert(ResponseBody value) throws IOException {
              JsonReader jsonReader = gson.newJsonReader(value.charStream());
              try {
                  return adapter.read(jsonReader);
              } finally {
                  value.close();
              }
          }
      }
    

直接將value中的值封裝爲JsonReader供Gson的TypeAdapter讀取,獲取轉換後的對象。

  • request的converter:

    
      final class RsaGsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
          private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
          private static final Charset UTF_8 = Charset.forName("UTF-8");
      
          private final Gson gson;
          private final TypeAdapter<T> adapter;
      
          RsaGsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
              this.gson = gson;
              this.adapter = adapter;
          }
      
          @Override public RequestBody convert(T value) throws IOException {
      
              Buffer buffer = new Buffer();
              Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
              JsonWriter jsonWriter = gson.newJsonWriter(writer);
      
              adapter.write(jsonWriter, value);
              jsonWriter.close();
              //如果是RsaReq的子類,則進行一層加密。
              if(value instanceof RsaReq){
                 //加密過程
              }
              //不需要加密,則直接讀取byte值,用來創建requestBody
              else {
                  //這個構造方法是okhttp專門爲okio服務的構造方法。
                  return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
              }
          }
      }   
    

上面的流操作使用的是第三方庫okio。可以看到,retrofitokhttp,okio這三個庫是完全相互兼容並互相提供了專有的API。

請求的分發和線程池技術

OKHttpClient類中有個成員變量dispatcher負責請求的分發。既在真正的請求RealCall的execute方法中,使用dispatcher來執行任務:

  • RealCall的execute方法:

    
      @Override 
      public Response execute() throws IOException {
      synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
      }
      try {
        //使用dispatcher 來分發任務
        client.dispatcher().executed(this);
        Response result = getResponseWithInterceptorChain();
        if (result == null) throw new IOException("Canceled");
        return result;
      } finally {
        client.dispatcher().finished(this);
      }
      }
    
  • RealCall的enqueue方法:

    
      @Override public void enqueue(Callback responseCallback) {
          synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
          }
          //使用dispatcher來將人物加入隊列
          client.dispatcher().enqueue(new AsyncCall(responseCallback));
        }
    

OKHttp3中分發器只有一個類 ——Dispathcer.

 

Dispathcer

(1) 其中包含了線程池executorService:


 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:核心線程數量。保持在線程池中的線程數量(即使已經空閒),爲0代表線程空閒後不會保留,等待一段時間後停止。
  • Integer.MAX_VALUE: 線程池可容納線程數量。
  • 60,TimeUnit.SECONDS: 當線程池中的線程數大於核心線程數時,空閒的線程會等待60s後纔會終止。如果小於,則會立刻停止。
  • new SynchronousQueue<Runnable>():線程的等待隊列。同步隊列,按序排隊,先來先服務。
    Util.threadFactory("OkHttp Dispatcher", false): 線程工廠,直接創建一個名爲 “OkHttp Dispathcer”的非守護線程。

(2) 執行同步的Call:直接加入runningSyncCalls隊列中,實際上並沒有執行該Call,交給外部執行。


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

(3) 將Call加入隊列:如果當前正在執行的call數量大於maxRequests,64,或者該call的Host上的call超過maxRequestsPerHost,5,則加入readyAsyncCalls排隊等待。否則加入runningAsyncCalls,並執行。


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

(4) 從ready到running的輪轉,在每個call 結束的時候調用finished,並:


    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!");
          //每次remove完後,執行promoteCalls來輪轉。
          if (promoteCalls) promoteCalls();
          runningCallsCount = runningCallsCount();
          idleCallback = this.idleCallback;
        }
        //線程池爲空時,執行回調
        if (runningCallsCount == 0 && idleCallback != null) {
          idleCallback.run();
        }
      }

(5) 線程輪轉:遍歷readyAsyncCalls,將其中的calls添加到runningAysncCalls,直到後者滿。


    private void promoteCalls() {
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
          AsyncCall call = i.next();
            
          if (runningCallsForHost(call) < maxRequestsPerHost) {             i.remove();
            runningAsyncCalls.add(call);
            executorService().execute(call);
          }
    
          if (runningAsyncCalls.size() >= maxRequests) return; 
        }
      }  

執行請求

同步的請求RealCall 實現了Call接口:
可以execute,enqueue和cancle。
異步的請求AsyncCall(RealCall的內部類)實現了Runnable接口:
只能run(調用了自定義函數execute).

execute 對比:

  • RealCall:

    
    @Override public Response execute() throws IOException {
      synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
      }
      try {
        //分發。實際上只是假如了隊列,並沒有執行
        client.dispatcher().executed(this);
        //實際上的執行。
        Response result = getResponseWithInterceptorChain();
        //返回結果
        if (result == null) throw new IOException("Canceled");
        return result;
      } finally {
        //執行完畢,finish
        client.dispatcher().finished(this);
      }
    }
    
  • AsyncCall:


    @Override protected void execute() {
          boolean signalledCallback = false;
          try {
            //實際執行。
            Response response = getResponseWithInterceptorChain();
            //執行回調
            if (retryAndFollowUpInterceptor.isCanceled()) {
              signalledCallback = true;
              responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
            } else {
              signalledCallback = true;
              responseCallback.onResponse(RealCall.this, response);
            }
          } catch (IOException e) {
            if (signalledCallback) {
              Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
            } else {
              responseCallback.onFailure(RealCall.this, e);
            }
          } finally {
            //執行完畢,finish
            client.dispatcher().finished(this);
          }
        }

實際上的執行函數都是getResponseWithInterceptorChain():


    private Response getResponseWithInterceptorChain() throws IOException {
        //創建一個攔截器列表
        List<Interceptor> interceptors = new ArrayList<>();
        //優先處理自定義攔截器
        interceptors.addAll(client.interceptors());
        //失敗重連攔截器
        interceptors.add(retryAndFollowUpInterceptor);
        //接口橋接攔截器(同時處理cookie邏輯)
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        //緩存攔截器
        interceptors.add(new CacheInterceptor(client.internalCache()));
        //分配連接攔截器
        interceptors.add(new ConnectInterceptor(client));
        //web的socket連接的網絡配置攔截器
        if (!retryAndFollowUpInterceptor.isForWebSocket()) {
          interceptors.addAll(client.networkInterceptors());
        }
        //最後是連接服務器發起真正的網絡請求的攔截器
        interceptors.add(new CallServerInterceptor(
            retryAndFollowUpInterceptor.isForWebSocket())); 
        Interceptor.Chain chain = new RealInterceptorChain(
            interceptors, null, null, null, 0, originalRequest);
        //流式執行並返回response
        return chain.proceed(originalRequest);
      }

這裏的攔截器的作用:將一個流式工作分解爲可配置的分段流程,既實現了邏輯解耦,又增強了靈活性,使得該流程清晰,可配置。

各個攔截器(Interceptor)

這裏的攔截器有點像安卓裏面的觸控反饋的Interceptor。既一個網絡請求,按一定的順序,經由多個攔截器進行處理,該攔截器可以決定自己處理並且返回我的結果,也可以選擇向下繼續傳遞,讓後面的攔截器處理返回它的結果。這個設計模式叫做責任鏈模式

與Android中的觸控反饋interceptor的設計略有不同的是,後者通過返回true 或者 false 來決定是否已經攔截。而OkHttp這裏的攔截器通過函數調用的方式,講參數傳遞給後面的攔截器的方式進行傳遞。這樣做的好處是攔截器的邏輯比較靈活,可以在後面的攔截器處理完並返回結果後仍然執行自己的邏輯;缺點是邏輯沒有前者清晰。

攔截器接口的源碼:


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

 interface Chain {
   Request request();

   Response proceed(Request request) throws IOException;

   Connection connection();
 }
}

其中的Chain是用來傳遞的鏈。這裏的傳遞邏輯僞代碼如下:
代碼的最外層邏輯


 Request request = new Request(){};
 
 
 Arrlist<Interceptor> incpts = new Arrlist();
 Interceptor icpt0 = new Interceptor(){ XXX };
 Interceptor icpt1 = new Interceptor(){ XXX };
 Interceptor icpt2 = new Interceptor(){ XXX };
 ...
 incpts.add(icpt0);
 incpts.add(icpt1);
 incpts.add(icpt2);
 
 Interceptor.Chain chain  = new MyChain(incpts);
 chain.proceed(request);

封裝的Chain的內部邏輯


 public class MyChain implement Interceptor.Chain{
    Arrlist<Interceptor> incpts;
    int index = 0;
    
    public MyChain(Arrlist<Interceptor> incpts){
        this(incpts, 0);
    }
    
    public MyChain(Arrlist<Interceptor> incpts, int index){
        this.incpts = incpts;
        this.index =index;
    }
    
    public void setInterceptors(Arrlist<Interceptor> incpts ){
        this.incpts = incpts;
    }
 
    @override
    Response proceed(Request request) throws IOException{
            Response response = null;
            ...
            //取出第一個interceptor來處理
            Interceptor incpt = incpts.get(index);
            //生成下一個Chain,index標識當前Interceptor的位置。
            Interceptor.Chain nextChain = new MyChain(incpts,index+1);
            response =  incpt.intercept(nextChain);
            ...
            return response;
    }
  } 

各個Interceptor類中的實現:


public class MyInterceptor implement Intercetpor{
    @Override 
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        //前置攔截邏輯
        ...
        Response response = chain.proceed(request);//傳遞Interceptor
        //後置攔截邏輯
        ...
        return response;
    }
}

在這個鏈中,最後的一個Interceptor一般用作生成最後的Response操作,它不會再繼續傳遞給下一個。

  • 失敗重連以及重定向的攔截器:RetryAndFollowUpInterceptor

    失敗重連攔截器核心源碼:
    一個循環來不停的獲取response。每循環一次都會獲取下一個request,如果沒有,則返回response,退出循環。而獲取下一個request的邏輯,是根據上一個response返回的狀態碼,分別作處理。

intercept方法源碼:


    @Override public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
    
        ...
    
        int followUpCount = 0;
        Response priorResponse = null;
        
        //循環入口
        while (true) {
          ...
    
          Response response = null;
          try {
            //一次請求處理,獲得結果
            response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
          } catch (RouteException e) {
            ...
            continue;
          } catch (IOException e) {
            ...
            continue;
          } finally {
            ...
          }
    
          // 如果前一次請求結果不爲空,講它添加到新的請求結果中。通常第一次請求一定是異常請求結果,一定沒有body。
          if (priorResponse != null) {
            response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
                .build();
          }
          //獲取後續的請求,比如驗證,重定向,失敗重連...
          Request followUp = followUpRequest(response);
    
          if (followUp == null) {
            ...
            //如果沒有後續的請求了,直接返回請求結果
            return response;
          }
           ...      
          //
          request = followUp;
          priorResponse = response;
        }
      }

獲取後續的的請求,比如驗證,重定向,失敗重連


private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Connection connection = streamAllocation.connection();
    Route route = connection != null
        ? connection.route()
        : null;
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      case HTTP_PROXY_AUTH:
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);

      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // Redirects don't include a request body.
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            requestBuilder.method(method, null);
          }
          requestBuilder.removeHeader("Transfer-Encoding");
          requestBuilder.removeHeader("Content-Length");
          requestBuilder.removeHeader("Content-Type");
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
    // repeat the request (even non-idempotent ones.)
    if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
      return null;
    }

    return userResponse.request();

  default:
    return null;
}

}

  • 橋接攔截器BridgeInterceptor

橋接攔截器的主要作用是將:

  1. 請求從應用層數據類型類型轉化爲網絡調用層的數據類型。

     

  2. 將網絡層返回的數據類型 轉化爲 應用層數據類型。

    
         1. 保存最新的cookie(默認沒有cookie,需要應用程序自己創建,詳見
                     [Cookie的API]
                     (https://square.github.io/okhttp/3.x/okhttp/okhttp3/CookieJar.html)
                     和
                     [Cookie的持久化]
                     (https://segmentfault.com/a/1190000004345545));
         2. 如果request中使用了"gzip"壓縮,則進行Gzip解壓。解壓完畢後移除Header中的"Content-Encoding""Content-Length"(因爲Header中的長度對應的是壓縮前數據的長度,解壓後長度變了,所以Header中長度信息實效了);
         3. 返回response。
    

補充:Keep-Alive 連接:
HTTP中的keepalive連接在網絡性能優化中,對於延遲降低與速度提升的有非常重要的作用。
通常我們進行http連接時,首先進行tcp握手,然後傳輸數據,最後釋放

 

http連接

這種方法的確簡單,但是在複雜的網絡內容中就不夠用了,創建socket需要進行3次握手,而釋放socket需要2次握手(或者是4次)。重複的連接與釋放tcp連接就像每次僅僅擠1mm的牙膏就合上牙膏蓋子接着再打開接着擠一樣。而每次連接大概是TTL一次的時間(也就是ping一次),在TLS環境下消耗的時間就更多了。很明顯,當訪問複雜網絡時,延時(而不是帶寬)將成爲非常重要的因素。
當然,上面的問題早已經解決了,在http中有一種叫做keepalive connections的機制,它可以在傳輸數據後仍然保持連接,當客戶端需要再次獲取數據時,直接使用剛剛空閒下來的連接而不需要再次握手

 

Keep-Alive連接

在現代瀏覽器中,一般同時開啓6~8個keepalive connections的socket連接,並保持一定的鏈路生命,當不需要時再關閉;而在服務器中,一般是由軟件根據負載情況決定是否主動關閉。

  • 緩存攔截器CacheInterceptor

緩存攔截器的主要作用是將請求 和 返回 關連得保存到緩存中。客戶端與服務端根據一定的機制,在需要的時候使用緩存的數據作爲網絡請求的響應,節省了時間和帶寬。

客戶端與服務端之間的緩存機制:

  • 作用:將HTPTP和HTTPS的網絡返回數據緩存到文件系統中,以便在服務端數據沒發生變化的情況下複用,節省時間和帶寬;
  • 工作原理:客戶端發器網絡請求,如果緩存文件中有一份與該請求匹配(URL相同)的完整的返回數據(比如上一次請求返回的結果),那麼客戶端就會發起一個帶條件(例子,服務端第一次返回數據時,在response的Header中添加上次修改的時間信息:Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT;客戶端再次請求的時候,在request的Header中添加這個時間信息:If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT)的獲取資源的請求。此時服務端根據客戶端請求的條件,來判斷該請求對應的數據是否有更新。如果需要返回的數據沒有變化,那麼服務端直接返回 304 "not modified"。客戶端如果收到響應碼味304 的信息,則直接使用緩存數據。否則,服務端直接返回更新的數據。具體如下圖所示:

     

     

    帶緩存的網絡請求流程

客戶端緩存的實現:

OKHttp3的緩存類爲Cache類,它實際上是一層緩存邏輯的包裝類。內部有個專門負責緩存文件讀寫的類:DiskLruCache。於此同時,OKHttp3還定義了一個緩存接口:InternalCache。這個緩存接口類作爲Cache的成員變量其所有的實現,都是調用了Cahce類的函數實現的。它們間具體的關係如下:

 

緩存的工作機制

InternalCache接口:


    public interface InternalCache {
      Response get(Request request) throws IOException;
    
      CacheRequest put(Response response) throws IOException;
    
      /**
       * 移除request相關的緩存數據
       */
      void remove(Request request) throws IOException;
    
      /**
       * 用網絡返回的數據,更新緩存中數據
       */
      void update(Response cached, Response network);
    
      /** 統計網絡請求的數據 */
      void trackConditionalCacheHit();
    
      /** 統計網絡返回的數據 */
      void trackResponse(CacheStrategy cacheStrategy);
    }

Note:setInternalCache這個方法是不對應用程序開放的,應用程序只能使用cache(Cache cache)這個方法來設置緩存。並且,Cache內部的internalCache是final的,不能被修改。總結:internalCache這個接口雖然是public的,但實際上,應用程序是無法創建它,並附值到OkHttpClient中去的。

那麼問題來了,爲什麼不用Cahce實現InternalCache這個接口,而是以組合的方式,在它的內部實現都調用了Cache的方法呢?
因爲:

  • 在Cache中,InternalCache接口的兩個統計方法:trackConditionalCacheHit和trackResponse (之所以要統計,是爲了查看緩存的效率。比如總的請求次數與緩存的命中次數。)需要用內置鎖進行同步。
  • Cache中,將trackConditionalCacheHit和trackResponse方法 從public變爲爲privite了。不允許子類重寫,也不開放給應用程序。

Cache類:

  • 將網絡請求的文件讀寫操作委託給內部的DiskLruCache類來處理。

  • 負責將url轉換爲對應的key。

  • 負責數據統計:寫成功計數,寫中斷計數,網絡調用計數,請求數量計數,命中數量計數。

  • 負責網絡請求的數據對象Response 與 寫入文件系統中的文本數據 之間的轉換。使用內部類Entry來實現。具體如下:

    
      網絡請求的文件流文本格式如下:
              {
                   http://google.com/foo
                   GET
                   2
                   Accept-Language: fr-CA
                   Accept-Charset: UTF-8
                   HTTP/1.1 200 OK
                   3
                   Content-Type: image/png
                   Content-Length: 100
                   Cache-Control: max-age=600
                   ...
                }     
      內存對象的Entry格式如下.
              private static final class Entry {
                  private final String url;
                  private final Headers varyHeaders;
                  private final String requestMethod;
                  private final Protocol protocol;
                  private final int code;
                  private final String message;
                  ...
              }           
    

通過okio的讀寫API,實現它們之間靈活的切換。

DiskLruCache類:

簡介:一個有限空間的文件緩存。

  • 每個緩存數據都有一個string類型的key和一些固定數量的值。
  • 緩存的數據保存在文件系統的一個目錄下。這個目錄必須是該緩存獨佔的:因爲緩存運行時會刪除和修改該目錄下的文件,因而該緩存目錄不能被其他線程使用。
  • 緩存限制了總的文件大小。如果存儲的大小超過了限制,會以LRU算法來移除一些數據。
  • 可以通過edit,update方法來修改緩存數據。每次調用edit,都必須以commit或者abort結束。commit是原子操作。
  • 客戶端調用get方法來讀取一個緩存文件的快照(存儲了key,快照序列號,數據源和數據長度)。

緩存使用了日誌文件(文件名爲journal)來存儲緩存的數據目錄和操作記錄。一個典型的日誌文件的文本文檔如下:

     //第一行爲緩存的名字
     libcore.io.DiskLruCache                            
     1                                                    //緩存的版本
     100                                                 //應用版本
     2                                                   //值的數量          
     //緩存記錄:操作 key 第一個數據的長度 第二個數據的長度    
     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054    
     DIRTY 335c4c6028171cfddfbaae1a9c313c52
     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
     REMOVE 335c4c6028171cfddfbaae1a9c313c52
     DIRTY 1ab96a171faeeee38496d8b330771a7a
     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
     READ 335c4c6028171cfddfbaae1a9c313c52
     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

操作記錄:狀態+key+額外擦數

  • CLEAN key param0 param1:該key的對應的數據爲最新的有效數據,後續爲額外的參數
  • DIRTY key:該key對應的數據被創建或被修改。
  • REMOVE key:該key對應的數據被刪除。
  • READ key:改key對應的數據被讀取的記錄。——用於LRU算法來統計哪些數據是最新的數據。

一些冗餘的操作記錄,比如DIRTY,REMOVE...比較多的時候(大於2000個,或者超過總數量),會發器線程對該日誌文件進行壓縮(刪除這些冗餘的日誌記錄)。此時,會創建一個journal.tmp文件作爲臨時的文件,供緩存繼續使用。同時還有個journal.bkp文件,用作journal文件的臨時備份。
換文文件結構如下:

 

緩存的文件mu lu

工作原理:

  • 每個緩存記錄在內存中的對象封裝爲Entry類

    
       private final class Entry {         
         private final String key; //緩存對應的key
         
         private final long[] lengths; //文件的長度
         private final File[] cleanFiles; //有效的數據文件
         private final File[] dirtyFiles; //正在修改的數據文件
     
         private boolean readable;//是否可讀
     
         private Editor currentEditor;//當前的編輯器。一個Entry一個時候只能被一個編輯器修改。
    
         private long sequenceNumber; //唯一序列號,相當於版本號,當內存中緩存數據發生變化時,該序列號會改變。
         
         ...    
         }
    
  • 創建緩存快照對象,作爲每次讀取緩存時的一個內容快照對象:

    
       public final class Snapshot implements Closeable {
         private final String key;   //緩存的key
         private final long sequenceNumber; //創建快照的時候的緩存序列號
         private final Source[] sources;//數據源,okio的API,可以直接讀取
         private final long[] lengths;//數據長度。
         ...
         }
    

    內容快照作爲讀去緩存的對象(而不是將Entry直接返回)的作用:
    (1). 內容快照的數據結構方式更便於數據的讀取(將file轉換爲source),並且隱藏了Entry的細節;
    (2). 內容快照在創建的時候記錄了當時的Entry的序列號。因而可以用快照的序列號與緩存的序列號對比,如果序列號不相同,則說明緩存數據發生了修改,該條數據就是失效的。

  • 緩存內容Entry的這種工作機制(單個editor,帶有序列號的內容快照)以最小的代價,實現了單線程修改,多線程讀寫的數據對象(否則則需要使用複雜的鎖機制)既添降低了邏輯的複雜性,又提高了性能(缺點就是高併發情況下,導致數據頻繁失效,導致緩存的命中率降低)。
  • 變化的序列號計數在很多涉及併發讀取的機制都有使用。比如:SQlite的連接。
  • DiskLruCache緩存文件工作流:

 

DiskLruCache緩存工作流

其返回的SnapShot數據快照,提供了Source接口(okio),供外部內類Cache直接轉化爲內存對象Cache.Entry。外部類進一步Canche.Entry轉化外OKHttpClient使用的Response。

OkHttpClient從緩存中獲取一個url對應的緩存數據的數據格式變化過程如下:

 

緩存數據格式變化過程

LRU的算法體現在:DiskLreCache的日誌操作過程中,每一次讀取緩存都產生一個READ的記錄。由於緩存的初始化是按照日誌文件的操作記錄順序來讀取的,所以這相當於把這條緩存數據放置到了緩存隊列的頂端,也就完成了LRU算法:last recent used,最近使用到的數據最新。

連接攔截器 和 最後的請求服務器的攔截器

這兩個連接器基本上完成了最後發起網絡請求的工作。追所以劃分爲兩個攔截器,除了解耦之外,更重要的是在這兩個流程之間還可以插入一個專門爲WebSocket服務的攔截器( WebSocket一種在單個 TCP 連接上進行全雙工通訊的協議,本文不做詳解)。

關於OKHttp如何真正發起網絡請求的,下面專門詳細講解。

發起網絡請求

  • 簡介:OKHttp的網絡請求的實現是socket(應用程序與網絡層進行交互的API)。socket發起網絡請求的流程一般是:
    (1). 創建socket對象;
    (2). 連接到目標網絡;
    (3). 進行輸入輸出流操作。
  • 在OKHttp框架裏面,(1)(2)的實現,封裝在connection接口中,具體的實現類是RealConnection。(3)是通過stream接口來實現,根據不同的網絡協議,有Http1xStream和Http2xStream兩個實現類。
  • 由於創建網絡連接的時間較久(如果是HTTP的話,需要進行三次握手),而請求經常是頻繁的碎片化的,所以爲了提高網絡連接的效率,OKHttp3實現了網絡連接複用:
  • 新建的連接connection會存放到一個緩存池connectionpool中。網絡連接完成後不會立即釋放,而是存活一段時間。網絡連接存活狀態下,如果有相同的目標連接,則複用該連接,用它來進行寫入寫出流操作。
  • 統計每個connection上發起網絡請求的次數,若次數爲0,則一段時間後釋放該連接。
  • 每個網絡請求對應一個stream,connection,connectionpool等數據,將它封裝爲StreamAllocation對象。

具體的流程見下圖:

 

網絡請求API流程

類之間的關係如下:

 

類之間的關係

幾個主要的概念:

Connection:連接。

真正的底層實現網絡連接的接口。它包含了連接的路線,物理層socket,連接協議,和握手信息。


public interface Connection {
  /** 返回連接線路信息(包涵url,dns,proxy) */
  Route route();

  /**
   * 返回連接使用的Socket(網絡層到應用層之間的一層封裝)
   */
  Socket socket();

  /**
   * 返回傳輸層安全協議的握手信息
   */
  Handshake handshake();

  /**
   * 返回網絡協議:Http1.0,Http1.1,Http2.0...
   */
  Protocol protocol();
}

在OKHttp3中的實現:RealConnection.

  • 包含了連接的信息,包括socket,它是與網絡層交互的接口,真正實現網絡連接;
  • 內部維護了一個列表List<StreamAllocation>,相當於發起網絡請求的引用計數容器。下面詳細討論。
  • 包涵輸入輸出流,source和sink。但是Connection只負責將socket的操作,與source和sink建立起連接,針對source和sink的寫入和讀取操作,交給響應的Stream完成(解耦)。將socket封裝爲okio的代碼:
    source = Okio.buffer(Okio.source(socket));
    sink = Okio.buffer(Okio.sink(socket));

這就將socket的讀寫操作標準華爲okio的API。

  • 還有個內部類framedConnection,專門用來處理Http2和SPDY(goole推出的網絡協議)的(不詳細討論)。

Stream: 完成網絡請求的讀寫流程功能。

接口,源碼如下:


public interface HttpStream {


  /** 創建請求的輸出流*/
  Sink createRequestBody(Request request, long contentLength);

  /** 寫請求的頭部信息(Header) */
  void writeRequestHeaders(Request request) throws IOException;

  /** 將請求寫入sokcet */
  void finishRequest() throws IOException;

  /** 讀取網絡返回的頭部信息(header)*/
  Response.Builder readResponseHeaders() throws IOException;

  /** 返回網絡返回的數據 */
  ResponseBody openResponseBody(Response response) throws IOException;

  /**
   * 異步的取消,並釋放相關的資源。(connect pool的線程會自動完成)
   */
  void cancel();
}
  • 這個功能實際上是從connection中剝離出來:connection負責底層連接的實現,其寫入request和讀取response的功能交給stream來完成。
  • 之所以需要將該功能解耦出來,一個重要的原因:根據不同的網絡請求協議,request的寫入和response的讀出,具有不同的實現。比如http1,http2等,響應的類爲Http1xStream,Http2xStream。

ConnectionPool:內存連接池

負責管理HTTP連接,可以複用相同Address(包括url,dns,port,是否加密,proxy等信息)的連接,以減少網絡延遲。

  • 維護了一個雙端隊列,默認的實現是:最多5個空閒的連接,這5個連接最多存活5分鐘。
  • 直到連接被清除,底層的socket連接纔會釋放。

StreamAllocation: 網絡流的分配計數器(下文簡稱SA)

計數功能:
每發起一個網絡請求,都會新建SA對象。由於連接connection可以被複用,所以一個connection可以對應多次網絡請求,即多個SA。所以RealConnection中維護了一個SA的列表。每次創建新的SA的時候,會在對應的connection的列表+1。以下情形下,會把列表-1,並釋放SA對應的資源:

  • 網絡請求完成;
  • 網絡請求時發生異常;
  • 重定向,則release原來的網絡請求,並新建新的;
  • 重定向次數超過限制(OKHttp3最大20);
  • 網絡返回信息頭部包涵"Connection:close"的信息;
  • 網絡返回的數據信息長度未知,則響應的connection不再分配stream。

該引用計數的列表的作用:統計一個connection對應的網絡請求的數量,如果爲空,則connection進入空閒,開始倒計時回收。未回收之前的connection都可以複用,降低網絡延遲(因爲創建和銷燬連接的開銷比較長,如果時HTTP,則需要三次握手)。

SA中的方法:

  • newStream: 創建新的連接,並從connectionPool中找能夠複用的connection,如果沒有能複用的,則新建connection,並加入到connectionPool中。
  • acquire:網絡被創建,將對應的connection中SA的引用+1.
  • release:網絡請求結束,將對應的connection中SA的引用-1.

最後的發起網絡請求的代碼


    @Override 
    public Response intercept(Chain chain) throws IOException {
    
        //獲取httpstream對象,在之前的ConnectInterceptor中創建
        HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();
        StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
        Request request = chain.request();
    
        long sentRequestMillis = System.currentTimeMillis();
        
        //寫請求的header
        httpStream.writeRequestHeaders(request);
        
        //寫請求的body
        if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
          Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
          BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
        
        //完成網絡請求
        httpStream.finishRequest();
    
        //讀取網絡請求返回的header
        Response response = httpStream.readResponseHeaders()
            .request(request)
            .handshake(streamAllocation.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    
        //讀取網絡請求返回的body
        if (!forWebSocket || response.code() != 101) {
          response = response.newBuilder()
              .body(httpStream.openResponseBody(response))
              .build();
        }
                    
        ...
        
        //返回結果
        return response;
        }  

深入網絡請求Socket



 

發佈了39 篇原創文章 · 獲贊 61 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章