面试罐头(一)——okhttp内部总结分析
本文意图是针对面试,理清内部流程关系,而非细抠代码,汇总他人的文字而来,原地址 youyuge.cn
1.概述
okhttp3总体流程图:
先来回顾一下代码的使用流程,然后跟着流程一步步来分析:
1.1 创建 OkHttpClient
对象
OkHttpClient client = new OkHttpClient();
其实okHttpClient
用的也是builder
构建者模式:
public OkHttpClient() {
this(new Builder());
}
实际上,OkHttpClient.Builder
类成员很多,用来设置连接的各种参数,官方建议使用单例模式构建一个client。 直接new只是使用了默认配置:
public Builder() {
dispatcher = new Dispatcher();
protocols = DEFAULT_PROTOCOLS;
connectionSpecs = DEFAULT_CONNECTION_SPECS;
proxySelector = ProxySelector.getDefault();
cookieJar = CookieJar.NO_COOKIES;
socketFactory = SocketFactory.getDefault();
hostnameVerifier = OkHostnameVerifier.INSTANCE;
certificatePinner = CertificatePinner.DEFAULT;
proxyAuthenticator = Authenticator.NONE;
authenticator = Authenticator.NONE;
connectionPool = new ConnectionPool();
dns = Dns.SYSTEM;
followSslRedirects = true;
followRedirects = true;
retryOnConnectionFailure = true;
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
}
1.2 创建Request
请求对象
Request request = new Request.Builder()
.url(url)
.build();
1.3 创建Call
对象
OkHttpClient 实现了Call.Factory
,负责根据请求创建新的 Call。
callFactory 负责创建 HTTP 请求,HTTP 请求被抽象为了 okhttp3.Call 类,它表示一个已经准备好,可以随时执行的 HTTP 请求
Call call = client.newCall(request);
1.4 同步阻塞执行请求
Response response = call.execute();
1.5 异步回调执行请求
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
}
});
2. 精妙的线程池
2.1 同步请求时
实际上newCall
方法会返回一个Realcall
对象,而它同步执行execute()方法时:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
try {
//仅仅标记作用
client.dispatcher().executed(this);
//这才是真正的执行下一步发请求,同步异步都会走到这个方法
Response result = getResponseWithInterceptorChain(false);
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
虽然dispatcher也掺和了,其实没什么用,涉及到 dispatcher 的内容只不过是在内部做了个标记,表明有个有个任务正在执行:
/** Used by {@code Call#execute} to signal it is in-flight. */
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
2.2 异步请求
2.2.1 反向代理模型
服务器会添加Header并自动转发请求给后端集群,接着返回数据结果给用户。可以提高服务的负载均衡能力,实现非阻塞、高可用、高并发连接,避免资源全部放到一台服务器而带来的负载。
fxdl
而在OkHttp中,非常类似于上述场景,它使用Dispatcher
作为任务的派发器,线程池对应多台后置服务器,用AsyncCall
对应Socket请求,用Deque<readyAsyncCalls>
对应Nginx的内部缓存。
具体成员如下
- maxRequests = 64: 最大并发请求数为64
- maxRequestsPerHost = 5: 每个主机最大请求数为5
- Dispatcher: 分发者,也就是生产者(默认在主线程)
- AsyncCall: 队列中需要处理的Runnable(包装了异步回调接口)
- ExecutorService:消费者池(也就是线程池)
-
Deque<readyAsyncCalls>
:缓存(用数组实现,可自动扩容,无大小限制) -
Deque<runningAsyncCalls>
:正在运行的任务,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存
通过将请求任务分发给多个线程,可以显著的减少I/O等待时间。
2.2.2 异步执行过程
具体过程如下:
-
enqueue()
方法调用后,判断runningAsyncCalls
队列是否还有空位子。 - 若有空位,那就调用
executorService().execute(call);
交给线程池执行请求,并且添加进运行队列 - 啊,没空位了,只能进
readyAsyncCalls
等待队列了……
看一下call任务内部的执行方法:
…………
//果然和同步方式一样,最后通过这个方法去发送给下一步。
Response response = getResponseWithInterceptorChain(forWebSocket);
…………
finally {
//最关键的代码
client.dispatcher().finished(this);
}
当任务执行完成后,无论是否有异常,finally代码段总会被执行,也就是会调用Dispatcher
的finished函数,打开源码,发现它将正在运行的任务Call从队列runningAsyncCalls
中移除后,接着执行promoteCalls()
函数
if (runningCallsForHost(call) < maxRequestsPerHost) {
//将缓存等待区最后一个移动到运行区中,并执行
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
实际上就是会在任务执行完的最后,把自己从运行队列移除,唤醒一个等待队列的任务,让他去执行。
这样,就主动的把缓存队列向前走了一步,而没有使用互斥锁等复杂编码。
2.2.3 异步总结
- OkHttp采用Dispatcher技术,反向代理,优化了单生产者多消费者模式,与线程池配合实现了高并发,低阻塞的运行
- Okhttp采用Deque作为缓存,按照入队的顺序先进先出
- OkHttp最出彩的地方就是在try/finally中调用了finished函数,可以在任务结束时候唤醒等待的任务,主动控制等待队列的移动,而不是采用锁或者wait/notify,极大减少了编码复杂性
3. 工作的核心----拦截器
3.1 拦截器的介绍与种类
不管同步异步发送任务请求,最后都会执行getResponseWithInterceptorChain
:
private 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 (!retryAndFollowUpInterceptor.isForWebSocket()) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(
retryAndFollowUpInterceptor.isForWebSocket()));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
拦截器是个啥?官方介绍和图片:
the whole thing is just a stack of built-in interceptors.
ljq2
可见 Interceptor 是 OkHttp 最核心的一个东西,不要误以为它只负责拦截请求进行一些额外的处理(例如 cookie),实际上它把实际的网络请求、缓存、透明压缩等功能都统一了起来,每一个功能都只是一个 Interceptor,它们再连接成一个 Interceptor.Chain
,环环相扣,最终圆满完成一次网络请求。
从 getResponseWithInterceptorChain
函数我们可以看到,Interceptor.Chain
的分布依次是:
- 在配置
OkHttpClient
时设置的interceptors
; - 负责失败重试以及重定向的
RetryAndFollowUpInterceptor
; - 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的
BridgeInterceptor
; - 负责读取缓存直接返回、更新缓存的
CacheInterceptor
; - 负责和服务器建立连接的
ConnectInterceptor
; - 配置 OkHttpClient 时设置的
networkInterceptors
; - 负责向服务器发送请求数据、从服务器读取响应数据的
CallServerInterceptor
。
3.2 拦截器的责任链模式
实际上,是责任链模式的最佳应用(如同事件分发机制),每个拦截器可以自己拦截处理,或者交给下一个拦截器,让每个 Interceptor
自行决定能否完成任务以及怎么完成任务。
其实 Interceptor
的设计也是一种分层的思想,每个 Interceptor
就是一层。为什么要套这么多层呢?分层的思想在 TCP/IP
协议中就体现得淋漓尽致,分层简化了每一层的逻辑,每层只需要关注自己的责任(单一原则思想也在此体现),而各层之间通过约定的接口/协议进行合作(面向接口编程思想),共同完成复杂的任务。
4 总结
-
OkHttpClient
实现Call.Factory
,负责为 Request 创建 Call; -
RealCall
为具体的 Call 实现,其enqueue()
异步接口通过Dispatcher
利用ExecutorService
实现,而最终进行网络请求时和同步execute()
接口一致,都是通过getResponseWithInterceptorChain()
函数实现; -
getResponseWithInterceptorChain()
中利用Interceptor
链条,分层实现缓存、透明压缩、网络 IO 等功能;
引用