1. 程式人生 > >OkHttp3的原始碼探究(一)okHttp的使用場景

OkHttp3的原始碼探究(一)okHttp的使用場景

版權宣告:本文為博主原創文章,轉載請註明出處。

一、前言
對於開發者來說優秀的原始碼是最好的學習資源。通過閱讀優質原始碼就相當於和大牛有一次對話。
OkHttp是支援HTTP和HTTP/2的網路請求框架。自從Android4.4開始,Google已經開始將原始碼中的HttpURLConnection替換為OkHttp,而在Android6.0之後的SDK中google更是移除了對於HttpClient的支援,在專案中用的比較多的Retrofit同樣是對OkHttp進行再次封裝而來的。Okhttp3中提供了Builder,很好的使用了建立者設計模式。這裡來探究一下Okhttp3的原始碼。

注:這個系列的文章是根據OkHttp最新的版本3.6.0進行的。
二,原始碼閱讀心得
對於開源專案原始碼的閱讀,自己總結了以下的方式:
1.先要了解該專案的基本用法。
2.根據基本用法去檢視各個模組的原始碼。
3.在各個模組瞭解的基礎上,再整體的去把握一下。
三,前期準備
工欲善其事必先利其器,很多原始碼分析的文章不會涉及到這一點,為了更好的方便讀者進行原始碼閱讀,這裡向大家介紹一下前期的準備工作。

閱讀工具:IntelliJ IDEA ,和Android Studio快捷鍵類似。下載地址:idea
原始碼下載:okhttp原始碼
可以下載zip包然後匯入或通過cvs的github選項線上下載。Okhttp是使用Maven構建,也可以瞭解一下Maven相關的知識。

四,OkHttp3的原始碼目錄。
構建完成我們可以看到以下的目錄:

OKHttp原始碼目錄

五,OkHttp的使用場景。
閱讀原始碼的第一步先要了解它的使用場景。OkHttp的原始碼的使用場景在他的原始碼中有體現,就是上圖中的samples。讀者可以去程式碼中閱讀,也可以來看我下面的例子。
1.Get請求
Get同步請求(提示:Android 要在子執行緒)

public class GetExample {
  OkHttpClient client = new OkHttpClient();

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

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

  public static void main(String[] args) throws IOException {
    GetExample example = new GetExample();
    String response = example.run("https://raw.github.com/square/okhttp/master/README.md");
    System.out.println(response);
  }
}

Get非同步請求:

public final class AsynchronousGet {
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();
    //非同步請求添加了callback
    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 {
        try (ResponseBody responseBody = response.body()) {
          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(responseBody.string());
        }
      }
    });
  }

  public static void main(String... args) throws Exception {
    new AsynchronousGet().run();
  }
}

2.Post請求
Post同步請求:

public class PostExample {
  public static final MediaType JSON
      = MediaType.parse("application/json; charset=utf-8");

  OkHttpClient client = new OkHttpClient();

  String post(String url, String json) throws IOException {
    RequestBody body = RequestBody.create(JSON, json);
    Request request = new Request.Builder()
        .url(url)
        .post(body)
        .build();
    try (Response response = client.newCall(request).execute()) {
      return response.body().string();
    }
  }

  String bowlingJson(String player1, String player2) {
    return "{'winCondition':'HIGH_SCORE',"
        + "'name':'Bowling',"
        + "'round':4,"
        + "'lastSaved':1367702411696,"
        + "'dateStarted':1367702378785,"
        + "'players':["
        + "{'name':'" + player1 + "','history':[10,8,6,7,8],'color':-13388315,'total':39},"
        + "{'name':'" + player2 + "','history':[6,10,5,10,10],'color':-48060,'total':41}"
        + "]}";
  }

  public static void main(String[] args) throws IOException {
    PostExample example = new PostExample();
    String json = example.bowlingJson("Jesse", "Jake");
    String response = example.post("http://www.roundsapp.com/post", json);
    System.out.println(response);
  }
}

Post非同步和Get類似,新增CallBack
3.新增請求頭資訊和獲得請求頭資訊
OkHttp的API,試圖使這兩種情況下都能舒適使用。當寫請求頭,用header(name, value)來為唯一出現的name設定value。如果它本身存在值,在新增新的value之前,他們會被移除。使用addHeader(name, value)來新增頭部不需要移除當前存在的headers。

public final class AccessHeaders {
  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();

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

  public static void main(String... args) throws Exception {
    new AccessHeaders().run();
  }
}

這裡看到Vary欄位,想了解的同學可以看看這個HTTP 協議中 Vary 的一些研究
4.登入認證
我們在訪問一個網站的時候,有時候要去登入認證後才能訪問,如:
http://publicobject.com/secrets/hellosecret.txt ,OkHttp提供了這樣的功能。

public final class Authenticate {
  private final OkHttpClient client;

  public Authenticate() {
    client = new OkHttpClient.Builder()
        .authenticator(new Authenticator() {
          @Override public Request authenticate(Route route, Response response) throws IOException {
            if (response.request().header("Authorization") != null) {
              return null; // Give up, we've already attempted to authenticate.
            }

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

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

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

  public static void main(String... args) throws Exception {
    new Authenticate().run();
  }
}

5.設定請求快取

public final class CacheResponse {
  private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)
        .build();
  }

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

    String response1Body;
    try (Response response1 = client.newCall(request).execute()) {
      if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

      response1Body = response1.body().string();
      System.out.println("Response 1 response:          " + response1);
      System.out.println("Response 1 cache response:    " + response1.cacheResponse());
      System.out.println("Response 1 network response:  " + response1.networkResponse());
    }

    String response2Body;
    try (Response response2 = client.newCall(request).execute()) {
      if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

      response2Body = response2.body().string();
      System.out.println("Response 2 response:          " + response2);
      System.out.println("Response 2 cache response:    " + response2.cacheResponse());
      System.out.println("Response 2 network response:  " + response2.networkResponse());
    }

    System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  }

  public static void main(String... args) throws Exception {
    new CacheResponse(new File("CacheResponse.tmp")).run();
  }
}

執行的結果如下:

Response 1 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response:    null
Response 1 network response:  Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}

Response 2 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response:    Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response:  null
Response 2 equals Response 1? true

從結果來看第二次的cache response不為null,而network response為null。
6.取消請求

public class CancelCall {
  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);

    System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
    try (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);
    }
  }

  public static void main(String... args) throws Exception {
    new CancelCall().run();
  }
}

ScheduledExecutorService的介紹:Java併發包:ScheduledExecutorService
System.nanoTime返回的是毫微秒為單位的,1秒=1000毫秒=1000 000微秒=1000 000 000 毫微秒。這裡1e9f指的是1e9是冪指數,f表示布林型別。
例子的執行結果是:

0.01 Executing call.
1.01 Canceling call.
1.01 Canceled call.
1.02 Call failed as expected: java.net.SocketException: Socket closed

這個例子是請求一個延遲兩秒才有資料返回的介面,從結果來看在1s內將請求取消了。
7.設定固定證書

public final class CertificatePinning {
  private final OkHttpClient client;

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

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

  public static void main(String... args) throws Exception {
    new CertificatePinning().run();
  }
}

固定證書避免了信任證書頒發機構的需要。
8.配置超時時間

public final class ConfigureTimeouts {
  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();

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

  public static void main(String... args) throws Exception {
    new ConfigureTimeouts().run();
  }
}

這個例子當配置為下面的情況時可以看到異常:

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

9.獲得網路請求的相關資訊

public final class LoggingInterceptors {
  private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
  private final OkHttpClient client = new OkHttpClient.Builder()
      .addInterceptor(new LoggingInterceptor())
      .build();

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

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

  private static class LoggingInterceptor implements Interceptor {
    @Override public Response intercept(Chain chain) throws IOException {
      long t1 = System.nanoTime();
      Request request = chain.request();
      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",
          request.url(), (t2 - t1) / 1e6d, response.headers()));
      return response;
    }
  }

  public static void main(String... args) throws Exception {
    new LoggingInterceptors().run();
  }
}

執行的結果:

Mar 13, 2017 3:41:32 PM okhttp3.recipes.LoggingInterceptors$LoggingInterceptor intercept
INFO: Sending request https://publicobject.com/helloworld.txt on null

Mar 13, 2017 3:41:35 PM okhttp3.recipes.LoggingInterceptors$LoggingInterceptor intercept
INFO: Received response for https://publicobject.com/helloworld.txt in 2682.8ms
Server: nginx/1.10.0 (Ubuntu)
Date: Mon, 13 Mar 2017 07:41:26 GMT
Content-Type: text/plain
Content-Length: 1759
Last-Modified: Tue, 27 May 2014 02:35:47 GMT
Connection: keep-alive
ETag: "5383fa03-6df"
Accept-Ranges: bytes

這裡我們可以看到請求的資訊和返回的資料資訊。
10.上傳檔案

public final class PostFile {
  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();

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

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

  public static void main(String... args) throws Exception {
    new PostFile().run();
  }
}

11.上傳表單
訪問wiki並搜尋"Jurassic Park"
在url中相當於訪問:[https://en.wikipedia.org/w/index.php?search=Jurassic Park](https://en.wikipedia.org/w/index.php?search=Jurassic Park)

public final class PostForm {
  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();

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

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

  public static void main(String... args) throws Exception {
    new PostForm().run();
  }
}

12.上傳表單,新增多個屬性

public final class PostMultipart {
  /**
   * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
   * these examples, please request your own client ID! https://api.imgur.com/oauth2
   */
  private static final String IMGUR_CLIENT_ID = "9199fdef135c122";
  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();

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

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

  public static void main(String... args) throws Exception {
    new PostMultipart().run();
  }
}

13.上傳流

public final class PostStreaming {
  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();

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

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

  public static void main(String... args) throws Exception {
    new PostStreaming().run();
  }
}

這裡用到了okio,一個不錯的框架,這裡有一篇部落格介紹的不錯Android 善用Okio簡化處理I/O操作
14.上傳字串

public final class PostString {
  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();

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

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

  public static void main(String... args) throws Exception {
    new PostString().run();
  }
}

15.得到進度

public final class Progress {

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

    final ProgressListener progressListener = new ProgressListener() {
      @Override public void update(long bytesRead, long contentLength, boolean done) {
        System.out.println(bytesRead);
        System.out.println(contentLength);
        System.out.println(done);
        System.out.format("%d%% done\n", (100 * bytesRead) / contentLength);
      }
    };

    OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new Interceptor() {
          @Override public Response intercept(Chain chain) throws IOException {
            Response originalResponse = chain.proceed(chain.request());
            return originalResponse.newBuilder()
                .body(new ProgressResponseBody(originalResponse.body(), progressListener))
                .build();
          }
        })
        .build();

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

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

  public static void main(String... args) throws Exception {
    new Progress().run();
  }

  private static class ProgressResponseBody extends ResponseBody {

    private final ResponseBody responseBody;
    private final ProgressListener progressListener;
    private BufferedSource bufferedSource;

    public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
      this.responseBody = responseBody;
      this.progressListener = progressListener;
    }

    @Override public MediaType contentType() {
      return responseBody.contentType();
    }

    @Override public long contentLength() {
      return responseBody.contentLength();
    }

    @Override public BufferedSource source() {
      if (bufferedSource == null) {
        bufferedSource = Okio.buffer(source(responseBody.source()));
      }
      return bufferedSource;
    }

    private Source source(Source source) {
      return new ForwardingSource(source) {
        long totalBytesRead = 0L;

        @Override public long read(Buffer sink, long byteCount) throws IOException {
          long bytesRead = super.read(sink, byteCount);
          // read() returns the number of bytes read, or -1 if this source is exhausted.
          totalBytesRead += bytesRead != -1 ? bytesRead : 0;
          progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
          return bytesRead;
        }
      };
    }
  }

  interface ProgressListener {
    void update(long bytesRead, long contentLength, boolean done);
  }
}

下載的時候可以拿到progress。
16.響應webSocket

public final class WebSocketEcho extends WebSocketListener {
  private void run() {
    OkHttpClient client = new OkHttpClient.Builder()
        .readTimeout(0,  TimeUnit.MILLISECONDS)
        .build();

    Request request = new Request.Builder()
        .url("ws://echo.websocket.org")
        .build();
    client.newWebSocket(request, this);

    // Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
    client.dispatcher().executorService().shutdown();
  }

  @Override public void onOpen(WebSocket webSocket, Response response) {
    webSocket.send("Hello...");
    webSocket.send("...World!");
    webSocket.send(ByteString.decodeHex("deadbeef"));
    webSocket.close(1000, "Goodbye, World!");
  }

  @Override public void onMessage(WebSocket webSocket, String text) {
    System.out.println("MESSAGE: " + text);
  }

  @Override public void onMessage(WebSocket webSocket, ByteString bytes) {
    System.out.println("MESSAGE: " + bytes.hex());
  }

  @Override public void onClosing(WebSocket webSocket, int code, String reason) {
    webSocket.close(1000, null);
    System.out.println("CLOSE: " + code + " " + reason);
  }

  @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) {
    t.printStackTrace();
  }

  public static void main(String... args) {
    new WebSocketEcho().run();
  }
}

WebSocket是基於H5的一種全雙工通訊,這裡有一篇文章介紹得到很好。Java後端WebSocket的Tomcat實現
六.總結
到此OkHttp常用的場景已經介紹完了,從例子中我們可以學到很多的東西,下一篇開始原始碼閱讀分析。