STEP BY STEP↑↑ - Okio & OkHttp

F9856cc7a15ed2cb9e6ebfab41fdf1cf?s=47 Shohei Kawano
February 17, 2018

STEP BY STEP↑↑ - Okio & OkHttp

At DroidKaigi Reject Conference, I have talked about Okio, specifically the internal implementation of Okio, with @stsn_jp talking about the internal implementation of OkHttp. To describe it (maybe too) simply, Okio is a library for I/O and OkHttp is a library for HTTP+HTTP/2 client.

Both libraries are for Java and Android and applications, and both are the ones of the most famous libraries, especially in Android development.

* square/okio: A modern I/O API for Java
https://github.com/square/okio

* square/okhttp: An HTTP+HTTP/2 client for Android and Java applications.
https://github.com/square/okhttp

* Droidcon Montreal Jake Wharton - A Few Ok Libraries - YouTube
https://www.youtube.com/watch?v=WvyScM_S88c

* DroidKaigi 2018 Reject Conference - connpass
https://connpass.com/event/73993/

F9856cc7a15ed2cb9e6ebfab41fdf1cf?s=128

Shohei Kawano

February 17, 2018
Tweet

Transcript

  1. STEP BY STEP↑↑ - Okio & OkHttp Shun Sato@stsn_jp Shohei

    Kawano@shaunkawano
  2. 本日話すこと(ニッチかも) • Okio, OkHttpの内部実装周り ◦ Okio ▪ 背景 ▪ バッファリング周り

    ▪ タイムアウト周り ◦ OkHttp ▪ Interceptor周り ▪ Cache周り
  3. Okio

  4. JavaでのI/O処理つらい (例) InputStream

  5. private static final int MAX_SKIP_BUFFER_SIZE = 2048; public abstract int

    read() throws IOException public int read(byte b[]) throws IOException public int read(byte b[], int off, int len) throws IOException public long skip(long n) throws IOException public int available() throws IOException public void close() throws IOException public synchronized void mark(int readlimit) public synchronized void reset() throws IOException public boolean markSupported() InputStream.java
  6. APIの使い勝手 ▪ 用意されているAPIの中に、バッファ処理に関するものまで入っている (本来、 BufferedInputStream内に用意されているべき APIなど) ▪ 複数のread メソッド。独自拡張する場合には両方 overrideして処理を書く必要がある

    ▪ byte[]を渡す、どれくらい読み込んで、末尾までどれくらいか、自前で whileで都度チェック パフォーマンスへの意識 ▪ 渡したbyte[]が読み込んでいるデータ量より小さい場合は別途 byte[]を作成 ▪ 必要に応じてbyte[]のコピー処理 ▪ 不要になったbyte[]に対してGCが走ってしまう? InputStream
  7. Okio

  8. public interface Source extends Closeable { long read(Buffer sink, long

    byteCount) throws IOException; Timeout timeout(); @Override void close() throws IOException; } Source.java(InputStreamの補完)
  9. Okio 概要 Okio.sink() Okio.source() といったstaticメソッドがある: • Source: InputStreamの補完 - Source

    of Bytes 読み込み先を指定 • Sink: OutputStreamの補完 - Sink for Bytes 書き込み先を指定 一般的なアプリで利用する場合にはBufferを利用する: • Okio.buffer(Source source) => return BufferedSource • Okio.buffer(Sink sink) => return BufferedSink
  10. Okio 概要 Okio.sink() Okio.source() といったstaticメソッドがある: • Source: InputStreamの補完 - Source

    of Bytes 読み込み先を指定 • Sink: OutputStreamの補完 - Sink for Bytes 書き込み先を指定 一般的なアプリで利用する場合にはBufferを利用する: • Okio.buffer(Source source) => return BufferedSource • Okio.buffer(Sink sink) => return BufferedSink
  11. public interface Source extends Closeable { long read(Buffer sink, long

    byteCount) throws IOException; Timeout timeout(); @Override void close() throws IOException; } Source.java
  12. Okio: Buffer & Segment

  13. Buffer • メモリ上のバイトの集まり(=バッファ)を表すクラス • BufferedSource, BufferedSink両方を実装(データの読み書きを行なう) • 読み書きしたデータをSegmentクラス(Bufferの一部)のbyte[] data内に保存 Segment

    • バッファの断片を表すクラス • byte[] dataを保持している(MAX: 8KB) • 自分の前(prev)のSegment, 次(next)のSegmentを知っている • SegmentPoolによって生成・最大限プールされる
  14. Okio: SegmentPool

  15. SegmentPool • Segmentの生成と、使われていないSegmentをプールするクラス • MAX 64KBまでのSegmentをプールする • プールにSegmentがない時と、書き込み先のSegmentに必要なデータ容量がない 場合にSegmentを新しく生成する •

    (他にもタイミングはありますが、大きくはこの流れ)
  16. None
  17. DroidKaigiRejectConferenceDroidKaigiRejectConferenceDr oidKaigiRejectConferenceDroidKaigiRejectConferenceDroi dKaigiRejectConferenceDroidKaigiRejectConferenceDroidK aigiRejectConferenceDroidKaigiRejectConferenceDroidKai giRejectConferenceDroidKaigiRejectConferenceDroidKaigi RejectConferenceDroidKaigiRejectConferenceDroidKaigiRe jectconConferenceDroidKaigiRejectConferenceDroidKaigiR ectconConferenceDroidKaigiRejectConferenceDroidKaigi…..

  18. )); Okio.buffer(Okio.Source(

  19. Buffer RealBufferedSource Source

  20. BufferedSource#readUtf8(); ↓ DroidKaigiRejectConferenceDroidKaigi RejectConferenceDroidKaigiRejectConf erenceDroidKaigiRejectConferenceDroi dKaigiRejectConferenceDroidKaigiR….

  21. BufferedSource#readUtf8();

  22. BufferedSource#readUtf8(); SegmentPool

  23. SegmentPool.take(); BufferedSource#readUtf8();

  24. BufferedSource#readUtf8(); Segment

  25. BufferedSource#readUtf8();

  26. BufferedSource#readUtf8();

  27. “DroidKaigiRejectConferenceDrodKai…. BufferedSource#readUtf8();

  28. BufferedSource#readUtf8();

  29. BufferedSource#readUtf8();

  30. BufferedSource#readUtf8();

  31. BufferedSource#readUtf8();

  32. “DroidKaigiRejectConferenceDrodKai…. BufferedSource#readUtf8();

  33. “DroidKaigiRejectConferenceDrodKai…. BufferedSource#readUtf8();

  34. “DroidKaigiRejectConferenceDrodKai…. BufferedSource#readUtf8();

  35. BufferedSource#readUtf8();

  36. BufferedSource#readUtf8();

  37. Buffer#readUtf8(); BufferedSource#readUtf8();

  38. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai….

  39. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai….

  40. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. SegmentPool.recycle(segment);

  41. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai….

  42. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. ...

  43. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. … ...

  44. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. … … ...

  45. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. … … … ...

  46. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. … … … … ...

  47. BufferedSource#readUtf8(); “DroidKaigiRejectConferenceDrodKai…. … … … … ...

  48. DroidKaigiRejectConferenceDroidKaigiRejectConferenceDr oidKaigiRejectConferenceDroidKaigiRejectConferenceDroi dKaigiRejectConferenceDroidKaigiRejectConferenceDroidK aigiRejectConferenceDroidKaigiRejectConferenceDroidKai giRejectConferenceDroidKaigiRejectConferenceDroidKaigi RejectConferenceDroidKaigiRejectConferenceDroidKaigiRe jectconConferenceDroidKaigiRejectConferenceDroidKaigiR ectconConferenceDroidKaigiRejectConferenceDroidKaigi…..

  49. None
  50. None
  51. None
  52. SegmentPool.take();

  53. None
  54. ✨Segment from the pool is reused.✨

  55. Okio: Inside OkHttp

  56. Okio inside OkHttp • OkHttpはネットワーク通信のためのライブラリ • 受け取ったレスポンスをSourceとして取り込み処理を行なう • タイムアウト等の処理ももちろん行なう Sourceからデータを読み取る・Sinkにデータを書き込む時、タイムアウトを実現したい。

    タイムアウトしたら、SourceやSinkをcloseする処理
  57. Okio: Timeout

  58. public static Source source(Socket socket) throws IOException { if (socket

    == null) throw new IllegalArgumentException("socket == null"); AsyncTimeout timeout = timeout(socket); Source source = source(socket.getInputStream(), timeout); return timeout.source(source); } Okio.source(Socket socket)
  59. Set Timeout to OkHttpClient

  60. Example: How Timeout is set? - OkHttpClient.Builder @Provides fun provideOkHttpClientBuilder(cache:

    Cache): OkHttpClient.Builder = OkHttpClient.Builder().cache(cache) .connectTimeout(10L, TimeUnit.SECONDS) .writeTimeout(10L, TimeUnit.SECONDS) .readTimeout(30L, TimeUnit.SECONDS)
  61. Create Raw Call

  62. Example: How Timeout is set? - OkHttpCall private okhttp3.Call createRawCall()

    throws IOException { Request request = serviceMethod.toRequest(args); okhttp3.Call call = serviceMethod.callFactory.newCall(request); if (call == null) { throw new NullPointerException("Call.Factory returned null."); } return call; }
  63. Example: How Timeout is set? - OkHttpCall private okhttp3.Call createRawCall()

    throws IOException { Request request = serviceMethod.toRequest(args); okhttp3.Call call = serviceMethod.callFactory.newCall(request); if (call == null) { throw new NullPointerException("Call.Factory returned null."); } return call; }
  64. Example: How Timeout is set? - OkHttpClient @Override public Call

    newCall(Request request) { return RealCall.newRealCall(this, request, false /* for web socket */); }
  65. Example: How Timeout is set? - OkHttpClient @Override public Call

    newCall(Request request) { return RealCall.newRealCall(this, request, false /* for web socket */); } static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { // Safely publish the Call instance to the EventListener. RealCall call = new RealCall(client, originalRequest, forWebSocket); call.eventListener = client.eventListenerFactory().create(call); return call; }
  66. Example: How Timeout is set? - OkHttpClient @Override public Call

    newCall(Request request) { return RealCall.newRealCall(this, request, false /* for web socket */); } static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { // Safely publish the Call instance to the EventListener. RealCall call = new RealCall(client, originalRequest, forWebSocket); call.eventListener = client.eventListenerFactory().create(call); return call; }
  67. Example: How Timeout is set? - RealCall -> Chain final

    class RealCall implements Call { final OkHttpClient client; … Response getResponseWithInterceptorChain() throws IOException { Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); return chain.proceed(originalRequest); } }
  68. Example: How Timeout is set? - RealConnection public HttpCodec newCodec(OkHttpClient

    client, Interceptor.Chain chain, StreamAllocation streamAllocation) throws SocketException { if (http2Connection != null) { return new Http2Codec(client, chain, streamAllocation, http2Connection); } else { socket.setSoTimeout(chain.readTimeoutMillis()); source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS); sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS); return new Http1Codec(client, streamAllocation, source, sink); } }
  69. Example: How Timeout is set? - RealConnection public HttpCodec newCodec(OkHttpClient

    client, Interceptor.Chain chain, StreamAllocation streamAllocation) throws SocketException { if (http2Connection != null) { return new Http2Codec(client, chain, streamAllocation, http2Connection); } else { socket.setSoTimeout(chain.readTimeoutMillis()); source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS); sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS); return new Http1Codec(client, streamAllocation, source, sink); } }
  70. Example: How Timeout is set? - RealConnection private void connectSocket(int

    connectTimeout, int readTimeout, Call call, EventListener eventListener) throws IOException { … try { source = Okio.buffer(Okio.source(rawSocket)); sink = Okio.buffer(Okio.sink(rawSocket)); } catch (NullPointerException npe) { if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) { throw new IOException(npe); } }
  71. public static Source source(Socket socket) throws IOException { if (socket

    == null) throw new IllegalArgumentException("socket == null"); AsyncTimeout timeout = timeout(socket); Source source = source(socket.getInputStream(), timeout); return timeout.source(source); } Okio.source(Socket socket)
  72. public static Source source(Socket socket) throws IOException { if (socket

    == null) throw new IllegalArgumentException("socket == null"); AsyncTimeout timeout = timeout(socket); Source source = source(socket.getInputStream(), timeout); return timeout.source(source); } Okio.source(Socket socket)
  73. AsyncTimeout

  74. • バックグランドスレッドを利用してタイムアウト検知・必要に応じた処理を行なうため のクラス • enter()でタイムアウトしうる処理を開始、exit()で処理の終了(中断もありえる) → exit がtrueを返す=タイムアウト • WatchdogというThreadを継承したstaticクラスによってタイムアウトが発生したかど

    うかを判定・監視している AsyncTimeout
  75. public static Source source(Socket socket) throws IOException { if (socket

    == null) throw new IllegalArgumentException("socket == null"); AsyncTimeout timeout = timeout(socket); Source source = source(socket.getInputStream(), timeout); return timeout.source(source); } Okio.source(Socket socket)
  76. public final Source source(final Source source) { return new Source()

    { @Override public long read(Buffer sink, long byteCount) throws IOException { boolean throwOnTimeout = false; enter(); try { long result = source.read(sink, byteCount); throwOnTimeout = true; return result; } catch (IOException e) { throw exit(e); } finally { exit(throwOnTimeout); } } AsyncTimeout.source
  77. public final Source source(final Source source) { return new Source()

    { @Override public long read(Buffer sink, long byteCount) throws IOException { boolean throwOnTimeout = false; enter(); try { long result = source.read(sink, byteCount); throwOnTimeout = true; return result; } catch (IOException e) { throw exit(e); } finally { exit(throwOnTimeout); } } AsyncTimeout.source
  78. public final Source source(final Source source) { return new Source()

    { @Override public long read(Buffer sink, long byteCount) throws IOException { boolean throwOnTimeout = false; enter(); try { long result = source.read(sink, byteCount); throwOnTimeout = true; return result; } catch (IOException e) { throw exit(e); } finally { exit(throwOnTimeout); } } AsyncTimeout.source
  79. AsyncTimeout

  80. static @Nullable AsyncTimeout head;

  81. static @Nullable AsyncTimeout head; private @Nullable AsyncTimeout next;

  82. static @Nullable AsyncTimeout head; private @Nullable AsyncTimeout next;

  83. None
  84. enter()

  85. WatchDog Thread

  86. WatchDog Thread

  87. WatchDog Thread

  88. WatchDog Thread exit();

  89. WatchDog Thread exit();

  90. WatchDog Thread exit();

  91. WatchDog Thread ✅ No more task for timeout!

  92. WatchDog Thread wait() for timeout time

  93. WatchDog Thread ❌ Nothing changes! Then Timeout!

  94. OkHttp

  95. OkHttp 概要 http://square.github.io/okhttp/ 効率的にHTTPなどのネットワーク通信をする - Connection Poolを使い、リクエストのレイテンシを減らす - GZIPなどの圧縮を透過的に行うことが出来る -

    Responseをキャッシュし、繰り返しのRequest時にネットワーク通信を避けることが 出来る InterceptorとCacheに絞って説明します。
  96. Interceptorの概要/仕組み

  97. RequestからResponseを取得する時 に、間に処理を差し込むための機能 例 Loggingをする User-Agentヘッダーを付ける Interceptor?

  98. Interceptorがどのように実装されて いるか?

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

    interface Chain { Response proceed(Request request) throws IOException; ... }
  100. Chain? Interceptor間のつなぎ込みをするクラス chain0(interceptor0, chain1) chain1(interceptor1, chain2) chain2… … chain2… chain1(interceptor1,

    chain2) chain0(interceptor0, chain1) 各chainは次へのchainを持っているので、chainが連鎖していく
  101. Chain? chain0(interceptor0, chain1) chain1(interceptor1, chain2) chain2… … chain2… chain1(interceptor1, chain2)

    chain0(interceptor0, chain1)
  102. /** Chain.proceedメソッド内のコード */ // Call the next interceptor in the

    chain. RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next);
  103. /** 一般的なInterceptor.interceptメソッド内のコード */ @Override public Response intercept(Chain next) { ...

    Response response = next.proceed(request); ... return response; }
  104. Cacheをどのように行うか

  105. 以前までのResponseを保存し、同じURLのときにshort-cutとして使用する CacheInterceptorクラスが存在し、このInterceptorでCache Responseを早期に返す か、それともNetworkリクエストをするかを決定する CacheInterceptorの概要 - 取得したResponseをファイルに書き込む - Requestに対応したCache Responseの読み込み

    Cache?
  106. Responseをファイルに書き込む デフォルトではファイルにLRUアルゴリズムで書き込む(DiskLruCacheクラス) - Least Recently Used: 更新が古いものから削除(evict)する 同期的にファイルに書き込んでいては時間が掛かるので適切なタイミングでファイルに 書き込むことでパフォーマンスを向上させている ただ、タイミングによってはファイルへの書き込みが終わっていないので状態を保持する

    必要がある - DIRTY: まだ完全に書き込みが終わっていない状態 - CLEAN: header, bodyが全て書き終わった状態 この状態をJournalファイルで管理している
  107. 適切なタイミング? ResponseBodyの段階ではまだSource(InputStream)なので読み込みが完了していな い。byte[]などに変換されていない状態 これを同期的に読みこんでしまうと処理に時間がかかってしまう。上手いことやりたい 具体的にいうとユーザがResponseBodyが必要になったと同時にファイルへの書き込 みを行いたい

  108. Journalファイル? Responseの書き込みが完全に完了しているのか、キャッシュが削除されたなどを管理 するファイル。indexファイルをイメージすると良い 下の場合、f418cc9de6ab23b30744a5771f22154cがCLEANなので、完全に書き込み が完了している状態になる 再起動などでメモリがリフレッシュされても、このJournalファイルがあれば、どのcacheを 持っているかを再構築できる DIRTY f418cc9de6ab23b30744a5771f22154c CLEAN

    f418cc9de6ab23b30744a5771f22154c 5457 2305 READ f418cc9de6ab23b30744a5771f22154c DIRTY f418cc9de6ab23b30744a5771f22154c CLEAN f418cc9de6ab23b30744a5771f22154c 5465 2305
  109. DiskLruCacheクラス Journal in memory

  110. put レスポンス a put Journal in memory

  111. put レスポンス a put Journal DIRTY a in memory DIRTY

    a
  112. put レスポンス a put Journal DIRTY a in memory DIRTY

    a headers a hoge: fuga
  113. put レスポンス a put headers a hoge: fuga body a

    Journal DIRTY a in memory DIRTY a
  114. put レスポンス a オリジナルレ スポンス body a body a put

    Journal DIRTY a in memory DIRTY a headers a hoge: fuga
  115. put レスポンス a put Journal DIRTY a in memory DIRTY

    a オリジナルレ スポンス body a body a headers a hoge: fuga
  116. Source cacheWritingSource = new Source() { boolean cacheRequestClosed; @Override public

    long read(Buffer sink, long byteCount) { long bytesRead; try { bytesRead = source.read(sink, byteCount); // sourceはResponseBodyのstream } catch (IOException e) { // error handling throw e; } if (bytesRead == -1) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheBody.close(); } return -1; } sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead); // ここでcacheに結果を読み込んだ結果をコピーする cacheBody.emitCompleteSegments(); return bytesRead; }
  117. put レスポンス a put source.read() Journal DIRTY a in memory

    DIRTY a headers a hoge: fuga body a オリジナルレ スポンス body a
  118. put body a Hello world レスポンス a source.read() 結果を返す put

    ついでに書き 込みも行う Journal DIRTY a in memory DIRTY a headers a hoge: fuga オリジナルレ スポンス body a
  119. put body a Hello world レスポンス a オリジナルレ スポンス body

    a source.read() 結果を返す put Journal DIRTY a CLEAN a in memory CLEAN a 完了通知 headers a hoge: fuga
  120. Journal DIRTY a CLEAN a in memory CLEAN a body

    a Hello world headers a hoge: fuga
  121. 再起動時

  122. Journal DIRTY a CLEAN a ... in memory body a

    Hello world headers a hoge: fuga
  123. Journal DIRTY a CLEAN a ... in memory CLEAN a

    body a Hello world headers a hoge: fuga ファイルからin memoryに読み込み
  124. Journal DIRTY a CLEAN a ... in memory CLEAN a

    body a Hello world headers a hoge: fuga
  125. Candidate Responseの取得 URL、Vary Headerに対応したResponseを先程のCLEANな状態のCacheから取得する Vary Header? - Vary: User-Agent のようにheader-nameを指定する

    指定したheader-nameがmatchするかどうかでCacheを取得するかどうかを決定す る
  126. Journal DIRTY a CLEAN a in memory CLEAN a body

    a Hello world headers a hoge: fuga リクエスト a get key (url, vary) Candidate Response
  127. CacheStrategyでCandidateの妥当性を確認する 上記で取得したのはあくまでCandidate Response Cache-Controlなどを見て有効性を判定する これらのHeaderをもとに、本当にこのResponse Cacheが妥当かどうかを確認する。合 わせて304(Not Modified)のためのheaderが含まれるかも確認している

  128. long now = System.currentTimeMillis(); CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(),

    cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse;
  129. CacheControl requestCaching = request.cacheControl(); if (requestCaching.noCache() || hasConditions(request)) { return

    new CacheStrategy(request, null); } CacheControl responseCaching = cacheResponse.cacheControl(); if (responseCaching.immutable()) { return new CacheStrategy(null, cacheResponse); } long ageMillis = cacheResponseAge(); long freshMillis = computeFreshnessLifetime(); if (requestCaching.maxAgeSeconds() != -1) { freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); } ...
  130. String conditionName; String conditionValue; if (etag != null) { conditionName

    = "If-None-Match"; conditionValue = etag; } else if (lastModified != null) { conditionName = "If-Modified-Since"; conditionValue = lastModifiedString; } else if (servedDate != null) { conditionName = "If-Modified-Since"; conditionValue = servedDateString; } else { return new CacheStrategy(request, null); // No condition! Make a regular request. }
  131. Journal DIRTY a CLEAN a in memory CLEAN a body

    a Hello world headers a hoge: fuga リクエスト get key (url, method, vary) Candidate Response Candidate Responseが妥当かど うかをCacheStrategyから判定
  132. Journal DIRTY a CLEAN a in memory CLEAN a body

    a Hello world headers a hoge: fuga リクエスト get key (url, method, vary) Candidate Response Candidate Responseが妥当かど うかをCacheStrategyから判定 結果から、Network通信をするか、 Cache Responseを返すか決定す る Cacheが有効ならここから Responseを読み込む
  133. Thank you!!!!! Happy Okio!! Happy OkHttp!!