1 OKHttp简介
1.1 OKHttp是什么?有什么作用?为什么好用(1.1-1.2)?
OkHttp是一套高性能Http网络请求的依赖库,由Square公司设计研发并开源,目前可以在Java和Kotlin中使用。它的职责跟 HttpUrlConnection是一样的,支持spdy、http 2.0、websocket ,支持同步、异步,而且OkHttp又封装了线程池,封装了数据转换,封装了参数使用、错误处理等,api使用起来更加方便。可以把它理解成是一个封装之后的类似HttpUrlConnection的一个库。因此它也是每一个 Android开发工程师的必备技能,了解其内部实现原理可以更好地进行功能扩展、封装以及优化。本博客会在的3.12.1的基础上,重新梳理整个网络请求的流程,以及实现机制。
1.2 OkHttp VS Volley?
(1)Volley的优势在于封装的更好,而使用OkHttp需要我们有足够的能力再进行一次封装,才能像使用一个框架一样更加顺手;
(2)而OkHttp的优势在于性能更高,因为OkHttp基于NIO和Okio ,所以性能上要比Volley更快。(补充:从硬盘读取数据,程序一直等,数据读完后才能继续操作,这种是最简单的也叫阻塞式IO;你读你的,我程序接着往下执行,等数据处理完你再来通知我,然后再处理回调,这种就是NIO的非阻塞方式。所以NIO当然要比IO的性能要好了,而Okio是Square公司基于IO和NIO基础上做的一个更简单、高效处理数据流的一个库。)
(3)Volle不支持提交大数据,所以不适合上传文件;而OkHttp具备这种功能。
补充:Volley是Google官方出的一套小而巧的异步请求库,该框架封装的扩展性很强,支持HttpClient、HttpUrlConnection,甚至支持 OkHttp。Volley也有缺陷,比如不支持post大数据,所以不适合上传文件。不过Volley设计的初衷本身也就是为频繁的、数据量小的网络请求而生!
1.3 OkHttp VS Retrofit?
1.4 使用
private synchronized OkHttpClient getOkHttpClient() {
return new OkHttpClient
.Builder()
// 设置公共参数拦截器
.addInterceptor(new CommonInterceptor())
// 设置缓存策略:设置读写的缓存目录,和缓存大小的限制为10 * 1024 * 1024(OKHttp默认不支持Post缓存)
.cache(new Cache(new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "OkHttpCache"), 20 * 1024 * 1024))
// 设置缓存拦截器
.addInterceptor(new CacheInterceptor())
// 设置网络拦截器
.addNetworkInterceptor(new CacheInterceptor())
// 连接超时
.connectTimeout(3000, TimeUnit.MILLISECONDS)
// 读取超时
.readTimeout(3000, TimeUnit.MILLISECONDS)
// 写入超时
.writeTimeout(3000, TimeUnit.MILLISECONDS)
.build();
}
public final class CommonInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder()
.header("userId", "11249")
.header("sessionId", "155056366467311249");
Request build = builder.build();
return chain.proceed(build);
}
}
public final class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder requestBuilder = request.newBuilder();
requestBuilder.removeHeader("Pragma");
if (NetworkUtils.isNetworkAvailable(OkHttpActivity.this)) {
// 如果有网,确定响应缓存多长时间 read from cache for 0 minute
requestBuilder.header("Cache-Control", "public, max-age=" + 0);
} else {
// 如果没网,设置超时为1天,用原来的请求重新构建一个强制从缓存中读取的请求
// requestBuilder.cacheControl(CacheControl.FORCE_CACHE);
requestBuilder.header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24);
}
return chain.proceed(requestBuilder.build());
}
}
/**
* 异步get请求
*
* @param view
*/
public void getEnqueue(View view) {
// 1、创建okHttpClient对象
OkHttpClient mOkHttpClient = getOkHttpClient();
// 2、创建一个Request对象
final Request request = new Request.Builder()
.url("https://m.so.com/jump?u=http://m.msxf.net/book/70789/index.html&m=edd316&from=m.so.com")
.tag(this)
.build();
// 3、通过request的对象去构造一个Call对象,将你的请求封装成了任务,有execute()和cancel()等方法
Call call = mOkHttpClient.newCall(request);
// 4、调用的是call.enqueue以异步的方式去执行请求,将call加入调度队列
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
LogUtils.e(TAG, "onFailure:" + e.toString());
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
// 最终的url:response.request().url():https://m.msxf.cn/book/70789/index.html(意味着url进行了跳转)
LogUtils.d(TAG, "response.request().url():" + response.request().url());
// onResponse执行的线程并不是UI线程
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
// 5、Response
// 通过response.body().string()获取返回的字符串;
// 调用response.body().bytes()获得返回的二进制字节数组;
// 调用response.body().byteStream()获取返回的inputStream
LogUtils.d(TAG, "response:" + response.body().string());
LogUtils.d(TAG, "cacheResponse:" + response.cacheResponse());
LogUtils.d(TAG, "networkResponse:" + response.networkResponse());
// response.networkResponse().request().url():https://m.msxf.cn/book/70789/index.html
LogUtils.d(TAG, "response.networkResponse().request().url():" + response.networkResponse().request().url());
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
});
}
/**
* 同步get请求
*
* @param view
*/
public void getExecute(View view) {
ThreadPoolManager.getInstance().executeIo(new Runnable() {
@Override
public void run() {
// 2、创建一个Request对象
final Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.tag(this)
.build();
// 3、通过request的对象去构造一个Call对象,将你的请求封装成了任务,有execute()和cancel()等方法
Call call = mOkHttpClient.newCall(request);
try {
// 4、调用call.execute()以阻塞的方式去执行请求 Response response = client.newCall(request).execute();
Response response = call.execute();
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
// 5、Response
LogUtils.d(TAG, "response:" + response.body().string());
LogUtils.d(TAG, "cacheResponse:" + response.cacheResponse());
LogUtils.d(TAG, "networkResponse:" + response.networkResponse());
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
1.6 补充
(1)补充一点网络知识:一些常用的状态码
100~199:指示信息,表示请求已接收,继续处理
200~299:请求成功,表示请求已被成功接收、理解
300~399:重定向,要完成请求必须进行更进一步的操作
400~499:客户端错误,请求有语法错误或请求无法实现
500~599:服务器端错误,服务器未能实现合法的请求
1.7 学习链接
Android OkHttp完全解析 是时候来了解OkHttp了-鸿洋
2 OKHttp原理
2.1 OKHttp内部的请求流程图如下所示
2.2 在整个Okhttp的系统中,我们还要理解以下几个关键角色?
(1)OkHttpClient:通信的客户端,用来统一管理发起请求与解析响应;
(2)Call:Call是一个接口,它是HTTP请求的抽象描述,具体实现类是RealCall,它由CallFactory创建;
(3)Dispatcher:Dispatcher是OkHttpClient的任务调度器,是一种门户模式,主要用来实现执行、取消异步+同步请求操作。本质上是内部维护了一个线程池去执行异步操作,并且根据一定的策略,保证最大并发个数、同一host主机允许执行请求的线程个数等;
(4)Request:请求,封装请求的具体信息,例如:url、header等;
(5)RequestBody:请求体,用来提交流、表单等请求信息;
(6)Response:HTTP请求的响应,获取响应信息,例如:响应header等;
(7)ResponseBody:HTTP请求的响应体,被读取一次以后就会关闭,所以我们重复调用responseBody.string()获取请求结果是会报错的;
(8)Interceptor:Interceptor是请求拦截器,负责拦截并处理请求,它将网络请求、缓存、透明压缩等功能都统一起来,每个功能都是一个Interceptor,所有的Interceptor最终连接成一个Interceptor.Chain,典型的责任链模式实现;
(9)StreamAllocation:用来控制Connections与Streas的资源分配与释放;
(10)RouteSelector:选择路线与自动重连;
(11)RouteDatabase:记录连接失败的Route黑名单;
3 请求与响应流程?
Okhttp的整个请求与响应的流程就是:(1)OkHttpClient发起请求后,将Request封装到接口Call的实现类RealCall中;(2)同步请求通过调用RealCall.exectute()方法直接返回当前请求的Response,异步请求调用RealCall.enqueue()方法将请求(AsyncCall)发送到Dispatcher的请求队列中去;(3)Dispatcher不断从请求队列里取出请求(Call);(4)请求从RetryAndFollowUpInterceptor开始层层传递到CallServerInterceptor,每一层都对请求做相应的处理,处理的结构再从CallServerInterceptor层层返回给RetryAndFollowUpInterceptor,最后请求的发起者通过回调(Callback)获取服务器返回的结果。
3.1 请求的封装
Request是由Okhttp发出,真正的请求都被封装了在了接口Call的实现类RealCall中,如下所示:
(1)Request的构造方法如下
public final class Request {
Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
this.tags = Util.immutableMap(builder.tags);
}
(2)Call接口如下
public interface Call extends Cloneable {
// 返回当前请求
Request request();
// 同步请求方法,此方法会阻塞当前线程知道请求结果放回
Response execute() throws IOException;
// 异步请求方法,此方法会将请求添加到队列中,然后等待请求返回
void enqueue(Callback responseCallback);
// 取消请求
void cancel();
// 请求是否在执行,当execute()或者enqueue(Callback responseCallback)执行后该方法返回true
boolean isExecuted();
// 请求是否被取消
boolean isCanceled();
// 创建一个新的一模一样的请求
Call clone();
interface Factory {
Call newCall(Request request);
}
}
(3)RealCall封装了请求的调用,它的构造方法如下
final class RealCall implements Call {
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// 我们构建的OkHttpClient,用来传递参数
this.client = client;
this.originalRequest = originalRequest;
// 是不是WebSocket请求,WebSocket是用来建立长连接的,后面我们会说。
this.forWebSocket = forWebSocket;
// 构建RetryAndFollowUpInterceptor拦截器
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
}
3.2 请求的发送
1、同步请求:直接执行,并返回请求结果
final class RealCall implements Call {
public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
timeout.enter();
eventListener.callStart(this);
try {
// 通知dispatcher已经进入执行状态
client.dispatcher().executed(this);
// 通过getResponseWithInterceptorChain()获取Response
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
e = timeoutExit(e);
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
}
2、异步请求:构造一个AsyncCall,并将自己加入处理队列中
final class RealCall implements Call {
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
// 构造一个AsyncCall,并将自己加入处理队列中
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
}
3、AsyncCall本质上是一个Runable,Dispatcher会调度ExecutorService来执行这些Runable的run()方法,再执行execute()
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
protected void execute() {
boolean signalledCallback = false;
timeout.enter();
try {
// 通过getResponseWithInterceptorChain()获取Response
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
// 回调失败
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
// 回调结果
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
e = timeoutExit(e);
if (!signalledCallback) {
.......
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
}
public abstract class NamedRunnable implements Runnable {
public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
3.3 请求的调度
1、分析如下:
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
// 正在准备中的异步请求队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
// 正在运行中的异步请求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
// 同步请求
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
/** Used by {@code Call#execute} to signal it is in-flight. */
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
private boolean promoteAndExecute() {
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; // 正在运行中的异步请求不得超过64
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // 同一个host下的异步请求不得超过5个
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;
}
}
2、整个流程如下:
(1)Dispatcher是一个任务调度器,它内部维护了三个双端队列:
readyAsyncCalls:准备运行的异步请求
runningAsyncCalls:正在运行的异步请求
runningSyncCalls:正在运行的同步请求
(2)记得异步请求与同步请求,都利用ExecutorService来调度执行AsyncCall;
(3)同步请求就直接把请求添加到正在运行的同步请求队列runningSyncCalls中;异步请求会做个判断:如果正在运行的异步请求不超过64,而且同一个host下的异步请求不得超过5个则将请求添加到正在运行的同步请求队列中runningAsyncCalls并开始执行请求,否则就只添加到readyAsyncCalls继续等待。
3.4 请求的处理
final class RealCall implements Call {
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
// 这里可以看出,我们自定义的Interceptor会被优先执行
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) {
// 配置OkHttpClient时设置的networkInterceptors
interceptors.addAll(client.networkInterceptors());
}
// 负责从服务器读取响应的数据
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
}
(1)完成了对请求的所有处理过程,Interceptor将网络请求、缓存、建立连接、从服务器读取响应的数据等功能统一了起来,它的实现采用责任链模式,各司其职,每个功能都是一个Interceptor,上一级处理完成以后传递给下一级,它们最后连接成了一个Interceptor.Chain,开启链式调用。
(2)位置决定功能,位置靠前的先执行,最后一个则复制与服务器通讯,请求从RetryAndFollowUpInterceptor开始层层传递到CallServerInterceptor,每一层都对请求做相应的处理,处理的结构再从CallServerInterceptor层层返回给RetryAndFollowUpInterceptor,最后请求的发起者获得了服务器返回的结果。
4 拦截器(Okhttp核心功能)
(1)所有的拦截器(包括我们自定义的)都实现了Interceptor接口,如下所示:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
// 返回Request执行后返回的连接
@Nullable Connection connection();
}
}
(2)Okhttp内置的拦截器如下所示:
RetryAndFollowUpInterceptor:负责失败重试以及重定向。
BridgeInterceptor:负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。
CacheInterceptor:负责读取缓存以及更新缓存。
ConnectInterceptor:负责与服务器建立连接。
CallServerInterceptor:负责从服务器读取响应的数据。
(3)继续看RealInterceptorChain里是怎么一级级处理的:
public final class RealInterceptorChain implements Interceptor.Chain {
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
// 递归调用下一个拦截器的intercept()方法(核心方法)
RealInterceptorChain next = new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index + 1, request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
return response;
}
}
这个方法在调用proceed方法之后,会继续构建一个新的RealInterceptorChain对象,并调用下一个interceptor来继续请求,直到所有interceptor都处理完毕,将得到的response返回。
(5)每个拦截器的方法都遵循这样的规则:
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 1、Request阶段,该拦截器在Request阶段负责做的事情
// 2、调用RealInterceptorChain.proceed(),其实是在递归调用下一个拦截器的intercept()方法
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
// 3、Response阶段,完成了该拦截器在Response阶段负责做的事情,然后返回到上一层的拦截器。
return response;
}
}
从上面的描述可知,Request是按照interpretor的顺序正向处理,而Response是逆向处理的,这参考了OSI七层模型的原理。CallServerInterceptor相当于最底层的物理层,请求从上到逐层包装下发,响应从下到上再逐层包装返回。interceptor的执行顺序:RetryAndFollowUpInterceptor -> BridgeInterceptor -> CacheInterceptor -> ConnectInterceptor -> 。CallServerInterceptor。
4.1 RetryAndFollowUpInterceptor:负责失败重试以及重定向
4.1.1 源码如下
public final class RetryAndFollowUpInterceptor implements Interceptor {
private static final int MAX_FOLLOW_UPS = 20;
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
// 1. 构建一个StreamAllocation对象,StreamAllocation相当于是个管理类,维护了
//Connections、Streams和Calls之间的管理,该类初始化一个Socket连接对象,获取输入/输出流对象。
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
// 重定向次数
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release(true);
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
// 2. 继续执行下一个Interceptor,即BridgeInterceptor
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// 3. 抛出异常,则检测连接是否还可以继续重试
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getFirstConnectException();
}
releaseConnection = false; // 继续重试
continue;
} catch (IOException e) {
// 和服务端建立连接失败
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false; // 继续重试
continue;
} finally {
// 检测到其他未知异常,则释放连接和资源
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release(true);
}
}
// 构建响应体,这个响应体的body为空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
// 4. 根据响应码处理请求,返回Request不为空时则进行重定向处理
Request followUp;
try {
followUp = followUpRequest(response, streamAllocation.route());
} catch (IOException e) {
streamAllocation.release(true);
throw e;
}
if (followUp == null) {
streamAllocation.release(true);
return response;
}
closeQuietly(response.body());
// 重定向的次数不能超过20次
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release(true);
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release(true);
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
if (!sameConnection(response, followUp.url())) {
streamAllocation.release(false);
streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(followUp.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}
}
}
4.1.2 流程分析
1、先来说说StreamAllocation这个类的作用,这个类协调了三个实体类的关系:
(1)Connections:连接到远程服务器的物理套接字,这个套接字连接可能比较慢,所以它有一套取消机制;
(2)Streams:定义了逻辑上的HTTP请求/响应对,每个连接都定义了它们可以携带的最大并发流,HTTP/1.x每次只可以携带一个,HTTP/2每次可以携带多个;
(3)Calls:定义了流的逻辑序列,这个序列通常是一个初始请求以及它的重定向请求,对于同一个连接,我们通常将所有流都放在一个调用中,以此来统一它们的行为;
2、整个方法的流程:
(1)构建一个StreamAllocation对象,相当于是个管理类,维护了Connections、Streams和Calls之间的管理,该类初始化一个Socket连接对象,获取输入/输出流对象;
(2)继续执行下一个Interceptor,即BridgeInterceptor;
(3)抛出异常,则检测连接是否还可以继续重试。以下情况不会重试:
1.客户端配置出错不再重试;
2.出错后,request body不能再次发送;
3.发生以下Exception也无法恢复连接:ProtocolException:协议异常、InterruptedIOException:中断异常、SSLHandshakeException:SSL握手异常
、SSLPeerUnverifiedException:SSL握手未授权异常;
4.没有更多线路可以选择;
(4)根据响应码处理请求,返回Request不为空时,则调用followUpRequest(response, streamAllocation.route())进行重定向处理;(注意:重定向的次数不能超过20次)。
4.1.3 根据响应码进行重定向处理,由followUpRequest()方法完成,源码如下
1、重定向会涉及到一些网络编程的知识,这里如果没有完成理解,只要知道RetryAndFollowUpInterceptor的作用就是处理了一些连接异常以及重定向就可以了。
public final class RetryAndFollowUpInterceptor implements Interceptor {
private Request followUpRequest(Response userResponse) throws IOException {
if (userResponse == null) throw new IllegalStateException();
Connection connection = streamAllocation.connection();
Route route = connection != null
? connection.route()
: null;
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
// 407,代理认证
case HTTP_PROXY_AUTH:
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
// 401,未经认证
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
// 307,308
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
// 300,301,302,303
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// 客户端在配置中是否允许重定向
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// url为null,不允许重定向
if (url == null) return null;
// 查询是否存在http与https之间的重定向
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408,超时
case HTTP_CLIENT_TIMEOUT:
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
return userResponse.request();
default:
return null;
}
}
}
4.2 BridgeInterceptor:也称连接桥,针对Header做了一些处理,在Request阶段配置用户信息并添加一些请求头;在Response阶段,进行gzip解压等
1、分析如下:
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
// 1 进行Header的包装
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
// 这里有个坑:如果你在请求的时候主动添加了"Accept-Encoding: gzip" ,transparentGzip=false,那你就要自己解压,如果
// 你没有吹解压,或导致response.string()乱码。
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
// 创建OkhttpClient配置的cookieJar
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());
// 解析服务器返回的Header,如果没有这事cookie,则不进行解析
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
// 判断服务器是否支持gzip压缩,如果支持,则将压缩提交给Okio库来处理
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
}
2、主要提一下"Accept-Encoding"、“gzip”,关于它有以下几点需要注意:
(1)开发者没有添加Accept-Encoding时,自动添加Accept-Encoding: gzip;
(2)自动添加Accept-Encoding,会对request,response进行自动解压;
(3)手动添加Accept-Encoding,不负责解压缩;
(4)自动解压时移除Content-Length,所以上层Java代码想要contentLength时为-1;
(5)自动解压时移除Content-Encoding;
(6)自动解压时,如果是分块传输编码,Transfer-Encoding: chunked不受影响;
4.3 CacheInterceptor:负责读取缓存以及更新缓存
4.3.1 源码如下
1、为了节省流量和提高响应速度,Okhttp是有自己的一套缓存机制的,CacheInterceptor就是用来负责读取缓存以及更新缓存的。
public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
// 1、读取候选缓存,具体如何读取在 6.1 缓存机制-缓存策略 中讲解
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 2、创建并返回缓存策略,强制缓存、对比缓存等,关于缓存策略在 6.2 缓存机制-缓存策略 中讲解
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); // get()
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// 3(1)、 根据策略,不使用网络,又没有缓存,直接报错并返回错误码504
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 3(2)、根据策略,不使用网络,有缓存,直接返回缓存
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// 3(3)、根据策略,使用网络,没有缓存,继续执行下一个Interceptor,即ConnectInterceptor
networkResponse = chain.proceed(networkRequest);
} finally {
// 如果发生IO异常,则释放掉缓存
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 3(4)、根据策略,使用网络,有缓存,需要服务端参与判断是否继续使用缓存。如果返回304,客户端可以继续使用缓存;
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
// 3(4)、如果返回200,表示需要重新请求资源,则读取网络请求结果
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 5、如果网络请求成功,并且请求头没配置no-store,则对请求结果进行缓存
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
// 6、最后返回网络读取的结果
return response;
}
}
4.3.2 流程分析
1、读取候选缓存,具体如何读取在 6.2 缓存机制-缓存策略 中讲解;
2、创建并返回缓存策略,强制缓存、对比缓存等,关于缓存策略在 6.3 缓存机制-缓存策略 中讲解;
3、缓存策略CacheStrategy利用networkRequest与cacheResponse这两个结果,组合生成最终的策略如下所示:(networkRequest为null指:不使用网络请求;cacheResponse为null指:没有缓存)
(1)根据策略,不使用网络,又没有缓存,直接报错并返回错误码504;
(2)根据策略,不使用网络,有缓存,直接返回缓存(强制缓存);
(3)根据策略,使用网络,没有缓存,继续执行下一个Interceptor,即ConnectInterceptor;
(4)根据策略,使用网络,有缓存,需要服务端参与判断是否继续使用缓存(对比缓存)。当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ;客户端第二次请求数据时,将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回200,表示需要重新请求资源,则读取网络请求结果;如果返回304,客户端可以继续使用缓存。纤细看6.1.1
4、如果网络请求成功,并且请求头没配置no-store,则对请求结果进行缓存;
5、最后返回网络读取的结果;
4.4 ConnectInterceptor:负责与服务器建立连接
1、在RetryAndFollowUpInterceptor里初始化了一个StreamAllocation对象,它内部初始化了一个Socket对象用来做连接,但是并没有真正的连接,等到处理完header和缓存信息之后,才调用ConnectInterceptor来进行真正的连接:
public final class ConnectInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
// 创建输出流
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
// 建立连接,包含了连接、连接池等一整套的Okhttp的连接机制,具体在 5 连接机制 中讲解;
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
2、ConnectInterceptor在Request阶段建立连接,处理方式也很简单,创建了两个对象:
HttpCodec:用来编码HTTP requests和解码HTTP responses;
RealConnection:连接对象,负责发起与服务器的连接;
这里事实上包含了连接、连接池等一整套的Okhttp的连接机制,具体在 5 连接机制 中讲解;
4.5 CallServerInterceptor:负责发送和接收服务器数据
1、CallServerInterceptor:负责发送和接收服务器数据
public final class CallServerInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
// 这些对象在前面的Interceptor都已经创建完毕
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
// 1. 写入请求头
httpCodec.writeRequestHeaders(request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return what
// we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
responseBuilder = httpCodec.readResponseHeaders(true);
}
// 2. 写入请求体
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from
// being reused. Otherwise we're still obligated to transmit the request body to leave the
// connection in a consistent state.
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
// 3. 读取响应头
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
// 4. 读取响应体
int code = response.code();
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
}
2、通过ConnectInterceptor已经连接到服务器了,接下来我们就是写入请求数据以及读出返回数据了。整个流程:
写入请求头 --> 写入请求体 --> 读取响应头 --> 读取响应体;
5 连接机制
1、连接的创建是在StreamAllocation对象统筹下完成的,它早在RetryAndFollowUpInterceptor就被创建了,StreamAllocation对象主要用来管理两个关键角色:
RealConnection:真正建立连接的对象,利用Socket建立连接;
ConnectionPool:连接池,用来管理和复用连接;
5.1 创建连接
1、ConnectInterceptor用来完成连接,而真正的连接在RealConnection中实现,连接由连接池ConnectPool来管理,连接池最多保持5个地址的连接keep-alive,每个keep-alive时长为5分钟,并有异步线程清理无效的连接。主要由以下两个方法完成:
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
5.1.1 StreamAllocation.newStream()调用findConnection()方法,接着会创建一个RealConnection对象来建立连接
public final class StreamAllocation {
public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
}
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled);
return candidate;
}
}
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
synchronized (connectionPool) {
// 1 查看当前是否有完好的连接
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
// 2 连接池中是否有可用的连接,有则直接返回RealConnection对象使用
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
return connection;
}
selectedRoute = route;
}
// 线程的选择,多IP操作
if (selectedRoute == null) {
selectedRoute = routeSelector.next();
}
// 3 如果没有可用连接,则自己创建一个RealConnection对象
RealConnection result;
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
// Now that we have an IP address, make another attempt at getting a connection from the pool.
// This could match due to connection coalescing.
Internal.instance.get(connectionPool, address, this, selectedRoute);
if (connection != null) {
route = selectedRoute;
return connection;
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result);
}
// 4 调用RealConnection的connect()方法建立连接,最终调用的是Java中Socket的connect()方法,开始TCP以及TLS握手操作
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
routeDatabase().connected(result.route());
// 5 将新创建的连接,放在连接池中
Socket socket = null;
synchronized (connectionPool) {
// Pool the connection.
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
return result;
}
}
1、整个流程如下:
(1)查找是当前否有完整的连接可用:Socket没有关闭;输入流没有关闭;输出流没有关闭;Http2连接没有关闭;
(2)连接池中是否有可用的连接,有则直接返回RealConnection对象使用;
(3)如果没有可用连接,则自己创建一个RealConnection对象;
(4)调用RealConnection的connect()方法建立连接,最终调用的是Java中Socket的connect()方法,开始TCP连接以及TLS握手操作;
(5)将新创建的连接加入连接池;
5.1.2 调用RealConnection的connect()方法建立连接,最终调用的是Java中Socket的connect()方法,开始TCP连接以及TLS握手操作
public final class RealConnection extends Http2Connection.Listener implements Connection {
public void connect(
int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
if (protocol != null) throw new IllegalStateException("already connected");
// 线路选择
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
// 开始连接
while (true) {
try {
// 如果是通道模式,则建立通道连接
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout);
}
// 否则进行Socket连接,一般都是属于这种情况
else {
connectSocket(connectTimeout, readTimeout);
}
// 建立https连接
establishProtocol(connectionSpecSelector);
break;
} catch (IOException e) {
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
// 根据代理类型的不同处理Socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
try {
// 建立Socket连接
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
// 获取输入/输出流
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
}
5.2 连接池
5.2.1 为什么需要连接池?
(1)在复杂的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。复用连接就需要对连接进行管理,这里就引入了连接池的概念。
(2)Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。
5.2.2 连接池被调用位置
public final class RetryAndFollowUpInterceptor implements Interceptor {
public Response intercept(Chain chain) throws IOException {
// client.connectionPool()
// 构建一个StreamAllocation对象,StreamAllocation相当于是个管理类,维护了Connections、Streams和Calls之间的管理,该类初始化一个Socket连接对象,获取输入/输出流对象
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
}
}
5.2.3 连接池的put()和acquire()方法
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable); // cleanupRunnable
}
connections.add(connection);
}
void acquire(Address address, StreamAllocation streamAllocation, @Nullable Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return;
}
}
}
5.2.4 ConectionPool在内部维护了一个线程池来清理连接
1、分析如下:
public final class ConnectionPool {
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
// 清理连接,在线程池executor里调用。
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
while (true) {
// 执行清理连接操作,并返回下次需要清理的时间。
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
// 在timeout时间内释放锁
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
}
2、整个流程如下:
(1)ConectionPool在内部维护了一个线程池来清理连接池,清理任务由cleanup()方法完成,详细看(4);
(2)这是一个阻塞操作,首先执行清理,并返回下次需要清理的间隔时间,调用调用wait()方法释放锁;
(3)等时间到了以后,再次进行清理,并返回下一次需要清理的时间,循环往复;
5.2.5 执行清理连接操作的cleanup()方法具体实现
1、分析如下:
public final class ConnectionPool {
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
// 遍历所有的连接,标记处不活跃的连接。
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// 1. 查询此连接内部的StreanAllocation的引用数量,pruneAndGetAllocationCount()
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// 2. 标记空闲连接。
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {
// 3. 如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 4. 返回此连接的到期时间,供下次进行清理。
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 5. 全部都是活跃连接,5分钟时候再进行清理。
return keepAliveDurationNs;
} else {
// 6. 没有任何连接,跳出循环。
cleanupRunning = false;
return -1;
}
}
// 7. 关闭连接,返回时间0,立即再次进行清理。
closeQuietly(longestIdleConnection.socket());
return 0;
}
}
2、整个流程如下:
(1)查询此连接内部的StreanAllocation的引用数量;
(2)标记空闲连接;
(3)如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉;
(4)返回此连接的到期时间,供下次进行清理;
(5)全部都是活跃连接,5分钟时候再进行清理;
(6)没有任何连接,跳出循环;
(7)关闭连接,返回时间0,立即再次进行清理;
5.2.6 查找引用计数由pruneAndGetAllocationCount()方法实现
(1)核心思想:每创建一个StreamAllocation对象,就会把它添加进StreamAllocation虚引用列表中,如果连接关闭以后就将StreamAllocation对象从该列表中移除。如果所有的StreamAllocation引用都没有了,返回引用计数0,判定一个连接为空闲连接:
(2)pruneAndGetAllocationCount方法
public final class ConnectionPool {
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
// 虚引用列表
List<Reference<StreamAllocation>> references = connection.allocations;
// 遍历虚引用列表
for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);
// 如果虚引用StreamAllocation正在被使用,则跳过进行下一次循环,
if (reference.get() != null) {
// 引用计数
i++;
continue;
}
// We've discovered a leaked allocation. This is an application bug.
StreamAllocation.StreamAllocationReference streamAllocRef =
(StreamAllocation.StreamAllocationReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
// 否则移除该StreamAllocation引用
references.remove(i);
connection.noNewStreams = true;
// 如果所有的StreamAllocation引用都没有了,返回引用计数0
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
// 返回引用列表的大小,作为引用计数
return references.size();
}
}
(3)StreamAllocation虚引用列表allocations实现
public final class RealConnection {
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
private void release(RealConnection connection) {
for (int i = 0, size = connection.allocations.size(); i < size; i++) {
Reference<StreamAllocation> reference = connection.allocations.get(i);
if (reference.get() == this) {
connection.allocations.remove(i); // 连接关闭以后就将StreamAllocation对象从该列表中移除
return;
}
}
throw new IllegalStateException();
}
}
6 缓存机制
6.1 HTTP缓存策略的相关理论知识
6.1.1 HTTP的缓存可以分为两种:强制缓存和对比缓存
(1)强制缓存:无需再向服务端询问;当客户端第一次请求数据时,根据请求头校验控制Cache-Control,如果允许缓存,客户端将数据储存到缓存中;客户端第二次请求数据时,根据服务端返回的缓存过期时间Expires,没有过期,就可以继续使用缓存,否则不使用;
(2)对比缓存:需要服务端参与判断是否继续使用缓存;当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ;客户端第二次请求数据时,将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回200,表示需要重新请求资源,则读取网络请求结果;如果返回304,客户端可以继续使用本地缓存;
(3)强制缓存优先于对比缓存。
6.1.2 强制缓存使用的的两个标识
(1)Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据;到期时间是服务端生成的,客户端和服务端的时间可能有误差。
(2)Cache-Control:Expires有个时间校验控制,所有HTTP1.1采用Cache-Control替代Expires。Cache-Control的取值有以下几种:
1.private:数据只能被缓存到私有的cache,仅对某个用户有效,不能共享;
2.public:数据皆被缓存起来,就连有密码保护的网页也储存,安全性很低;
3.max-age=xxx:缓存的数据将在 xxx 秒后失效;
4.no-cache:当次响应数据可以缓存,但是下次请求头不可配置"no-cache",并且使用对比缓存跟服务器验证了响应的有效性后才能使用缓存;
5.no-store:请求和响应都禁止被缓存;
6.only-if-cached,表示只接受是被缓存的内容;
7.max-stale:允许读取过期时间小于max-stale值的缓存对象;
8.no-transform:告知代理不要更改媒体类型,比如jpg被改成png。
6.1.3 对比缓存的标识1:Last-Modified/If-Modified-Since
(1)第一次请求
1.客户端发起HTTP GET请求一个文件;
2.服务器处理请求,在HTTP响应头中将Last-Modified: "Tue, 12 Jan 2016 09:31:27 GMT"传送到客户端,状态码200;
(2)第二次请求
1.客户端发起HTTP GET请求一个文件,客户端会将If-Modified-Since: "Tue, 12 Jan 2016 09:31:27 GMT"添加到请求头;
2.服务端收到第二次请求的时候,发现携带了If-Modified-Since字段,与自己当前的资源修改时间进行对比。如果自己的资源修改时间大于客户端发来的资源修改时间,则返回200,表示资源最近做过修改,需要重新请求资源;否则返回304,表示资源没有被修改,客户端可以继续使用缓存;
6.1.4 对比缓存的标识2:Etag/If-None-Match
(1)Etag是Entity tag的缩写,理解为"被请求变量的实体值",当客户端发送第一次请求时,作为服务端会返回当前资源的标识码。ETag使用得当,是可以减少服务器带宽压力的;
(2)第一次请求
1.客户端发起HTTP GET请求一个文件;
2.服务器处理请求,在HTTP响应头中将ETag: "50b1c1d4f775c61:df3"传送到客户端,状态码200;
(3)第二次请求
1.客户端发起HTTP GET请求一个文件,客户端会将If-None-Match: "50b1c1d4f775c61:df3"添加到请求头;
2.服务端收到第二次请求的时候,发现携带了If-None-Match字段,就重新计算服务器对应资源的Etag。如果不同,则说明资源已经被修改,则返回200;如果相同则说明资源没有被修改,返回304,客户端可以继续使用缓存;
6.2 缓存策略
6.2.1 缓存策略的基本流程
Okhttp的缓存策略就是根据上述流程图实现的,具体的实现类是CacheStrategy,CacheStrategy的构造函数里有两个参数:
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest; // 网络请求
this.cacheResponse = cacheResponse; // 缓存响应,基于DiskLruCache实现的文件缓存,可以是请求中url的md5,value是文件中查询到的缓存
}
6.2.2 构建缓存策略
1、缓存策略是利用工厂模式进行构造的,CacheStrategy.Factory对象构建以后,调用它的get()方法即可获得具体的缓存策略。get()方法内部调用的是getCandidate()方法,它的核心实现如下所示:
public static class Factory {
public CacheStrategy get() {
CacheStrategy candidate = getCandidate(); // getCandidate()
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
private CacheStrategy getCandidate() {
// 1. 如果没有缓存,则返回:使用网络、没有缓存
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 2. 如果是Https请求的握手信息丢失,则返回:使用网络、没有缓存
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 3. 如果请求头有"no-store",会阻止响应被缓存,则返回:使用网络、没有缓存;("no-store" 指“无存储”)
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
// 4. 如果请求头有"no-cache"或者"If-Modified-Since"/"If-None-Match",则返回:使用网络、没有缓存;("no-cache"指使用对比缓存)
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
// 计算当前age的时间戳:now - sent + age
long ageMillis = cacheResponseAge();
// 刷新时间,一般服务器设置为max-age
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
// 一般取max-age
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
// 一般取0
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
// 5. 如果请求头没有"no-cache",并且缓存没过期,则返回:不使用网络、有缓存;(强制缓存)
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// 6. 如果缓存已过期,并且缓存的响应头包含"ETag"或"Last-Modified",就将If-None-Match: "50b1c1d4f775c61:df3"
// 或If-Modified-Since: "Tue, 12 Jan 2016 09:31:27 GMT"添加到请求头,则返回:使用网络,有缓存
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
}
2、整个函数的逻辑就是按照上面HTTP缓存判定流程图来实现,具体流程如下所示:new CacheStrategy(request, null) == 不使用网络请求、没有缓存
(1)如果没有缓存,则返回:使用网络、没有缓存;
(2)如果是Https请求的握手信息丢失,则返回:使用网络、没有缓存;
(3)如果请求头有"no-store",会阻止响应被缓存,则返回:使用网络、没有缓存;(“no-store” 指“无存储”)
(4)如果请求头有"no-cache"或者"If-Modified-Since"/“If-None-Match”,则返回:使用网络、没有缓存;(“no-cache"指使用对比缓存)
(5)如果请求头没有"no-cache”,并且缓存没过期,则返回:不使用网络、有缓存;(强制缓存)
(6)如果缓存已过期,并且缓存的响应头包含"ETag"或"Last-Modified",就将If-None-Match: "50b1c1d4f775c61:df3"或If-Modified-Since: "Tue, 12 Jan 2016 09:31:27 GMT"添加到请求头,则返回:使用网络,有缓存;
6.3 缓存管理
1、Okhttp的缓存机制是基于DiskLruCache做的,Cache类封装了缓存的实现,实现了InternalCache接口:
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
@Override public @Nullable
Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public @Nullable
CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
final DiskLruCache cache; // DiskLruCache
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize); // DiskLruCache
}
@Nullable
Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
@Nullable
CacheRequest put(Response response) {
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) { // invalidatesCach()可知Post请求不会缓存
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
}
public final class HttpMethod {
public static boolean invalidatesCache(String method) {
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
|| method.equals("DELETE")
|| method.equals("MOVE"); // WebDAV
}
}
2、整个流程如下:
(1)在Cache类里还定义一些内部类,这些类封装了请求与响应信息:
1.Cache.Entry:封装了请求与响应等信息,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis与receivedResponseMillis;
2.Cache.CacheResponseBody:继承于ResponseBody,封装了缓存快照snapshot,响应体bodySource,内容类型contentType,内容长度contentLength;
3.除了两个类以外,Okhttp还封装了一个文件系统类FileSystem类,这个类利用Okio这个库对Java的FIle操作进行了一层封装,简化了IO操作。理解了这些剩下的就是DiskLruCahe里的插入缓存、获取缓存和删除缓存的操作。
(2)默认情况下,使用GET通过HTTP访问的请求可以缓存;而使用HTTPS或Post请求不会缓存,因为实现起来的复杂性很高而收益甚微。那么我们该如何来设计使OKHttp支持POST缓存呢?参考以下链接:关于OkHttp支持Post缓存的解决方案。
(3)基于此在仅支持GET请求的条件下,Okhttp使用request URL作为缓存的key(当然还会经过一系列摘要算法);
(4)如果请求头中包含vary:*这样的头信息也不会被缓存。vary头用于提高多端请求时的缓存命中率,比如两个客户端,一个支持gzip压缩而另一个不支持,二者的请求URL都是一致的,但Accept-Encoding不同,这很容易导致缓存环错乱,我们可以声明vary:Accept-Encoding防止这种情况发生。而包含vary:*头信息,标识着此请求是唯一的,不应被缓存,除非有意为之,一般不会这样做来牺牲缓存性能。
7 Okhttp面试题解答
7.1 如何使用OkHttp进行异步网络请求,并根据请求结果刷新UI?
(1)第一步,创建一个OkHttpClient对象:OkHttpClient mClient = new OkHttpClient.Builder().build();
(2)第二步,创建携带请求信息的Request对象:Request request = new Request.Builder().url(“http://www.baidu.com”).get().build();
(3)第三步,通过request对象去构造一个Call对象,将请求封装成了任务:Call call = mClient.newCall(request);
(4)第四步,调用的是call.enqueue以异步的方式去执行请求,将call加入调度队列;
(5)注意:Callback回调方法执行的线程并不是UI线程,不能直接更新UI,否则会报出异常;
7.2 可否介绍一下OkHttp的整个请求流程?
(1)OkHttpClient发起请求后,将Request封装到接口Call的实现类RealCall中;
(2)同步请求通过调用RealCall.exectute()方法直接返回当前请求的Response,异步请求调用RealCall.enqueue()方法将请求(AsyncCall)发送到Dispatcher的请求队列中去;
(3)Dispatcher不断从请求队列里取出请求(Call);
(4)请求从RetryAndFollowUpInterceptor开始层层传递到CallServerInterceptor,每一层都对请求做相应的处理,处理的结构再从CallServerInterceptor层层返回给RetryAndFollowUpInterceptor,最后请求的发起者通过回调(Callback)获取服务器返回的结果。
7.3 addInterceptor与addNetworkInterceptor有什么区别?
7.3.1 Application interceptors,应用拦截器:在RetryAndFollowUpInterceptor之前,是最先执行的拦截器
(1)只可以监听请求的发起情况(请求头,请求体等),以及最终的响应结果,例如:设置公共参数过滤器;无法操作中间的操作,例如:BridgeInterceptor添加的额外的请求头、If-None-Match;
(2)在任何情况下只会执行一次,即使这个响应来自于缓存;
(3)允许short-circuit (短路),并且允许不去调用Chain.proceed()。(意思是说:Chain.proceed()不需要一定要调用去服务器请求,但是必须还是需要返回Respond实例。那么实例从哪里来?答案是从本地缓存中获取响应实例返回给客户端。这就是short-circuit (短路)的意思);
(4)允许请求失败重试,以及多次调用Chain.proceed()(本地异常重试);
7.3.2 Network Interceptors,网络拦截器:在ConnectInterceptor和CallServerInterceptor之间
(1)允许操作中间响应,监听链路上传输的数据,例如:HttpLoggingInterceptor、当请求操作发生重定向或者重试等;
(2)可能执行多次,因为相当于进行了二次请求;
(3)不允许调用缓存来short-circuit (短路)这个请求,仅可调用一次Chain.proceed()。(意思是说:不能从缓存池中获取缓存对象返回给客户端,必须通过请求服务的方式获取响应,也就是Chain.proceed());
(4)允许Connection对象装载这个请求对象。(Connection是通过Chain.proceed()获取的非空对象);
7.3.3 学习链接
7.4 网络缓存机制是如何实现的?
(1)第一次拿到响应后,根据请求头信息Cache-Control决定是否缓存;
(2)第二次请求时判断是否有本地缓存,是否需要使用对比缓存、封装请求头信息等;
(3)如果有本地缓存,并且缓存没有过期,则使用本地缓存;
(4)如果有本地缓存,并且缓存过期,则需要服务端参与判断是否继续使用缓存(对比缓存)。将第一次请求备份的缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)发送给服务端,服务端根据缓存标识进行判断,如果返回200,表示需要重新请求资源,则读取网络请求结果;如果返回304,客户端可以继续使用本地缓存;
(5)如果网络请求成功,并且请求头没配置no-store,则对请求结果进行缓存;
7.5 网络连接怎么实现复用?
(1)每次的请求都可以理解为一个连接,在复杂的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的。如果连接能够复用的话,就能够很好地解决这个效率问题了;
(2)能够复用的关键就是:客户端和服务端能够保持长连接,并让一个或者多个连接复用。怎么保持长连接呢?在BridgeInterceptor的intercept()方法中request的请求头添加了(“Connection”, “Keep-Alive”)的键值对,即是requestBuilder.header(“Connection”, “Keep-Alive”),这样就能够保持长连接;
(3)复用连接就需要对连接进行管理,这里就引入了连接池ConnectPool的概念,连接池最多保持5个地址的连接keep-alive,每个keep-alive时长为5分钟,并有异步线程清理无效的连接;
7.6 OkHttp如何做网络监控?TODO:结合retrofit设置?
1、思路
(1)为了监测网络状态我们将使用OkHttp Interceptor! 拦截器可以让我们拦截网络请求,并在okhttp其他chain完成前会进行网络检查,监测网络是否可连接然后决定继续请求还是抛出一个自定义RunTimeError异常。
(2)拦截器如何选择?用Application Interceptor,不需要担心每个重定向和中间响应的网络检查。然而,当okhttp从缓存中检索到你的请求时依然会执行没必要的网络检查,这让缓存的作用大打折扣。用Network Interceptor,则相反:okhttp提供缓存响应时不会进行网络检查,然而却需要为重定向和重试进行网络检查。
2、学习链接
使用retrofit+okhttp实现无缝网络状态监测
例子代码:android-okhttp-network-monitor
7.7 OkHttp对于网络请求都有哪些优化,如何实现的?
1、通过连接池来减少请求延时:
(1)每次的请求都可以理解为一个连接,在复杂的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的。如果连接能够复用的话,就能够很好地解决这个效率问题了;
(2)能够复用的关键就是:客户端和服务端能够保持长连接,并让一个或者多个连接复用。怎么保持长连接呢?在BridgeInterceptor的intercept()方法中request的请求头添加了(“Connection”, “Keep-Alive”)的键值对,即是requestBuilder.header(“Connection”, “Keep-Alive”),这样就能够保持长连接;
(3)复用连接就需要对连接进行管理,这里就引入了连接池ConnectPool的概念,连接池最多保持5个地址的连接keep-alive,每个keep-alive时长为5分钟,并有异步线程清理无效的连接;
2、通过缓存机制响应数据来减少重复的网络请求;
3、无缝支持GZIP压缩来减少数据流量;
4、可以从很多常用的连接问题中自动恢复;
7.10 OkHttp框架中都用到了哪些设计模式?
7.10.1 外观设计模式(门户模式)
(1)思想
提供者向使用者提供一个门户,使用者只需访问这个门户就能获取数据,使用者不需要了解内部构成,运行流程等。Dispatcher是OkHttpClient的调度器,是一种门户模式,主要向使用者提供了实现执行、取消同步+异步请求操作,隐藏同步内部的逻辑。
(2)用图说明
(3)简单用例
ReceptionEmployee employee = new ReceptionEmployee();
employee.orderFood();
public class ReceptionEmployee {
private EmployeeOne one;
private EmployeeTwo two;
public void orderFood() {
one.work();
two.work();
getFood();
}
private void getFood() {
System.out.println("001号顾客您的请到前台取餐,谢谢");
}
}
// 同理,EmployeeTwo
public class EmployeeOne {
public void work() {
System.out.println("处理食材");
}
}
7.10.2 建造者模式
(1)思想
建造者模式属于创建型模式的一种,它允许用户在不知道内部构建细节的情况下,可以更精细的控制对象的构造流程。该模式为了将构建复杂对象的过程和它的部件解耦,使构建的过程和部件的表示隔离开来。
(2)简单用例
public class OkHttpClient{
public OkHttpClient() {
this(new Builder());
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
OkHttpClient(Builder builder) {
this.dispatcher = builder.dispatcher;
this.callTimeout = builder.callTimeout;
}
public static final class Builder {
Dispatcher dispatcher;
int callTimeout;
public Builder() {
dispatcher = new Dispatcher();
callTimeout = 0;
}
Builder(OkHttpClient okHttpClient) {
this.dispatcher = okHttpClient.dispatcher;
this.callTimeout = okHttpClient.callTimeout;
}
public Builder callTimeout(long timeout, TimeUnit unit) {
callTimeout = checkDuration("timeout", timeout, unit);
return this;
}
}
}
7.10.3 责任链模式
(1)思想
有多个对象都有机会处理某个请求,从第一个请求开始,都会持有一个引用指向下一个请求(最后一个请求指向null),从而形成一条链,沿着这条链传递请求,直到请求被处理或者传递到最后一个请求结束。
(2)Okhttp的思想
Interceptor将网络请求、缓存、建立连接、从服务器读取响应的数据等功能统一了起来,它的实现采用责任链模式,各司其职,每个功能都是一个Interceptor,上一级处理完成以后传递给下一级,它们最后连接成了一个Interceptor.Chain,开启链式调用,源码分析如下:
final class RealCall implements Call {
// http处理请求是发生在RealCall中excute()处理的
public Response execute() throws IOException {
// ...
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain(); // getResponseWithInterceptorChain()
return result;
} catch (IOException e) {
throw e;
}
}
// getResponseWithInterceptorChain()方法处理用户定义以及框架定义的Interceptor逻辑。
Response getResponseWithInterceptorChain() throws 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));
// 将Interceptor集合、请求对象(注意参数index初始值为0)等参数封装到RealInterceptorChain对象中,
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
// 然后调用该对象的proceed()方法启动整个责任链执行工作。
return chain.proceed(originalRequest); // proceed(originalRequest)
}
}
public final class RealInterceptorChain implements Interceptor.Chain {
@Override
public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection); // proceed(request, streamAllocation, httpCodec, connection)
}
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
// RealInterceptorChain对象的proceed()会将下一个Interceptor封装成新的RealInterceptorChain对象next,
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
Interceptor interceptor = interceptors.get(index);
// 并调用当前Interceptor对象的intercept(next)方法,将新的RealinterceptorChain对象传递给下一个Interceptor,形成处理链条,形成责任链模式
Response response = interceptor.intercept(next);
return response;
}
}
// 以RetryAndFollowUpInterceptor为例
public final class RetryAndFollowUpInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace);
while (true) {
Response response;
try {
response = realChain.proceed(request, streamAllocation, null, null); // // 然后调用该对象的proceed()方法启动下一个拦截器执行工作
} catch (RouteException e) {
continue;
}
}
}
}
调用链接图如下:
(3)优缺点
1.优点:如果不使用责任链模式来处理,结果会怎样?是不是会用大量的if–esle来判断要处理的逻辑?如果有新需求或删除原有需求,是不是还要去大量的if–else代码中找,并且修改?严重违反了Java开发的开闭原则,也不符合类的单一原则。所以OkHttp中这样使用责任链的好处是:①符合开闭原则,拥抱改变,扩展性性更强;②符合类的单一原则,使类结构更加清晰;③用户的可配置性强,可自由定制个性化需求,这对于一个开源框架来说是非常重要的。
2.缺点:性能问题,一个请求必须从头遍历整个链条,直到找到符合要求的处理类,在链条特别长,性能是个很大的问题。;
(4)学习链接
从OkHttp中学习设计模式—责任链模式
7.10.4 工厂模式
(1)思想
工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
(2)Okhttp的思想
CacheStrategy接口提供了内部接口Factory,用于将对象的创建延迟到该工厂类的子类中进行,从而实现动态的配置。
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
public final class CacheStrategy {
public final @Nullable Request networkRequest;
public final @Nullable Response cacheResponse;
public static class Factory {
final long nowMillis;
final Request request;
final Response cacheResponse;
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
}
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
}
(3)简单用例:需要一个汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现
// 产品的抽象类
public interface Shape {
void draw();
}
// 产品(长方形、圆形)的具体实现
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
// 创建一个工厂,生成基于给定信息的实体类的对象。
public class ShapeFactory {
// 使用getShape方法获取形状类型的对象
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
}
return null;
}
}
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
// 获取Circle的对象,并调用它的draw方法
Shape shape1 = shapeFactory.getShape("CIRCLE");
// 调用Circle的draw方法
shape1.draw();
// 获取Rectangle的对象,并调用它的draw方法
Shape shape2 = shapeFactory.getShape("RECTANGLE");
// 调用Rectangle的 draw 方法
shape2.draw();
}
}
(4)优缺点
1.优点:①一个调用者想创建一个对象,只要知道其名称就可以了; ②扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以;③屏蔽产品的具体实现,调用者只关心产品的接口;
2.缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事;
(5)学习链接
工厂模式
7.10.5 策略模式
(1)思想
策略模式表示的是在遇到一种问题有多种解法时,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。正式的概念是:定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换。
(2)Okhttp的思想
CacheStrategy.Factory对象构建以后,调用它的get()方法即可获得具体的缓存策略。缓存策略利用networkRequest与cacheResponse这两个结果,组合生成最终的策略如下所示:(networkRequest为null指:不使用网络请求;cacheResponse为null指:没有缓存)
(1)根据策略,不使用网络,又没有缓存,直接报错并返回错误码504;
(2)根据策略,不使用网络,有缓存,直接返回缓存(强制缓存);
(3)根据策略,使用网络,没有缓存,继续执行下一个Interceptor,即ConnectInterceptor;
(4)根据策略,使用网络,有缓存,需要服务端参与判断是否继续使用缓存(对比缓存)。当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ;客户端第二次请求数据时,将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回200,表示需要重新请求资源,则读取网络请求结果;如果返回304,客户端可以继续使用缓存。
(3)简单用例
public interface TravelStrategy {
void goTravel();
}
public class TrainStrategy implements TravelStrategy {
@Override
public void goTravel() {
System.out.println("乘火车去旅行,火车策略");
}
}
public class HighSpeedStrategy implements TravelStrategy {
@Override
public void goTravel() {
System.out.println("乘高铁去旅行,高铁策略");
}
}
public class AirplaneStrategy implements TravelStrategy {
@Override
public void goTravel() {
System.out.println("乘飞机去旅行,飞机策略");
}
}
public class Traveler {
private TravelStrategy travelStrategy;
public TravelStrategy getTravelStrategy() {
return travelStrategy;
}
public void setTravelStrategy(TravelStrategy travelStrategy) {
this.travelStrategy = travelStrategy;
}
public void goTravel() {
travelStrategy.goTravel();
}
public static void main(String[] args) {
Traveler traveler = new Traveler();
traveler.setTravelStrategy(new TrainStrategy());
traveler.goTravel();
traveler.setTravelStrategy(new HighSpeedStrategy());
traveler.goTravel();
traveler.setTravelStrategy(new AirplaneStrategy());
traveler.goTravel();
}
}
(4)优缺点
1.优点:①我们之前在选择出行方式的时候,往往会使用if-else语句,也就是用户不选择A那么就选择B这样的一种情况。这种情况耦合性太高了,而且代码臃肿,有了策略模式我们就可以避免这种现象;②策略模式遵循开闭原则,实现代码的解耦合,扩展新的方法时也比较方便,只需要继承策略接口就好了。
2.缺点:①客户端必须知道所有的策略类,并自行决定使用哪一个策略类,策略模式会出现很多的策略类;②策略类由于继承了策略接口,所以有些数据可能用不到,但是依然初始化了。
(5)学习链接
设计模式之策略模式(Java实现例子说明)
7.10.6 享元模式
(1)思想
运用共享技术支持重用现有的细粒度同类对象,如果未找到匹配的对象,则创建新对象;目的是为了减少创建对象的数量,以减少内存占用和提高性能。
(2)简单用例
在Dispatcher的线程池中,所用到了享元模式,一个不限容量的线程池,线程空闲时存活时间为60秒。线程池实现了对象复用,降低线程创建开销,从设计模式上来讲,使用了享元模式。
7.11 学习链接
8 学习链接
(1)源码分析文章
Android开源框架源码鉴赏:Okhttp
Android主流三方库源码分析(一、深入理解OKHttp源码)
(2)官网