OkHttp基礎概念解釋

最近在整理Android經常使用第三方框架相關的東西,說道Android的框架,無外乎就是Android開發中常見的網絡、圖片緩存、數據交互、優化、頁面等框架,其中網絡做爲一個基礎部分,我相信你們更多的是使用OkHttp,而在長鏈接中有Socket和webSocket等,今天給你們總結下OkHttp相關的內容,部分參考網絡資源。php

OkHttp簡介

OkHttp做爲時下Android最火的Http第三方庫能夠說被大多數的Android客戶端程序所使用,Retrofit底層也是使用OkHttp,與Volley等網絡請求框架相比,OkHttp具備以下的一些特色:html

  • HTTP/2支持全部訪問相同主機的請求共享一個套接字。也就是說支持Google的SPDY協議,若是 SPDY 不可用,則經過鏈接池來減小請求延時。
  • 鏈接池減小了請求延遲(若是HTTP/2不可用)。
  • 透明GZIP壓縮減小了下載大小。
  • 響應緩存徹底避免了重複請求的網絡使用。
  • 當網絡出現問題時,OkHttp 會自動重試一個主機的多個 IP 地址

OkHttp官網地址:http://square.github.io/okhttp/
OkHttp GitHub地址:https://github.com/square/okhttpjava

使用示例

OkHttp的使用也很是簡單,支持Get、Post等多種請求方式,而且支持文件等的上傳下載等多種功能,能夠說如今你業務中能涉及到的狀況,OkHttp都能解決。下面是一些簡單的使用示例。android

同步Get請求

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    Headers responseHeaders = response.headers();
    for (int i = 0; i < responseHeaders.size(); i++) {
      System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
    }

    System.out.println(response.body().string());
  }

不過須要注意的是,做用在響應主體上的string()方法對於小文檔來講是方便和高效的,可是若是響應主體比較大(大於1MB),應避免使用string(),由於它會加載整個文檔到內存中。nginx

異步Get請求

異步使用enqueue進行請求,例如:git

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();
      }

      @Override public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        Headers responseHeaders = response.headers();
        for (int i = 0, size = responseHeaders.size(); i < size; i++) {
          System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
        }

        System.out.println(response.body().string());
      }
    });
  }

設置Header

典型的HTTP頭工做起來像一個Map< String, String >,每個字段有一個值或沒有值。可是有一些頭容許多個值,像Guava的Multimap。github

使用Request進行請求頭信息的設置時,有些信息再次設置是不會被覆蓋的,例如addHeader(name, value),使用addHeader(name, value)來添加一個頭而不移除已經存在的頭。web

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println("Server: " + response.header("Server"));
    System.out.println("Date: " + response.header("Date"));
    System.out.println("Vary: " + response.headers("Vary"));
  }

上傳字符串

使用HTTP POST來發送請求(好比文件)主體到服務器,由於整個請求主體同時存在內存中,應避免使用這個API上傳大的文檔大於1MB。若是是大文件,可使用OKHttp的斷點續傳功能。算法

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

固然,OkHttp也支持以stream的形式來上傳文件等請求主體。json

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody requestBody = new RequestBody() {
      @Override public MediaType contentType() {
        return MEDIA_TYPE_MARKDOWN;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8("Numbers\n");
        sink.writeUtf8("-------\n");
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = 2; i < n; i++) {
          int x = n / i;
          if (x * i == n) return factor(x) + " × " + i;
        }
        return Integer.toString(n);
      }
    };

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

文件上傳

文件的上傳相對簡單,直接提供File的路徑便可。

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    File file = new File("README.md");

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

上傳表格參數

OkHtpp支持使用FormBody.Builder來構建一個工做起來像HTML< form >標籤的請求主體。鍵值對會使用一個兼容HTML form的URL編碼進行編碼。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody formBody = new FormBody.Builder()
        .add("search", "Jurassic Park")
        .build();
    Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

多部分請求

MultipartBody.Builder能夠構造複雜的請求主體與HTML文件上傳表單兼容。multipart請求主體的每部分自己就是一個請求主體,能夠定義它本身的頭。若是存在本身的頭,那麼這些頭應該描述部分主體,例如它的Content-Disposition。Content-Length和Content-Type會在其可用時自動添加。

private static final String IMGUR_CLIENT_ID = "...";
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
        .build();

    Request request = new Request.Builder()
        .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
        .url("https://api.imgur.com/3/image")
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

緩存響應設置

要設置緩存響應,你須要一個進行讀取和寫入的緩存目錄,以及一個緩存大小的限制。緩存目錄應該是私有的,且不被信任的應用不可以讀取它的內容。讓多個緩存同時訪問相同的混存目錄是錯誤的。大多數應用應該只調用一次new OkHttpClient(),配置它們的緩存,並在全部地方使用相同的實例。不然兩個緩存實例會相互進行干涉。

同時OkHttp還支持對緩存的時間和大小進行設置。如添加像Cache-Control:max-stale=3600設置請求頭緩存大小,使用Cache-Control:max-age=9600來配置響應緩存時間。

網絡超時配置

網絡部分多是因爲鏈接問題,服務器可用性問題或者其餘緣由形成網絡請求超時。因此在使用時,能夠根據實際狀況進行網絡的超時設置。

private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    Response response = client.newCall(request).execute();
    System.out.println("Response completed: " + response);
  }

取消請求

OkHttp支持取消網絡請求,使用Call.cancel()來當即中止一個正在進行的調用。若是一個線程正在寫請求或讀響應,它會接收到一個IOException,同步和異步調用均可以取消。

private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    try {
      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
      Response response = call.execute();
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e);
    }
  }

認證請求

若是網絡請求涉及到認證機制,OkHttp也提供了Authenticator來進行應用證書認證,Authenticator的實現應該構建一個包含缺失證書的新請求,若是沒有證書可用,返回null來跳太重試。

使用Response.challenges()來獲取全部認證挑戰的模式和領域。當完成一個Basic挑戰時,使用Credentials.basic(username,password)來編碼請求頭。涉及的示例以下:

private final OkHttpClient client;

  public Authenticate() {
    client = new OkHttpClient.Builder()
        .authenticator(new Authenticator() {
          @Override public Request authenticate(Route route, Response response) throws IOException {
            System.out.println("Authenticating for response: " + response);
            System.out.println("Challenges: " + response.challenges());
            String credential = Credentials.basic("jesse", "password1");
            return response.request().newBuilder()
                .header("Authorization", credential)
                .build();
          }
        })
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/secrets/hellosecret.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

OkHttp的Call

OkHttp支持重寫,重定向,跟進和重試,OkHttp會使用Call來模化知足請求的任務,然而中間的請求和響應是必要的。OkHttp提供了兩種方式的Call:

  • Synchronous:線程會阻塞直到響應可讀;
  • Asynchronous:在一個線程中入隊請求,當你的響應可讀時在另一個線程獲取回調。

請求能夠從任何線程取消,若是請求尚未執行完成,會使請求失敗,請求失敗會出現IOException異常錯誤。

OkHttp支持同步和異步方式請求,對於同步調用,使用的是本身的線程並對管理你同時建立多少請求負責。對於異步調用,Dispatcher實現了最大併發請求的策略,你能夠設置每一個服務器最大值(默認是5)和全部最大值(默認是64)。

OkHttp網絡連接

在使用OkHttp進行請求的時候,咱們只須要提供請求的url地址便可實現網絡的訪問,其實OkHttp在規劃鏈接服務器的鏈接時提供了三種類型:URL,Address和Route。
下面就分別來講一下這三種連接的關係即便用場合。

URL

URL是HTTP和網絡的最基本的聯繫方式,成爲統一資源定位符,URL是一個抽象的概念。

  • 它們規定了調用多是明文(http)或密文(https),可是沒有規定應該使用哪一個加密算法。也沒有規定如何驗證對等的證書(HostnameVerifier)或者哪一個證書可被信任(SSLSocketFactory)。
  • 每個URL肯定一個特定路徑,每一個服務器包含不少的URL。

Addresses

在OkHttp中,Addresses規定了服務器和全部鏈接服務器須要的靜態配置:端口號,HTTPS設置和優先網絡協議(如HTTP/2或SPDY)。共享相同address的URLs也可能共享相同的下層TCP socket鏈接。
共享一個鏈接有巨大的性能好處:低延遲,高吞吐量(由於TCP啓動慢)和節省電源。OkHttp使用ConnectionPool來自動複用HTTP/1.X鏈接和多路傳輸HTTP/2和SPDY鏈接。

在OkHttp中,address的一些字段來自URL(機制,主機名,端口),剩下的來自OkHttpClient。

Routes

Routes提供了真正鏈接到服務器所須要的動態信息,它會Routes明確的要嘗試的IP地址以及代理服務器,以及什麼版本的TLS來協商(針對HTTPS鏈接)。

對於一個地址有可能有不少路由,一個存在多個數據中心的網絡服務器可能在它的DNS響應中產生多個IP地址。

OkHttp網絡鏈接流程

當你使用OkHttp請求一個URL時,下面是它執行的流程:
1. 它使用URL和配置的OkHttpClient來建立一個address,這個address規定了如何鏈接到服務器。
2. OkHttp嘗試使用這個address從鏈接池中獲取一個鏈接。
3. 若是它沒有在池中找到一個鏈接,它會選擇一個route來嘗試。這一般意味着建立一個DNS請求來獲取服務器的IP地址。
4. 若是這是一個新route,它會經過構建一個直接的socket鏈接或一個TLS隧道或一個直接的TLS鏈接來進行鏈接。若是須要它會執行TLS握手。
5. 而後發送HTTP請求而後讀取響應。

當鏈接出現問題時,OkHttp會選擇另一個route進行嘗試。一旦接收到服務端的響應,鏈接就會返回到池中,這樣它能夠在以後的請求複用,鏈接空閒一段時間會從池中移除。

攔截器

看過OkHttp源碼分析的同窗對於攔截器確定不會陌生,在OkHttp中攔截器是全部的網絡請求的必經之地,攔截器主要有如下一些做用。

一、攔截器能夠一次性對全部的請求和返回值進行修改;
二、攔截器能夠一次性對請求的參數和返回的結果進行編碼,好比統一設置爲UTF-8;
三、攔截器能夠對全部的請求作統一的日誌記錄,不須要在每一個請求開始或者結束的位置都添加一個日誌操做;
四、其餘須要對請求和返回進行統一處理的需求….

下面是一個最簡單的攔截器使用,用來打印OkHttp的請求和收到的響應。

class LoggingInterceptor implements Interceptor { 
 
   
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}

OkHttp使用列表來跟蹤攔截器,而且攔截器按順序被調用。棲攔截的模型以下:
這裏寫圖片描述

OkHttp中的攔截器分爲兩類:APP層面的攔截器(Application Interception)、網絡請求層面的攔截器(Network Interception)。在OkHttp中,首先從App Interceptor開始,而後執行Network Interceptor,最後又回到App Interceptor。

應用攔截器

下面咱們使用OkHttpCleint.Builder上調用addInterceptor()來註冊一個應用攔截器。代碼以下:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

若是咱們須要將http://www.publicobject.com/helloworld.txt這個URL重定向到https://publicobject.com/helloworld.txt,那麼OkHttp會自動跟進這個重定向。下面是重定向的相關的執行信息:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive 

經過日誌,咱們能夠看到OkHttp已經重定向了,能夠經過引文reponse.request().url()與request.url()不一樣來區分。咱們發現,應用攔截器只會被調用一次,而且從chain.proceed()返回的響應是重定向後的響應。

網絡攔截器

註冊一個網絡攔截器很類似,調用addNetworkInterceptor()替代addInterceptor()。一樣是上面的實例:

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

當咱們運行這個代碼,攔截器會執行兩次:一次是訪問http://www.publicobject.com/helloworld.txt的初始請求,另一個是重定向到https://publicobject.com/helloworld.txt

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/html Content-Length: 193 Connection: keep-alive Location: https://publicobject.com/helloworld.txt INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} User-Agent: OkHttp Example Host: publicobject.com Connection: Keep-Alive Accept-Encoding: gzip INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive 

網絡請求也包含更多數據,例如經過OkHttp添加的Accept-Encoding:gzip頭來通知支持響應壓縮。網絡攔截器的Chain有一個非空Connection,能夠用來訪問IP地址和用來鏈接網絡服務器的TLS配置。

應用攔截器VS網絡攔截器

選擇哪一種攔截器須要根據實際狀況,每種攔截器chain都有本身相對的優點。

應用攔截器

  • 不須要關心像重定向和重試這樣的中間響應;
  • 老是調用一次,即便HTTP響應從緩存中獲取服務;
  • 監視應用原始意圖。不關心OkHttp注入的像If-None-Match頭;
  • 容許短路並不調用Chain.proceed();
  • 容許重試並執行多個Chain.proceed()調用。

網絡攔截器

  • 能夠操做像重定向和重試這樣的中間響應;
  • 對於短路網絡的緩存響應不會調用;
  • 監視即將要經過網絡傳輸的數據;
  • 訪問運輸請求的Connection。

重寫請求

攔截器支持添加,移除或替換請求頭,若是有請求主體,它們也能夠改變。例如,若是你鏈接一個已知支持請求主體壓縮的網絡服務器,你還可使用一個應用攔截器來添加請求主體壓縮。

final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

重寫響應

固然,攔截器也能夠重寫響應頭而且改變響應主體。若是你在一個棘手的環境下並準備處理結果,重寫響應頭是一個解決問題強大的方式。

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

OkHttp使用Https

關於Https及其工做的流程本文不作任何的介紹,本文主要介紹在OkHttp中如何使用Https進行網絡校驗即請求。在使用OkHttpClient初始化OkHttpClient對象時,有兩個關鍵的地方須要注意:hostnameVerifier和sslSocketFactory。

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(20000L, TimeUnit.MILLISECONDS)
                .readTimeout(20000L, TimeUnit.MILLISECONDS)
                .addInterceptor(new LoggerInterceptor("TAG"))
                .hostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                })
                .sslSocketFactory(sslParams.sSLSocketFactory,sslParams.trustManager)
                .build();

其中sslSocketFactory傳入兩個參數,一個是SSLSocketFactory,另外一個是TrustManager,一般都是寫一個HttpsUtils,裏面持有這兩個對象,讀取本地的一個證書,進行相關初始化賦值動做。 hostnameVerifier則是對服務端返回的一些信息進行相關校驗的地方, 用於客戶端判斷所鏈接的服務端是否可信,一般默認return true。

public boolean verify(String host, X509Certificate certificate) {
    return verifyAsIpAddress(host)
        ? verifyIpAddress(host, certificate)
        : verifyHostname(host, certificate);
  }

OkHttp的驗證邏輯

對於一個android開發者來講,目前的網絡請求框架大部分都是使用okhttp進行網絡請求的,因此瞭解okhttp是如何具體工做的對於咱們平時開發有很大的幫助的。當咱們使用https進行網絡請求的時候最終進行鏈接的類是RealConnection,該類的關鍵代碼以下:

private void connectTls(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
    //建立Socket
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }
      // Force handshake. This can throw!
        //初次握手
      sslSocket.startHandshake();
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
      // Verify that the socket's certificates are acceptable for the target host.
    //校驗,回調hostnameVerifier.verify方法
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n certificate: " + CertificatePinner.pin(cert)
            + "\n DN: " + cert.getSubjectDN().getName()
            + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }
      // Check that the certificate pinner is satisfied by the certificates presented.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

在該類中,咱們主要關心的地方也是在初次握手創建鏈接和本地校驗的那,正常狀況下,咱們在調用https地址的時候會先鏈接,就是調到上面代碼的位置,以後執行初次握手,回調驗證服務端是否可信,而後在進行正常的網絡請求。若是在這個過程當中出現異常,就會報一個證書信任的問題,出現這種狀況有兩方面,一是客戶端驗證服務端,二是服務端驗證客戶端。

證書獲取

下面介紹下證書獲取的相關內容,證書校驗主要用到了hostnameVerifier.verify(),該方法的源碼以下:

@Override
 public boolean verify(String hostname, SSLSession session) {
       Certificate[] localCertificates = new Certificate[0];
       try {
     //獲取證書鏈中的全部證書
           localCertificates = session.getPeerCertificates();
         } catch (SSLPeerUnverifiedException e) {
                e.printStackTrace();
           }
    //打印全部證書內容
        for (Certificate c : localCertificates) {
          Log.d(TAG, "verify: "+c.toString());
        }
     try {
    //將證書鏈中的第一個寫到文件
           createFileWithByte(localCertificates[0].getEncoded());
            } catch (CertificateEncodingException e) {
              e.printStackTrace();
            }
       return true;
       }
    //寫到文件
    private void createFileWithByte(byte[] bytes) {
        // TODO Auto-generated method stub
        /** * 建立File對象,其中包含文件所在的目錄以及文件的命名 */
        File file = new File(Environment.getExternalStorageDirectory(),
                "ca.cer");
        // 建立FileOutputStream對象
        FileOutputStream outputStream = null;
        // 建立BufferedOutputStream對象
        BufferedOutputStream bufferedOutputStream = null;
        try {
            // 若是文件存在則刪除
            if (file.exists()) {
                file.delete();
            }
            // 在文件系統中根據路徑建立一個新的空文件
            file.createNewFile();
            // 獲取FileOutputStream對象
            outputStream = new FileOutputStream(file);
            // 獲取BufferedOutputStream對象
            bufferedOutputStream = new BufferedOutputStream(outputStream);
            // 往文件所在的緩衝輸出流中寫byte數據
            bufferedOutputStream.write(bytes);
            // 刷出緩衝輸出流,該步很關鍵,要是不執行flush()方法,那麼文件的內容是空的。
            bufferedOutputStream.flush();
        } catch (Exception e) {
            // 打印異常信息
            e.printStackTrace();
        } finally {
            // 關閉建立的流對象
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedOutputStream != null) {
                try {
                    bufferedOutputStream.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
        }
    }

hostnameVerifier主要有兩個參數,一個是hostname就是你請求地址的host,session則包括了從服務端返回的證書鏈。
證書鏈一般有三個,第一個是咱們本身的,而後也能在本地看到證書文件。包含一些相關信息,包括公鑰,頒發機構等,最爲嚴苛的方式就是能夠從本地讀取一個證書,取公鑰與服務器返回的證書公鑰進行對比。

可是證書也不是徹底安全的,CertificatePinner就是一個用來限制哪些證書和證書頒發機構能夠被信任。證書鎖定提高安全性,可是限制你的服務器團隊更新他們的TLS證書的能力。例如:

public CertificatePinning() {
    client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder()
            .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
            .build())
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/robots.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    for (Certificate certificate : response.handshake().peerCertificates()) {
      System.out.println(CertificatePinner.pin(certificate));
    }
  }

自定義可信任的證書

固然,也可使用自定義的證書來替換主機的證書,而後使用sslSocketFactory函數進行設置。

private final OkHttpClient client;

  public CustomTrust() {
    SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream());
    client = new OkHttpClient.Builder()
        .sslSocketFactory(sslContext.getSocketFactory())
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    System.out.println(response.body().string());
  }

  private InputStream trustedCertificatesInputStream() {
    ... // Full source omitted. See sample.
  }

  public SSLContext sslContextForTrustedCertificates(InputStream in) {
    ... // Full source omitted. See sample.
  }

SSLSocketFactory

安全套接層工廠,用於建立SSLSocket,默認的SSLSocket是信任手機內置信任的證書列表,咱們能夠經過OKHttpClient.Builder的sslSocketFactory方法定義本身的信任策略。下面是加載SSLSocketFactory的相關代碼:

public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) {
        try {
//用咱們的證書建立一個keystore
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = "server"+Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                try {
                    if (certificate != null) {
                        certificate.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
//建立一個trustmanager,只信任咱們建立的keystore
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            sslContext.init(
                    null,
                    trustManagerFactory.getTrustManagers(),
                    new SecureRandom()
            );
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

X509TrustManager

public interface X509TrustManager extends TrustManager { 
 
   
    void checkClientTrusted(X509Certificate[] var1, String var2) throws CertificateException;

    void checkServerTrusted(X509Certificate[] var1, String var2) throws CertificateException;

    X509Certificate[] getAcceptedIssuers();
}

HostnameVerifier

HostnameVerifier的接口定義以下:

public interface HostnameVerifier {
    boolean verify(String var1, SSLSession var2);
}

這個接口主要實現對於域名的校驗,OKHTTP實現了一個OkHostnameVerifier,對於證書中的IP及Host作了各類正則匹配,默認狀況下使用的是這個策略。相關代碼以下:

OKHttpClient.Builder.hostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                })

在實際使用中能夠將上面的東西封裝起來,例如:

public class SSLSocketClient{ 
 
   
     //獲取這個SSLSocketFactory 
    public static SSLSocketFactory getSSLSocketFactory(){
         try{
             SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, getTrustManager(), new SecureRandom());
             return sslContext.getSocketFactory();
        }
         catch (Exception e){
             throw new RuntimeException(e);
        }
     }

   //獲取TrustManager 
     private static TrustManager[] getTrustManager(){
         TrustManager[] trustAllCerts = new TrustManager[]{
  
  
   
   
            
   

  new X509TrustManager(){
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType){
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType){
            }
            @Override
            public X509Certificate[] getAcceptedIssuers(){
                 return new X509Certificate[]{};
             }
        }};
         return trustAllCerts;
    }

    //獲取HostnameVerifier 
     public static HostnameVerifier getHostnameVerifier(){
        HostnameVerifier hostnameVerifier = new HostnameVerifier(){
            @Override
            public boolean verify(String s, SSLSession sslSession){
                return true;
            }
        };
         return hostnameVerifier;
     }
 }

而後在須要使用的使用的地方

OkHttpClient.Builder builder=new OkHttpClient.Builder();
...
builder.sslSocketFactory(SSLSocketClient.getSSLSocketFactory();
builder.hostnameVerifier(SSLSocketClient.getHostnameVerifier();

本文同步分享在 博客「xiangzhihong8」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索