1. 程式人生 > >OKHttp 官方文件【二】

OKHttp 官方文件【二】

OkHttp 是這幾年比較流行的 Http 客戶端實現方案,其支援HTTP/2、支援同一Host 連線池複用、支援Http快取、支援自動重定向 等等,有太多的優點。 一直想找時間瞭解一下 OkHttp 的實現原理 和 具體原始碼實現,不過還是推薦在使用 和 瞭解其原理之前,先通讀一遍 OkHttp 的官方文件,由於官方文件為英文,我在通讀的時候,順便翻譯了一下,`如翻譯有誤,請幫忙指正`。 [OKHttp 官方文件【一】](https://xiaxl.blog.csdn.net/article/details/107702122) [OKHttp 官方文件【二】](https://xiaxl.blog.csdn.net/article/details/107729634) **OkHttp官方API地址:** [https://square.github.io/okhttp/](https://square.github.io/okhttp/) ## 六、HTTPS OkHttp 試圖平衡以下兩個矛盾的問題: + 連線到儘可能多的主機:這包括執行最新版本的boringssl的高階主機,以及執行舊版本OpenSSL的較過時的主機; + 連線的安全性:這包括使用證書對遠端web伺服器進行驗證,以及使用強密碼交換隱私資料; 當與HTTPS伺服器進行協商握手時,OkHttp 需要知道使用的哪一個 TLS 版本 和 加密套件。一個客戶端想要最大程度的連線,需要相容比較早的TLS版本 和 對應的較弱的密碼套件;一個客戶端想要最大程度的提高安全性,需要使用最新的TLS版本,並且只用安全級別最高的密碼套件; 特定的安全性與連線性策略由ConnectionSpec實現。OkHttp 包括四個內建的連線策略: + RESTRICTED_TLS 是一種安全的配置,旨在滿足更嚴格的安全性要求; + MODERN_TLS 是一個連線到當代流行HTTPS伺服器的安全配置; + COMPATIBLE_TLS 是一種安全配置,可連線到安全的HTTPS伺服器,但不相容當前流行的HTTPS伺服器版本。 + CLEARTEXT 是一種明文網路請求,不安全的網路配置,用於Http; 以上策略鬆散地遵循 Google Cloud Policies,OkHttp遵循以下策略: 預設情況下,OkHttp 嘗試建立一個MODERN_TLS 策略的連線, 但是,如果 MODERN_TLS 策略失敗,則可以通過 connectionSpecs 配置回退到 COMPATIBLE_TLS 連線。 ```java OkHttpClient client = new OkHttpClient.Builder() .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .build(); ``` 支援的`TLS版本`和`加密套件`會隨著 OkHttp 每一個release版本的釋出而有所改變。例如,OkHttp 2.2版本中為應對 POODLE 攻擊,我們停止了SSL 3.0的支援;OkHttp 2.3 版本中,我們停止了對 RC4 的支援。與你PC上安裝的瀏覽器軟體一樣,始終保持使用OkHttp的最新版本,是保證安全的最佳途徑。 你可以自定義 `TLS版本`和`加密套件`來構建自己的連線策略。 例如,此配置僅限於三個備受推崇的密碼套件。 缺點是執行版本需要為 Android 5.0+ 以及類似策略的 webserver。 ``` ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) .cipherSuites( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256) .build(); OkHttpClient client = new OkHttpClient.Builder() .connectionSpecs(Collections.singletonList(spec)) .build(); ``` ### 6.1、Debugging TLS Handshake Failures `TLS握手` 要求客戶端和伺服器共享一個通用的TLS版本和密碼套件,這取決於JVM版本、 Android版本、OkHttp版本以及webserver的配置。 如果沒有通用的密碼套件和TLS版本,您的呼叫將失敗,錯誤如下所示: ``` Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7f2719a89e80: Failure in SSL library, usually a protocol error error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:770 0x7f2728a53ea0:0x00000000) at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method) ``` 您可以使用`Qualys SSL Labs`檢查Web伺服器的配置,OkHttp的TLS配置歷史記錄在 `tls_configuration_history.md` 應用程式預期安裝在較早的Android裝置上,需要考慮到相容 Google Play Services’ ProviderInstaller。 這將提高使用者的安全性並增強與webservers的連線性。 ### 6.2、Certificate Pinning 預設情況下,OkHttp信任您的手機內建的所有TSL證書。此策略可最大程度地提高連線性,但會受到諸如 2011 DigiNotar 攻擊等證書頒發機構的攻擊。 這種策略假定您的HTTPS伺服器證書預設是由證書頒發機構簽名的。 ``` private final OkHttpClient 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)); } } } ``` ### 6.3、Customizing Trusted Certificates 以下完整的示例程式碼展示瞭如何用您自己的證書集替換主機平臺的證書頒發機構。 如上所述,如果沒有伺服器的TLS管理員的許可,請不要使用自定義證書! ``` private final OkHttpClient client; public CustomTrust() { X509TrustManager trustManager; SSLSocketFactory sslSocketFactory; try { trustManager = trustManagerForCertificates(trustedCertificatesInputStream()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { trustManager }, null); sslSocketFactory = sslContext.getSocketFactory(); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } client = new OkHttpClient.Builder() .sslSocketFactory(sslSocketFactory, trustManager) .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. } ``` ## 7、Interceptors 攔截器可以監聽、重寫、重試 網路請求,攔截器的作用非常強大。以下是一個簡單的攔截器,日誌列印網路請求`request資料`與`response資料`。 ``` 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; } } ``` 對 `chain.proceed(request)` 呼叫是每個攔截器的關鍵部分,這個看起來很簡單的方法是所有HTTP工作發生的地方,它生成一個響應來滿足請求。如果 `chain.proceed(request)` 被多次呼叫,之前的 response body 必須關閉。 攔截器可以組成執行鏈,假設你同時擁有一個`壓縮攔截器`和`一個校驗攔截器`,你需要決定資料是被壓縮然後校驗,還是校驗然後壓縮。OkHttp 將攔截器組成一個列表按順序執行。 ![Interceptors](https://upload-images.jianshu.io/upload_images/5969042-1a4075551ae21ab8?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ### 7.1、Application Interceptors 攔截器分為`應用程式`或`網路攔截器`,我們使用 `LoggingInterceptor` 來展示差異。 利用 `OkHttpClient.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`重定向到`https://publicobject.com/helloworld.txt`,OkHttp 自動執行該重定向。應用攔截器被執行一次,`response` 資料由 `chain.proceed()`返回,返回的response為重定向後的 response 資料。 ``` 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 ``` 我們可以看到URL被重定向為不同URL的表現為,API `response.request().url()`不同於`request.url()`,兩條不同的日誌,對應兩條不同的url。 ### 7.2、Network Interceptors 註冊一個`網路攔截器`與註冊`應用攔截器`非常相似,呼叫`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 ``` 網路請求中還包含其他引數,例如 `Accept-Encoding:gzip`header資料的新增,以支援 response 請求資料的壓縮。網路攔截器 擁有一個非空的連線,可以用於查詢IP地址 與 查詢伺服器的TLS配置 (`The network interceptor’s Chain has a non-null Connection that can be used to interrogate the IP address and TLS configuration that were used to connect to the webserver.`)。 ### 7.3、Choosing between application and network interceptors 每個攔截器都有各自的優點: **Application interceptors** + 無需關注類似`重定向`與`重試`之類的中間響應; + 只是呼叫一次,如果HTTP響應是從快取中獲取的(Are always invoked once, even if the HTTP response is served from the cache.) + 關注應用程式的最初意圖,不要關心一些注入Header,類似`If-None-Match`; + Permitted to short-circuit and not call Chain.proceed() (`不知道該怎麼翻譯,理解的小夥伴請留言`). + 允許重試,並多次呼叫`Chain.proceed()`. + 可以呼叫`withConnectTimeout、withReadTimeout、withWriteTimeout`表明請求超時; **Network Interceptors** + 能夠操作中間響應,如重定向和重試; + Not invoked for cached responses that short-circuit the network(`不知道該怎麼翻譯,理解的小夥伴請留言`). + 檢測網路傳輸的資料; + 對於攜帶網路請求的連線,是可通過的; #### 7.4、Rewriting Requests 攔截器可以新增、移除、替換`request headers`,攔截器還可以轉換 `request body`資料。例如:如果webserver伺服器支援 `request body`資料壓縮,攔截器可以新增壓縮相關欄位。 ``` /** This interceptor compresses the HTTP request body. Many webservers can't handle this! */ 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(); } }; } } ``` ### 7.5、Rewriting Responses 對比以上的`Rewriting Requests`,攔截器可重寫`response headers`和轉換`response body`。通常來說,`Rewriting Responses`相比`Rewriting Requests`來說是比較危險的,因為這可能違反webserver的預期。 如果你遇到某種棘手的情況下並準備好解決這個問題,則重寫`response headers`是解決問題的有效辦法。 例如,您可以修復伺服器的錯誤配置`Cache-Control`響應Header,以更好實現的響應資料快取: ``` /** Dangerous interceptor that rewrites the server's cache-control header. */ 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(); } }; ``` 通常這種方法是有效地,用來修復webserver的錯誤! ## 八、Recipes 我們編寫了一些示例程式碼,展示如何解決OkHttp的常見問題。 通讀它們以瞭解一切如何協同工作。 隨意剪下並貼上這些示例,這就是他們存在的目的。 ### 8.1、Synchronous Get Download a file, print its headers, and print its response body as a string. 下載一個檔案,列印它的`header`資料,並將 `response body`資料列印為一個字串。 對於小型檔案,用`string()` 方法展示`response body`資料簡單又方便,但是如果`response body`資料大於1M,避免使用`string()` 方法,這種方法會將響應資料讀進記憶體。大檔案的情況最好將`response body`作為資料流進行處理。 ``` private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (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()); } } ``` ### 8.2、Asynchronous Get 在工作執行緒中下載一個檔案,並在響應可讀時被回撥。回撥是在`response headers`準備好之後進行的,此時讀取`response body`可能仍會引起阻塞。OkHttp目前沒有提供非同步API來部分接收響應體。 ``` 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 { 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()); } } }); } ``` ### 8.3、Accessing Headers 通常`HTTP headers` 的工作方式類