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常用的場景已經介紹完了,從例子中我們可以學到很多的東西,下一篇開始原始碼閱讀分析。