Android 开源库分析——OkHttp3

本文链接:https://rainmonth.github.io/posts/A210402.html

  • 分析版本,OkHttp-3.12.10
  • 分析环境,IntellJIdeal

通过本文,可以了解如下内容:

  • OkHttp的基本使用;
  • OkHttp三次握手实现;
  • OkHttp的拦截器实现;
  • OkHttp的缓存实现;
  • OkHttp如何实现WebSocket;
  • 了解ArrayDeque,因为OkHttp中大量的使用ArrayDeque

基本使用

get请求

同步get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void getSync(String url) {
System.out.println("同步请求");
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url(url)
.build();
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
if (response.isSuccessful()) {
System.out.println(response.body().toString());
}
} catch (IOException e) {
e.printStackTrace();
}

}

异步get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void get(String url) {
System.out.println("异步请求");
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
final Request request = new Request.Builder()
.url(url)
.get()
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println(response.body().toString());
}
}
});
}

post请求

介绍post请求之前先了解下MediaType

MediaType指的是媒体类型,表示表示的时浏览器将以什么形式、什么编码对资源进行解析。主要包含三个信息:typesubTypecharset,MediaType常见类型如下:

  • text/(html,HTML文本格式;plain,纯文本格式;xml,xml格式;x-markdown,Markdown格式;)
  • image/(gif,gif图片;jpeg,jpg图片;png,png图片;)
  • application/(json,json格式数据;x-www-form-urlencode,键值对形式编码的参数;xhtml+xml,xhtml文本格式;xml,xml格式;pdf,pdf格式;msword,word格式;octet-stream,二进制数据(常用用于下载))
  • multipart/(form-data,不对字符编码发送大量二进制数据或包含non-ASCII字符的文本)

上面每种格式的前半部分表示type(“/”前的部分),后半部分表示subtype(“/”后的部分);

发送表单

1
2
3
4
5
6
7
8
9
10
11
12
13
 public void postForm(String url) throws IOException {
System.out.println("同步postForm");
OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();
RequestBody requestBody = new FormBody.Builder()
.add("user", "randy")
.add("pwd", "xxx")
.build();
final Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
}

发送json

1
2
3
4
5
6
7
8
9
10
11
public void postJson(String url) throws IOException {
System.out.println("同步postJson");
String json = "{\"user\":\"Randy\",\"nick\":\"荏苒追寻\"}";
OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), json);
final Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
}

发送Multipart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void postMultiPart(String url) throws IOException {
System.out.println("同步 postMultiPart");
OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();
RequestBody requestBody = new MultipartBody.Builder()
.addFormDataPart("user", "randy")
.addFormDataPart("nick", "xxx")
.addFormDataPart("file", "file.txt",
RequestBody.create(MediaType.parse("application/octet-stream"), new File("filePath")))
.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
}

建立WebSocket连接

OkHttpClient 可以直接发送WebSocket连接请求,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* @param url webSocket 链接地址
*/
public WebSocket webSocket(String url) {
Request request = new Request.Builder()
.url(url)
.get()
.build();

OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();

return okHttpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
}

@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
}

@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
}

@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
}

@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
}

@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
}
});
}

具体的OkHttpClent WebSocket 说明,参考文章:OkHttp3实现WebSocket连接

基本使用小结

上面就是使用OkHttp进行网络请求的常用场景,总结下来就是:

  1. 获取OkHttpClient实例(一般是单例模式);
  2. 构建Request,post请求需要先构建RequestBody
  3. 调用OkHttpClient的newCall(Request request)方法获取Call
  4. 获取Call对象后,
    1. 同步请求调用Call.execute()获取Response对象,并处理请求结果;
    2. 异步请求调用Call.enqueue(Callback callback),并在回调中处理结果;

拦截器

拦截器是OkHttpClient设计的精髓所在,它采用采用责任链模式。责任链代码具体体现在如下地方:

1
2
3
4
5
6
7
8
9
10
11
// RealCall.java
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList();
// 添加一系列的拦截器
...
// 构造责任链
Chain chain = new RealInterceptorChain(interceptors, (StreamAllocation)null, (HttpCodec)null, (RealConnection)null, 0, this.originalRequest, this, this.eventListener, this.client.connectTimeoutMillis(), this.client.readTimeoutMillis(), this.client.writeTimeoutMillis());
// 开始责任链处理
Response response = chain.proceed(this.originalRequest);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// RealInterceptorChain.java
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
// 一系列的检查,不合条件抛出异常
...

// 重新构建责任链
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
// 在intercept(next)调用后责任链开始执行
Response response = interceptor.intercept(next);
// 一系列的检查,不合条件抛出异常
...

return response;
}

责任的传递是通过调用获取到的拦截器的intercept(Chain chain)方法,并在方法内部调用Chain.proceed(Request request)方法来实现的,最后一个拦截器CallServerInterceptorintercept(Chain chain)方法中没有调用proceed(Request request)方法,说明任务到这个拦截器的处理工作完毕了。

拦截器的种类

要了解拦截器的种类,还是得看RealCallgetResponseWithInterceptorChain()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Response getResponseWithInterceptorChain() throw IOException {
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));

...
return response
}

总结下责任链中各个节点拦截器的作用,如下:

拦截器 作用
应用拦截器 拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor 处理错误重试和重定向
BridgeInterceptor 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor 缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
networkInterceptors(网络拦截器) 用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor 请求拦截器,在前置准备工作完成后,真正发起了网络请求。

RetryAndFollowUpInterceptor

处理错误和重定向。核心方法是followUpRequest(Response userResponse, Route route),该方法会根据返回的responseCode来进行相应的逻辑处理,最终得到相应的Request对象。这里主要看那些导致重试和重定向的responseCode:

  • 重试,HTTP_CLIENT_TIMEOUTHTTP_UNAVAILABLE(重试一次);
  • 重定向,HTTP_MULT_CHOICEHTTP_MOVED_PERMHTTP_MOVED_TEMPHTTP_SEE_OTHER,会进行重定向的尝试;

还有一个比较核心的点就是,该拦截器的intercept方法中,完成了StreamAllocation对象的创建,并将其值传递到了RealInterceptorChain中。

BridgeInterceptor

说明见上面的表格,需要注意的是,如果我们没有指定Accept-Encoding: gzip,OkHttpClient在Response的header中存在Accept-Encoding: gzip的情况下会默认帮我们处理Gzip压缩。但是如果我们制定了Accept-Encoding: gzip,则需要我们自己进行Response的Gzip压缩处理了。

CacheInterceptor

为什么说命中缓存就不会发起网络请求呢?看看CacheInterceptor的Intercept(Chain chain)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 命中了缓存,直接return了,此时没有执行责任链,即这次网络请求的任务已经完成了。所以此时不会再执行网络请求了,因为负责建立连接ConnectInterceptor和真正执行网络请求CallServerInterceptor的拦截器都没有执行。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}

Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}

OkHttpClient 的缓存设计算法采用的是Lru,基于Okio实现(相当于基于依据Http的缓存设计思路,用Okio进行了实现)。关于OkHttpClient的缓存,打算单独分析。

ConnectInterceptor

拦截器作用于职责:内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。整个ConnectInterceptor的代码还是比较简洁的,但是主要的处理都封装到了StreamAllocation这个类中了。StreamAllocation
建立连接的流程

  1. StreamAllocationnewStream(...)

  2. StreamAllocationfindHealthyConnection(...),找到RealConnection

  3. findHealthyConnection(...)的while(true)循环中调用findConnection(...),直到找到了一个健康的、可用的RealConnection才退出循环;

    1. StreamAllocation本身可以标记找到的RealConnection,方便下次查找;
    2. 先通过当前请求地址,从ConnectionPool中找;
    3. 通过全局路由路径,从ConnectionPool中找;
    4. 都找不到,就要新建一个连接,完成TCP+TLS了()
  4. 调用RealConnection的connect(…)方法,里面会根据情况调用connectTunnelconnectSocket,这个过程是阻塞的(一个while(true)循环,但最终都是调用connectSocket,方法内部会调用不同平台的connectSocket方法,创建成功后,将创建出来的RealConnection放入ConnectionPool。而三次握手也就发生在这个建立socket连接的过程中了,因此OkHttp的三次握手实现时依赖于socket的。

    了解三次握手

ConnectionPool内部有一个线程池,用来定期清理尝时间不用的RealConnection(毕竟RealConnection是系统资源)

CallServerInterceptor

真正发起网络请求的地方

问题

  1. addInterceptor(Interceptor interceptor)addNetworkInterceptor(Interceptor interceptor)区别?

    1. 目的:前者是添加应用拦截器(此时请求还未真正发起,是一些与处理过程),后者是添加网络拦截器,它位于ConnectInterceptorCallServerIntercptor之间(此时网络链路已经准备好,只带请求发送了)
    2. 执行时机:前者最先执行,后者在链接建立之后,请求发送之前;
    3. 调用:应用拦截器和网络拦截器的proceed(Chain chain)
  2. OkHttpClient 的Dispatcher是如何管理三个请求的队列的?
    首先这三个队列分别是:正在运行的同步双端队列(runningSyncCalls)、准备就绪的异步双端队列(readyAsyncCalls)、正在运行的异步双端队列(runningAsyncCalls)

client.dispatcher().executed(this)干了什么呢?

1
2
3
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}

将RealCall对象加入到正在运行的同步队列中,这样在getResponseWithInterceptorChain()调用结束得到Response对象后,最终运行finally块时,调用client.dispatcher().finished(this);,那么看看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void finished(RealCall call) {
finished(runningSyncCalls, call);
}

private <T> void finished(Deque<T> calls, T call) {
Runnable idleCallback;
synchronized (this) {
// 移除之前加入的同步请求
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}

// 尝试在readyAsynCall队列中获取待运行的任务,然后加入到runningAsynCall队列中运行
boolean isRunning = promoteAndExecute();

if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
/**
* 从就绪的异步任务中按规则取出一定数量异步任务,加入到可运行的异步任务队列中,然后将运行中的异步任务丢该线程池运行
* 这里的获取规则如下:
* 1. 当前运行的异步任务书超过最大请求数时,直接break;
* 2. 当前的异步任务超过美国host的最大限制是,直接跳过;
* 这就说明了一个问题:随缘OkHttpClient采用的Dispatcher中的线程池核心线程为0,工作线程为Integer.MAX_VALUE,但通过 maxRequests (默认值为64)进行了线程数量的控制,不至于因线程过多导致
* OOM的发生
*/
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));

List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();

if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.

i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}

for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}

return isRunning;`
}
  1. getResponseWithInterceptorChain()如何实现递归调用的?
    先看getResponseWithInterceptorChain()代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
    }
    // 注意:这个interceptors的列表中最后都会添加一个 CallServerInterceptor ,这个 CallServerInterceptor 的Intercept方法中是不调用 Chain.proceed(Request request)方法的。
    // 递归就是通过不停的调用Chain.process(Chain chain)方法的。相当于CallServerinterceptor是递归的终止条件。
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
    originalRequest, this, eventListener, client.connectTimeoutMillis(),
    client.readTimeoutMillis(), client.writeTimeoutMillis());

    Response response = chain.proceed(originalRequest);
    if (retryAndFollowUpInterceptor.isCanceled()) {
    closeQuietly(response);
    throw new IOException("Canceled");
    }
    return response;
    }

    所以递归是不断的调用RealInterceptorChainproceed(Request request)方法的,而递归终止的条件就是最终会走到CallServerInterceptor的intercept(Chain chain)方法,而该方法不会再调用Chain.proceed(Request request)

总结

至此OkHttpClient的基本内容就介绍完毕了,OkHttpClient十分强大强大,

  • 从设计模式角度,如对Http1和Http2的支持(采用的是策略模式),拦截器的处理(采用的是责任链模式),各种对象的创建如OkHttpClient、Request、Response(采用的Builder模式)
  • 从同步处理的角度,ConnectionPool中的同步处理、三个请求队列的操作处理;
  • 从线程池的角度,有Dispatcher(负责请求的分发调度)、ConnectionPool中的线程池(负责定期按规则清理不需要连接对象)
  • 从缓存设计的角度,采用Okio将http的缓存策略实现,并通过LRU算法(DiskLruCache)来维护缓存的更新。