OkHttp解析(三)關(guān)于Okio

OkHttp解析系列

OkHttp解析(一)從用法看清原理
OkHttp解析(二)網(wǎng)絡(luò)連接
OkHttp解析(三)關(guān)于Okio

從前兩篇文章我們知道,在OkHttp底層網(wǎng)絡(luò)連接是使用Socket,連接成功后則通過Okio庫與遠程socket建立了I/O連接,接著調(diào)用createTunnel創(chuàng)建代理隧道,在這里HttpStream與Okio建立了I/O連接。本篇文章就來看看Okio的使用

Okio

最新的Okio上看它的說明
這里介紹到

Okio 補充了 java.iojava.nio 的內(nèi)容,使得數(shù)據(jù)訪問、存儲和處理更加便捷。

ByteString and Buffer


Okio則建立在ByteStrings和Buffers上

  • ByteStrings:它是一個不可變的字節(jié)序列,對于字符數(shù)據(jù)來說,String是非常基礎(chǔ)的,但在二進制數(shù)據(jù)的處理中,則沒有與之對應(yīng)的存在,ByteString 應(yīng)運而生。ByteStrings很多方法與String用法一樣,它更容易把一些二進制數(shù)據(jù)當作一個值來處理,它更容易處理一些二進制數(shù)據(jù)。此外它也可以把二進制數(shù)據(jù)編解碼為十六進制(hex),base64和UTF-8格式。
    它向我們提供了和 String 非常類似的 API:

    • 獲取字節(jié):指定位置,或者整個數(shù)組;

    • 編解碼:hex,base64,UTF-8;

    • 判等,查找,子串等操作;

  • Buffer:Buffer 是一個可變的字節(jié)序列,就像 ArrayList 一樣。我們使用時只管從它的頭部讀取數(shù)據(jù),往它的尾部寫入數(shù)據(jù)就行了,而無需考慮容量、大小、位置等其他因素。

Source and Sink


Okio 吸收了 java.io 一個非常優(yōu)雅的設(shè)計:流(stream),流可以一層一層套起來,不斷擴充能力,最終完成像加密和壓縮這樣復雜的操作。這正是“修飾模式”的實踐。

修飾模式,是面向?qū)ο缶幊填I(lǐng)域中,一種動態(tài)地往一個類中添加新的行為的設(shè)計模式。就功能而言,修飾模式相比生成子類更為靈活,這樣可以給某個對象而不是整個類添加一些功能。

Okio 有自己的流類型,那就是 SourceSink,它們和 InputStreamOutputStream 類似,前者為輸入流,后者為輸出流。

它們還有一些新特性:

  • 超時機制,所有的流都有超時機制;

  • API 非常簡潔,易于實現(xiàn);

  • SourceSink 的 API 非常簡潔,為了應(yīng)對更復雜的需求,Okio 還提供了 BufferedSourceBufferedSink 接口,便于使用(按照任意類型進行讀寫,BufferedSource 還能進行查找和判等);

  • 不再區(qū)分字節(jié)流和字符流,它們都是數(shù)據(jù),可以按照任意類型去讀寫;

  • 便于測試,Buffer 同時實現(xiàn)了 BufferedSource(讀) 和 BufferedSink(寫) 接口,便于測試;

介紹完上面幾個類后,看個UML圖,理解他們之間的關(guān)系

Okio類圖

可以看到Buffer這里實現(xiàn)了兩個接口,它集 BufferedSourceBufferedSink 的功能于一身,為我們提供了訪問數(shù)據(jù)緩沖區(qū)所需要的一切 API。
而這里ReadBufferSourceReadBufferSink雖然各自實現(xiàn)了單獨的接口,但他們內(nèi)部都保存了個成員變量Buffer,而Buffer卻涵蓋了兩者。在ReadBufferSourceReadBufferSink中調(diào)用讀寫實際上是調(diào)用到了Buffer的讀寫。這種設(shè)計有點類似裝飾模式

官方例子


我們來看一下官方文檔中 PNG 解碼的例子:

private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");

public void decodePng(InputStream in) throws IOException {
  try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
    ByteString header = pngSource.readByteString(PNG_HEADER.size());
    if (!header.equals(PNG_HEADER)) {
      throw new IOException("Not a PNG.");
    }
    ...
}

我們先一點一點看,這里有個靜態(tài)成員變量PNG_HEADER,它則是把相應(yīng)的十六進制字符串轉(zhuǎn)換為相應(yīng)的字節(jié)串。

 public static ByteString decodeHex(String hex) {
    if (hex == null) throw new IllegalArgumentException("hex == null");
    if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex);

    byte[] result = new byte[hex.length() / 2];
    for (int i = 0; i < result.length; i++) {
      int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4;
      int d2 = decodeHexDigit(hex.charAt(i * 2 + 1));
      result[i] = (byte) (d1 + d2);
    }
    return of(result);
  }
  
  public static ByteString of(byte... data) {
    if (data == null) throw new IllegalArgumentException("data == null");
    return new ByteString(data.clone());
 }

可以看到,這里把十六進制中每個字符通過decodeHexDigit方法轉(zhuǎn)換為對應(yīng)的字節(jié),再存放到字節(jié)數(shù)組中,最后調(diào)用of方法來創(chuàng)建出ByteString

繼續(xù)看官方例子

public void decodePng(InputStream in) throws IOException {
  try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
    ByteString header = pngSource.readByteString(PNG_HEADER.size());
    if (!header.equals(PNG_HEADER)) {
      throw new IOException("Not a PNG.");
    }

    while (true) {
      Buffer chunk = new Buffer();

      // Each chunk is a length, type, data, and CRC offset.
      int length = pngSource.readInt();
      String type = pngSource.readUtf8(4);
      pngSource.readFully(chunk, length);
      int crc = pngSource.readInt();

      decodeChunk(type, chunk);
      if (type.equals("IEND")) break;
    }
  }
}

我們先來看下Okio.buffer(Okio.source(in))這里

private static Source source(final InputStream in, final Timeout timeout) {
    return new Source() {
      @Override public long read(Buffer sink, long byteCount) throws IOException {
       ...
      }
       ...
    };
  }

public static BufferedSource buffer(Source source) {
    return new RealBufferedSource(source);
  }

可以看到,首先調(diào)用Okio.source(in)InputStream輸入流轉(zhuǎn)換為Source,接著調(diào)用buffer方法創(chuàng)建了RealBufferedSource它實現(xiàn)了BufferSource方法。
此時這個pngSource則代表了圖片的輸入流信息

接著調(diào)用ByteString header = pngSource.readByteString(PNG_HEADER.size());
來讀取圖片首部的字節(jié)串

@Override public ByteString readByteString(long byteCount) throws IOException {
    require(byteCount);
    return buffer.readByteString(byteCount);
  }

可以看到,最終的讀取轉(zhuǎn)換則是通過Buffer來進行調(diào)用。而Buffer同樣也實現(xiàn)了和ReadBufferdSource的接口BufferedSource。

為什么要這么折騰呢?明明可以簡單的調(diào)用ReadBufferedSource為什么還要通過Buffer來調(diào)用?

讓我們從功能需求和設(shè)計方案來考慮。

BufferedSource 要提供各種形式的讀取操作,還有查找與判等操作。大家可能會想,那我就在實現(xiàn)類中自己實現(xiàn)不就好了嗎?干嘛要經(jīng)過 Buffer 中轉(zhuǎn)呢?這里我們實現(xiàn)的時候,需要考慮效率的問題,而且不僅 BufferedSource 需要高效實現(xiàn),BufferedSink 也需要高效實現(xiàn),這兩者的高效實現(xiàn)技巧,很大部分都是共通的,所以為了避免同樣的邏輯重復兩遍,Okio 就直接把讀寫操作都實現(xiàn)在了 Buffer 這一個類中,這樣邏輯更加緊湊,更加內(nèi)聚。而且還能直接滿足我們對于“兩用數(shù)據(jù)緩沖區(qū)”的需求:既可以從頭部讀取數(shù)據(jù),也能向尾部寫入數(shù)據(jù)。至于我們單獨的讀寫操作需求,Okio 就為 Buffer 分別提供了委托類:RealBufferedSource 和 RealBufferedSink,實現(xiàn)好 Buffer 之后,它們兩者的實現(xiàn)將非常簡潔(前者 450 行,后者 250 行)。

OkHttp里面Okio的使用


前面說到

在OkHttp底層網(wǎng)絡(luò)連接是使用Socket,連接成功后則通過Okio庫與遠程socket建立了I/O連接,接著調(diào)用createTunnel創(chuàng)建代理隧道,在這里HttpStream與Okio建立了I/O連接。

我們直接定位到RealConnection.connectSocket方法這里

 private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    try {
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      throw new ConnectException("Failed to connect to " + route.socketAddress());
    }
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
  }

可以看到,這里根據(jù)挑選出來的線路代理,創(chuàng)建完Socket后,調(diào)用了連接,連接成功后,則使用Okio.sourceOkio.sink打開對應(yīng)的輸入輸出流保存到BufferedSource sourceBufferedSink中。

之后再把創(chuàng)建出來的source和sink綁定到HttpStream,使得HttpStream擁有兩者的調(diào)用。
前一篇文章說到,當Socket連接完成后,就會根據(jù)source和sink來選擇創(chuàng)建對應(yīng)HttpStream

  public HttpStream newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
      ...
      HttpStream resultStream;
      if (resultConnection.framedConnection != null) {
        resultStream = new Http2xStream(client, this, resultConnection.framedConnection);
      } else {
        resultConnection.socket().setSoTimeout(readTimeout);
        resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
        resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
        resultStream = new Http1xStream(
            client, this, resultConnection.source, resultConnection.sink);
    ...
}

之后就可以進行讀取和寫入數(shù)據(jù)。
寫入數(shù)據(jù)的話,由第一篇文章可知道是在CallServerInterceptor中,在里面寫入我們的請求體

// CallServerInterceptor#intercept
// 發(fā)送請求 body
Sink requestBodyOut = httpCodec.createRequestBody(request, 
        request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();

// 讀取響應(yīng) body
response = response.newBuilder()
    .body(httpCodec.openResponseBody(response))
    .build();

可以看到在這里,先調(diào)用了createRequestBody來根據(jù)request創(chuàng)建一個Sink不過此時還未寫入數(shù)據(jù),里面只是空的,只是根據(jù)request來選擇創(chuàng)建Sink而已

 @Override public Sink createRequestBody(Request request, long contentLength) {
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
      // Stream a request body of unknown length.
      return newChunkedSink();
    }
    if (contentLength != -1) {
      // Stream a request body of a known length.
      return newFixedLengthSink(contentLength);
    }
    ...
  }

接著把創(chuàng)建好的Sink包裝到BufferedSink中,最終調(diào)用request.body().writeTo(bufferedRequestBody);來把自己的請求體寫入BufferedSink,這里也就是寫入到Socket里面了。

同理,讀取數(shù)據(jù)到Response則是使用BufferedSource,這里就不擴展開了。

參考資料

拆輪子系列:拆 Okio
官方Okio

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容