版权声明:本文为博主原创文章,转载请注明出处。
一、前言
对于开发者来说优秀的源码是最好的学习资源。通过阅读优质源码就相当于和大牛有一次对话。
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的源码的使用场景在他的源码中有体现,就是上图中的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常用的场景已经介绍完了,从例子中我们可以学到很多的东西,下一篇开始源码阅读分析。
推荐:
欢迎关注我创建的Android TV 简书专题,会定期给大家分享一些AndroidTv相关的内容:
http://www.jianshu.com/c/37efc6e9799b
网友评论