最近在整理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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。