目录:
1. okhttp做了哪些优化
2. okhttp的线程池逻辑
3. okhttp的5大拦截器
4. okhttp的责任链模式
5. 拦截器之连接拦截器
5. okhttp的链接池复用
6. okhttp的缓存机制
7. OKHttp框架中用到了哪些设计模式?
3.14.x版本及以前的版本,采用Java语言编写,4.0.0以后采用kotlin语言
1. okhttp做了哪些优化
1.1 为什么用Okhttp,而不选择其它网络框架?
关于HTTP2的优点,主要有:
1). 支持HTTP2/SPDY,允许所有同一个主机地址的请求共享同一个Socket连接
(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验)
在早期的版本中,OkHttp支持Http1.0,1.1,SPDY协议,但是Http2协议的问世,导致OkHttp也做出了改变,OkHttp鼓励开发者使用HTTP2,不再对SPDY协议给予支持。另外,新版本的OkHttp还有一个新的 亮点就是支持WebScoket,这样我 们就可以非常方便的建立长连接了
- .多路复用:就是针对同个域名的请求,都可以在同一条连接中并行进行,而且头部和数据都进行了二进制封装。
连接池减少请求延时(socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,减少了请求延迟,共享Socket,减少对服务器的请求次数。
.透明的GZIP压缩减少响应数据的大小(基于Headers的缓存策略减少重复的网络请求), 减少响应数据的大小{{{拥有Interceptors轻松处理请求与响应(自动处理GZip压缩)}}},使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗.重试及重定向
.头部压缩:HTTP1.x每次请求都会携带完整的头部字段,所以可能会出现重复传输,因此HTTP2采用HPACK对其进行压缩优化,可以节省不少的传输流量。
.二进制分帧:传输都是基于字节流进行的,而不是文本,二进制分帧层处于应用层和传输层之间。
简单来说就是:
OkHttp是一个非常优秀的网络请求框架,已被谷歌加入到Android的源码中。
- 支持http2,对一台机器的所有请求共享同一个socket 。2是否支持呢????不是1.2
- 内置连接池,支持连接复用,减少延迟
- 支持透明的gzip压缩响应体
- 通过缓存避免重复的请求
- 请求失败时自动重试主机的其他ip,自动重定向
- 很多设计模式,好用的API。链式调用
面试官:okhttp支持的协议是什么?
支持 http2.0、websocket,spdy 等协议
面试官:android支持http2.0,Okhttp如何开启的Http2.0?
https://blog.csdn.net/weixin_42522669/article/details/117576189
为什么只要后端将接口升级到Http2.0的支持之后,客户端就能自动的把所有的请求切换到Http2.0上呢?
Http2.0的前置条件是实现了https。
有非常好的握手过程
自动的一个过程:ALPN协议,是TLS的扩展,浏览器是基于ALPN协议来判断服务器是否支持HTTP2协议。
当后端支持的协议内包含Http2.0时,则就会把请求升级到Http2.0阶段。
2. 看过OkHttp的源码吗,简单说一下
2.1 三条主线:
第一。分发器(处理高并发请求)
- 1).用法解析 同步和异步 怎么实现的
- 2).okhttp线程池工作原理
- 3).同步队列里面,运行队列和准备怎么工作的
- 4).如何实现并发的?并发控制
- 第二。拦截器(每个拦截器的作用)
- 1).责任链模式
- 2).拦截器 缓存机制 缓存基于DiskLruCache
- 3).自定义拦截器
- 4).多域名如何封装?测试和正式如何封装
- 第三。网络拦截器ConnectionIntercepter原理
- 1).拦截器 socker连接池复用机制详解
- 2) .在无网的时候直接使用缓存,这该怎么做呢?很简单,我们只要自定义一个拦截器,在我们的请求头中判断没有网络可用时,缓存策略为强制使用缓存。
- 3). 缓存机制是怎么样的?网络请求缓存处理,okhttp如何处理网络缓存的?
2. 2 怎么设计一个自己的网络访问框架,为什么这么设计?
同上:并发,处理请求,处理响应,复用
先参考现有的框架,找一个比较合适的框架作为启动点,比如说,基于上面讲到的okhttp的优点,选择okhttp的源码进行阅读,并且将主线的流程抽取出,为什么这么做,因为okhttp里面虽然涉及到了很多的内容,但是我们用到的内容并不是特别多;保证先能运行起来一个基本的框架;
考虑拓展,有了基本框架之后,我会按照我目前在项目中遇到的一些需求或者网路方面的问题,看看能不能基于我这个框架进行优化,比如服务器它设置的缓存策略,
我应该如何去编写客户端的缓存策略去对应服务器的,还比如说,可能刚刚去建立基本的框架时,不会考虑HTTPS的问题,那么也会基于后来都要求https,进行拓展;
3. okhttp的线程池逻辑
3.1 问题: okhttp如何处理并发的?
同步和异步 同步1个队列,异步2个队列
3队列 64链接 5 host
异步请求处理:队列是否满,否则假如到准备队列。然后线程池执行,通过5大拦截器返回响应,处理完成,移除 。然后判断准备队列大小。添加到正在执行的队列里面
面试官:为什么既要队列又要线程池?下载文件为啥不用?
队列:64总量,最多存放 。线程池:控制同时并非的个数,处理完从队列取
下载文件:只用了队列+协程。处理了一个用下一个
其中Dispatcher这个类,有一个线程池,用于执行异步的请求.并且内部还维护了3个双向任务队列,3个就是上面说的
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
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<>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
2个队列:重点一:
同一个服务器地址不超过5个
如果这个AsyncCall请求符合条件(判断实际的运行请求数是否小于允许的最大的请求数量(64) 并且共享主机的正在运行的调用的数量小于同时最大的相同Host的请求数(5)) 才会添加到执行异步请求队列,然后通过线程池进行异步请求否则就把这个AsyncCall请求添加到就绪(等待)异步请求队列当中
如果都符合就把请求添加到正在执行的异步请求队列当中,然后通过线程池去执行这个请求call,否则的话在就绪(等待)异步请求队列当中添加
SynchronousQueue:经典的生产者-消费者模式
不像ArrayBlockingQueue、LinkedBlockingDeque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问
面试官:为什么要异步用两个队列呢?
因为Dispatcher默认支持最大的并发请求是64个,单个Host最多执行5个并发请求,
Call会先被放入到readyAsyncCall中,当出现空闲的线程时,再将readyAsyncCall中的线程移入到runningAsynCalls中,执行请求。先看Dispatcher的流程
面试官:大于64链接之后,准备都队列是如何处理的?准备队列是如何加入到运行队列里面?
通过结束后再来一次,处理完成一个请求之后,会在finlly调用,finish方法
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");//将请求移除集合
if (promoteCalls) promoteCalls();
...
}
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
通过拦截器链得到Response,然后通过重定向拦截器判断是否取消,取消调用callBack的失败方法,没有取消就直接返回结果
最后无论是否取消,都会调用dispatcher的finish方法,后面会讲到
同步请求总结:在哪个线程回调,就在哪个线程处理。
面试官:什么场景用同步请求?
举例:分片上传
3.2 面试官:okhttp线程池工作原理是怎样的?
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;
}
在Okhttp中,构建了一个核心为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒
线程池execute,其实就是要执行线程的run方法有封装了一层,最终看
AsyncCall的excute方法:
@Override protected void execute() {
boolean signalledCallback = false;
try {
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) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
同步和异步总结:
对于同步和异步请求,唯一的区别就是异步请求会放在线程池中去执行,而同步请求则会在当前线程中执行,注意:同步请求会阻塞当前线程。
同步请求可以得到respones,异步请求通过回调得到。最终都是经过拦截器
4. okhttp的5大拦截器
面试官:okhttp的拦截器是在哪个类添加的?
RealCall,它返回相应Respond
面试官:响应是如何得到
通过拦截器
4.1. 面试官:okhttp的拦截器是怎么理解的?
1.拦截器主要处理2个东西,request和respond.可以看看源码的拦截器,拦截器主要用来观察,修改以及可能短路的请求输出和响应的回来。
先介绍一个比较重要的类:RealInterceptorChain,直译就是拦截器链类
Response result = getResponseWithInterceptorChain();
没错,在RealCall类的 getResponseWithInterceptorChain();方法中我们就用到了这个RealInterceptorChain类。
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.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
4.2. 自定义拦截器
但是看上面这段源码发现好像并不是只有这五种对吧!有两句代码,分别是addAll方法,添加了两个集合,集合存储的是啥?
这里其实是自定义拦截器,可以看到,自定义拦截器添加的顺序分别又有两种,根据顺序分别叫做:Application Interceptors和Network Interceptors
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));
面试官:自定义过哪些拦截器?
自定义拦截器:通常情况下拦截器用来添加,移除或者转换请求或者响应的头部信息。比如将域名替换为ip地址,将请求头中添加host属性,也可以添加我们应用中的一些公共参数,比如设备id、版本号等等
okhttp拦截器主要在以下几种情况使用:
自定义拦截器不一定要继承基本的5大拦截器,而是继承Interceptor
网络请求、响应日志输出
在Header中统一添加cookie、token
设置网络缓存
1.添加日志拦截器
可以用系统的,或者通过添加自己写的拦截器
2.在Header中统一添加cookie、token
public class HeaderInterceptor implements Interceptor
面试官:我想要打印请求的一些信息以及返回的一些信息怎么处理 ?
答:这个我们可以利用上面所说的拦截器来实现,通过OkHttp的建造者模式构建OkHttpClient时的一个addInterceptor方法添加一个自定义拦截器,实现Interceptor的intercept方法,利用Chain对象可以获取到Request信息以及Response信息。
4.3 5大拦截器的分别介绍
4.3.1 RetryAndFollowUpInterceptor(重试,重定向拦截器,code:301,302)
他处于责任链的顶端,负责网络请求的开始工作,也负责收尾的工作。
常用的重定向方式有:301 redirect、302 redirect与meta fresh。 用来实现连接失败的重试和重定向
RetryAndFollowUpInterceptor拦截设置最大重定向次数为20次;不同情况不一样
/**
* How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
* curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
private static final int MAX_FOLLOW_UPS = 20;
一开始,创建了StreamAllocation对象,他封装了网络请求相关的信息:连接池,地址信息,网络请求,事件回调,负责网络连接的连接、关闭,释放等操作
followUpCount是用来记录我们发起网络请求的次数的,为什么我们发起一个网络请求,可能okhttp会发起多次呢?
例如https的证书验证,我们需要经过:发起 -> 验证 -> 响应,三个步骤需要发起至少两次的请求,或者我们的网络请求被重定向,在我们第一次请求得到了新的地址后,再向新的地址发起网络请求。(发现迁移到新地址,访问新的服务器地址!)
面试官:重试机制是怎么样的?
里面有个while(true)循环。通过continius和return退出循环
在网络请求中,不同的异常,重试的次数也不同,okhttp捕获了两种异常:RouteException和IOException。
RouteException:所有网络连接失败的异常,包括IOException中的连接失败异常;
IOException:除去连接异常的其他的IO异常。
这个时候我们需要判断是否需要重试:
其中的路由地址我们先忽略,这个之后我们还会讨论。假定没有其他路由地址的情况下:
1)、连接失败,并不会重试;
2)、如果连接成功,因为特定的IO异常(例如认证失败),也不会重试
其实这两种情况是可以理解的,如果连接异常,例如无网络状态,重试也只是毫秒级的任务,不会有特别明显的效果,如果是网络很慢,到了超时时间,应该让用户及时了解失败的原因,如果一味重试,用户就会等待多倍的超时时间,用户体验并不好。认证失败的情况就更不用多说了。
3). 默认重连3次?
不是,通过一系列的判断,recover ()方法.达到一定条件
如果我们非要重试多次怎么办?
自定义Interceptor,增加计数器,重试到你满意就可以了: 通过 recover 方法检测该 RouteException 是否能重新连接;
总结:只有在特定情况下,okhttp才会重试
4.3.2 BridgeInterceptor(桥接拦截器)
用来修改请求和响应的 header 信息,Zip压缩配置
负责设置编码方式,添加头部,Keep-Alive 连接以及应用层和网络层请求和响应类型之间的相互转换
public final class BridgeInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder()
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
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);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
}
4.3.3 CacheInterceptor(缓存拦截器)
http的缓存机制:根据 缓存是否过期、过期后是否有修改(1.缓存,2.etag 3. lastModify)
用来实现响应缓存。比如获取到的 Response 带有 Date,Expires,Last-Modified,Etag 等 header,表示该 Response 可以缓存一定的时间,下次请求就可以不需要发往服务端,直接拿缓存的。一些名词如下:
1).Etag是 Entity tag的缩写,可以理解为“被请求变量的实体值” ,它是关于缓存的一个字段,每次请求都会存在的一个标识符,将文本哈希编码来标识当前文本的状态
2).Last-Modified字段是否存在,这个字段表示响应中资源最后一次修改的时间
3). 304: Not Modified 客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。
面试官:okhttp的缓存是怎么样的?网络请求缓存处理,okhttp 如何处理网络缓存的?
在缓存拦截器里面。
总结: 没有缓存 ------> 直接请求服务器
eTag------>Last-Modified没过期,返回304--用本地
eTag------>Last-Modified过期-------->请求服务器
没有eTag-------------------------------------->请求服务器
第一: 网络缓存优先考虑强制缓存,再考虑对比缓存
<1>--有缓存的情况下,首先判断强制缓存中的数据的是否在有效期内。如果在有效期,则直接使用缓存。如果过了有效期,则进入对比缓存。(Date)
还需要强调一点,虽然缓存已经过期了,但是并非缓存与服务器的内容不同,比如服务端的数据并未做出任何更改,说明此时缓存的依旧是最新数据!所以还需要更详细的判断再来决定是否需要请求服务器更新数据,所以,避免了不必要的请求,这种缓存机制很大程度上减轻了服务器的压力!
第二:对比缓存
<2>----在对比缓存过程中,判断ETag 是否有变动如果服务端返回没有变动,说明资源未改变,使用缓存。如果有变动,判断Last-Modified。(Etag)
判断Last-Modified,如果服务端对比资源的上次修改时间没有变化,则使用缓存,否则重新请求服务端的数据,并作缓存工作(Last-modified)
结果处理缓存:最后如果服务器返回304,我们要直接使用缓存(304)HTTP_NO_MODIFIY
public static final int HTTP_NOT_MODIFIED = 304;
private static boolean validate(Response cached, Response network) {
if (network.code() == 304) {
return true;
} else {
Date lastModified = cached.headers().getDate("Last-Modified");
if (lastModified != null) {
Date networkLastModified = network.headers().getDate("Last-Modified");
if (networkLastModified != null && networkLastModified.getTime() < lastModified.getTime()) {
return true;
}
}
return false;
}
}
if (networkRequest == null && cacheResponse == null) {
return (new Builder()).request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_BODY).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();
} else if (networkRequest == null) {
return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
} else {
重点分析:面试官:okhttp是如何处理304的?
1).此时如果服务器返回的响应码为HTTP_NOT_MODIFIED,也就是我们常见的304,代表服务器的资源没有变化,客户端去取本地缓存即可,此时服务器不会返回响应体
private static boolean validate(Response cached, Response network) {
if (network.code() == 304) {
return true;
} else {
Date lastModified = cached.headers().getDate("Last-Modified");
if (lastModified != null) {
Date networkLastModified = network.headers().getDate("Last-Modified");
if (networkLastModified != null && networkLastModified.getTime() < lastModified.getTime()) {
return true;
}
}
return false;
}
}
2).直接使用networkResponse构建response并返回。此时我们还需要做一件事,就是更新我们的缓存,将最终response写入到cache对象中去.。通过okio。而OkHttp对Socket的读写操作使用的OkIo库进行了一层封装。
面试官 :okhttp为什么会使用okio而不是用普通io?
OkIO增强了流于流之间的互动,使得当数据从一个缓冲区移动到另一个缓冲区时,可以不经过copy能达到。
1. 在 Buffer 之间传输数据,建立在 Java IO, Java NIO 和 Socket 之上的,补充了java.io和java.nio的不足
2. 速度快
okio采用了segment机制进行内存共享,极大减少copy操作带来的时间消耗,加快了读写速度
okio引入ByteString使其在byte[]与String之间转换速度非常快(ByteString内部以两种变量记录了同个数据byte[] data; transient String utf8;),空间换时间
3 内存消耗小
segmentPool:通过一个链表环加上一个缓冲池来管理,okio的segement机制进行内存复用,上传大文件时完全不用考虑OOM
虽然okio在byteString采用空间换时间,但是对内存也做极致优化,总体还是极大提高了性能
4. 稳定
okio提供了超时机制,不仅在IO操作上加上超时的判定,包括close,flush之类的方法中都有超时机制
总结:
内部所有的操作都要经过buffer缓冲区处理,而缓冲区内部管理细粒度更加细小的Segment,是通过一个链表环加上一个缓冲池来管理,这样就能更大限度的使用内存,同时避免了过多的缓存对象生成。
https://www.jianshu.com/p/5061860545ef(牛逼)
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
// Some apps return a null body; for compatibility we treat that like a null cache request.
if (cacheRequest == null) return response;
Sink cacheBodyUnbuffered = cacheRequest.body();
if (cacheBodyUnbuffered == null) return response;
final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
};
CacheStrategy是其中的缓存策略,CacheControl:CacheStrategy是里面的一个类
Cache类:里面包含了DiskLruCache(文件化的LRU 缓存类)。主要用于添加,移除,更新。类似conectionPool连接池!
回到OkHttp,CacheInterceptor拦截器处理的逻辑,其实就是上面所说的HTTP缓存逻辑,注意到OkHttp提供了一个现成的缓存类Cache,它采用DiskLruCache实现缓存策略,至于缓存的位置和大小,需要你自己指定。
自己新增拦截器,自行实现缓存的管理。
解决办法:添加拦截器:
/**
* 有网时候的缓存
*/
final Interceptor NetCacheInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
int onlineCacheTime = 30;//在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0
return response.newBuilder()
.header("Cache-Control", "public, max-age="+onlineCacheTime)
.removeHeader("Pragma")
.build();
}
};
/**
* 没有网时候的缓存
*/
final Interceptor OfflineCacheInterceptor = new Interceptor() {
@Override
4.3.4.ConnectInterceptor负责与服务器建立链接:很重要和服务器通信
我们发现目前为止我们还没有进行真正的请求。别急,ConnectInterceptor就是一个负责建立http连接的拦截器
这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec。
1). 用来打开到服务端的连接。其实是调用了 StreamAllocation 的newStream 方法来打开连接的。建联的 TCP 握手,TLS 握手都发生该阶段。过了这个阶段,和服务端的 socket 连接打通
2). 在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。
需要看下这个源码:ConnectInterceptor的intercept()方法
4.3.5 CallServerInterceptor是拦截器链中最后一个拦截器,负责将网络请求提交给服务器
它与服务器进行数据交换:主要的工作就是把请求的Request写入到服务端,而后从服务端读取Response。
(1)、写入请求头
(2)、写入请求体
(3)、读取响应头
(4)、读取响应体
用来发起请求并且得到响应。上一个阶段已经握手成功,HttpStream 流已经打开,所以这个阶段把 Request 的请求信息传入流中,并且从流中读取数据封装成 Response 返回
面试官:okhttp是如何保证通信安全的?
面试官:看下okhttp如何封装https
面试官:okhttp如何方法https里面怎么处理SSL?
RealConnection里面,connectTls方法:
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
// block for session establishment
SSLSession sslSocketSession = sslSocket.getSession();
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
if (!peerCertificates.isEmpty()) {
X509Certificate cert = (X509Certificate) peerCertificates.get(0);
throw new SSLPeerUnverifiedException(
"Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
} else {
throw new SSLPeerUnverifiedException(
"Hostname " + address.url().host() + " not verified (no certificates)");
}
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
public final class Address {
final HttpUrl url;
final Dns dns;
final SocketFactory socketFactory;
final Authenticator proxyAuthenticator;
final List<Protocol> protocols;
final List<ConnectionSpec> connectionSpecs;
final ProxySelector proxySelector;
final @Nullable Proxy proxy;
final @Nullable SSLSocketFactory sslSocketFactory;
final @Nullable HostnameVerifier hostnameVerifier;
final @Nullable CertificatePinner certificatePinner;
简单的说Okhttp就是抽象了下所有Tls,SSLSocket相关的代码,然后通过一个Platform,根据当前使用环境的不同,去反射调用不同的实现类,然后这个抽象的类去调用Platform的实现类代码,做到多平台的兼容。
其中Tls当生成好SSLSocket之后,就会开始进行client say hello 和server say hello的操作了,这部分完全和https定义的一模一样。Handshake则会把服务端支持的Tls版本,加密方式等都带回来,然后会把这个没有验证过的HandShake用X509Certificate去验证证书的有效性。然后会通过Platform去从SSLSocket去获取ALPN的协议支持信息,当后端支持的协议内包含Http2.0时,则就会把请求升级到Http2.0阶段。
1).客户端默认信任全部证书
自定义X509TrustManager的形式实现来规避所有的证书检测
然后将其放入okhttp的sslSocketFactory中。
private OkHttpClient getHttpsClient() {
OkHttpClient.Builder okhttpClient = new OkHttpClient().newBuilder();
//信任所有服务器地址
okhttpClient.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//设置为true
return true;
}
});
//创建管理器
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
} };
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
//为OkHttpClient设置sslSocketFactory
okhttpClient.sslSocketFactory(sslContext.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
return okhttpClient.build();
}
总结:在okhttpClient设置SSLsocketFactory。
在RealConnection的时候获取SSLsocketFactory.然后进行TLS的访问
会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP,此外OkHttp还处理了代理服务器问题和SSL握手失败问题。
2).Okhttp验证本地证书(certificate pinning),cer 和 pem 格式都可以
首先将下载的证书srca.cer放到工程的assets文件夹下。
然后读证书得到流
最后设置sslSocketFactory
OkHttpClient httpClient = new OkHttpClient().newBuilder()
.sslSocketFactory(getSLLContext().getSocketFactory())
.build();
private SSLContext getSLLContext() {
SSLContext sslContext = null;
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
InputStream certificate = mContext.getAssets().open("gdroot-g2.crt");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
String certificateAlias = Integer.toString(0);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
sslContext = SSLContext.getInstance("TLS");
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
} catch (CertificateException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return sslContext;
}
5. okhttp的责任链模式
5.1 .责任链模式:(递归调用)循环调用,通过不停的调用自己。然后最后退出
面试官:责任链模式是怎么样的?
责任链模式的类似情况如下:
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index + 1, request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
每个拦截器负责一个特殊的职责.最后那个拦截器负责请求服务器,然后服务器返回了数据再根据这个拦截器的顺序逆序返回回去,最终就得到了网络数据.
责任链模式特点:一个对象持有下个对象的引用
请求:通过5个拦截器,把一个一个请求拼接起来
结果:最后一个先响应,先进后出的原理。
举一个列子:出现弹框,然后一个个关闭。
最后通过 拦截器得到响应的结果respond
可以看到这里的拦截链使用的非常巧妙,有点像栈的数据结构。依次将各个拦截器的方法入栈,最后得到response,再依次弹栈。如果是我来写的话,可能就直接一个for循环依次调用每个拦截器的拦截方法。但是这样的话我还得再来一遍反循环,再来依次处理加工response。很明显这里栈的结构更符合我们的业务场景。
面试官:为什么用责任链?
OkHttp的这种拦截器链采用的是责任链模式,这样的好处是将请求的发送和处理分开,并且可以动态添加中间的处理方实现对请求的处理、短路等操作
不知你有没有发现,这一过程 和 公司工作生产流程 很像:
老板接到一笔订单,要求10天内生产100台电脑。
总经理拿到任务后,修改了任务和时间:8天内生产110台,这是基于 生产合格率 以及进行重工、检验、包装、运输的时间上的考量,既要保质保量,也要按时交货。
任务接着到了部门经理,部门经理先去确认了仓库中是否有足够存货,如果有就直接使用存货来交货,这样不存在任何交货风险(质量、时间);如果没有存货,那么就去要求生产线生产。
生产线按时按量生产完以后,会把生产情况 上报给部门经理,部门经理把结果总结成excel呈现给总经理,总经理则会把整个生产流程结果及各部门的配合情况,总结成PPT报告给老板。
6. 拦截器之连接拦截器(最重要的一个拦截器 )
7. okhttp的链接池复用
面试官:一个 TCP 连接可以对应几个 HTTP 请求?
一个tcp连接的存活时间是大于Http请求的,所以一个Tcp可以对应多个Http请求。就是所谓的多路复用
面试官:为什么一个引用有多个链接?
因为同一个域名可以有多个链接。
复用发生在哪个阶段?或者哪个拦截器的时候?
第四个,连接拦截器。
7.1 面试官:多路复用和连接池多路复用是一样的么?
一个是socket复用,一个是nio复用
7. 2 面试官:okhttp是如何实现连接池复用的?
Socket:
OkHttp的底层是通过Java的Socket发送HTTP请求与接受响应的(这也好理解,HTTP就是基于TCP协议的),但是OkHttp实现了连接池的概念,
KeepAlive
当然大量的连接每次连接关闭都要三次握手四次分手的很显然会造成性能低下,因此http有一种叫做keepalive connections的机制,
它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手
没有复用 keep-alive会导致每次请求都需要重新进行 DNS解析,3次握手4次挥手操作,这样是非常浪费性能的,
默认的是5个空闲TCP接连,并且活跃时间为5分钟。Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)。
面试官:OkHttp中ConnectionPool的实现原理。怎么判断connection可复用?
即对于同一主机的多个请求,其实可以公用一个Socket连接,而不是每次发送完HTTP请求就关闭底层的Socket,这样就实现了连接池的概念。
即连接池,用于管理http1.1/http2.0连接重用,以减少网络延迟。相同Address的http请求可以共享一个连接,ConnectionPool就是实现了连接的复用。
总结来说: 同一端口和同一域名。socket复用。减少3次握手,4次挥手。连接池源码分析。
连接拦截器使用了5种方法查找连接
- 首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)
- 若没有 已分配的可用连接,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:
address
一致——host
、port
、代理等一致,且匹配的连接可以接受新的请求。 -
若从连接池没有获取到,则传入****
routes
****再次尝试获取(路由信息),这主要是针对****Http2.0
****的一个操作,Http2.0
可以复用square.com
与square.ca
的连接 - 若第二次也没有获取到,就创建
RealConnection
实例,进行TCP + TLS
握手,与服务端建立连接。 - 此时为了确保
Http2.0
连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。 - 第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。
优化思想:
是同一个域名下连接都可以复用,服务器和PC浏览器同一个域名下只能建立5个TCP 连接,为了让同一个网页中的图片快速加载,所以要把图片放到不同的域名下,这样就可以实现>5个的连接请求。
源码中同一域名下默认是5个TCP接连,超过后会等待(这个是分发器要求的5)
不同域名下最多64个请求,但是大部分时候同一个域名比较多
请求队列RealConnection
private final Deque<RealConnection> connections = new ArrayDeque<>();
Deque<RealConnection>,双向队列,双端队列同时具有队列和栈性质,双端队列中的元素可以从两端弹出,插入和删除操作限定在队列的两边进行
普通队列是限制级的一端进,另一端出的FIFO形式,栈是一端进出的LIFO形式,而双端队列就没有这样的限制级,也就是我们可以在队列两端进行插入或者删除操作
数据结构:数组实现
它经常在缓存中被使用,里面维护了RealConnection也就是socket物理连接的包装。
面试官:为什么是arrayDeque,这个队列,有什么好处
说到这LinkedList表示不服,LinkedList同样也实现了Deque接口,内部是用链表实现的双端队列,那为什么不用LinkedList呢?
实际上这与readyAsyncCalls向runningAsyncCalls转换有关,当执行完一个请求或调用enqueue方法入队新的请求时,会对readyAsyncCalls进行一次遍历,将那些符合条件的等待请求转移到runningAsyncCalls队列中并交给线程池执行。尽管二者都能完成这项任务,
原因:但是由于链表的数据结构致使元素离散的分布在内存的各个位置,CPU缓存无法带来太多的便利,
另外在垃圾回收时,使用数组结构的效率要优于链表。
作为队列使用时由于 ArrayDeque 性能比 LinkedList 更快【速度会更快】
问题: 为什么 ArrayDeque 性能比 LinkedList 更快呢?
因为 ArrayDeque 是用循环数组来实现的,LinkedList 是用链表实现的,增删改查的操作都比链表高(删除操作比链表高吗?如果是删除数组中间的某个元素,不是的;如果是当成栈或队列使用的场景下,是的。)。
具体的流程:
ConnectionPool封装了一个RealConnectionPool。里面是主要实现,ConnectionPool只是入口而已
ConnectionPool提供对Deque<RealConnection>进行操作的方法分别为put、get、connectionBecameIdle、evictAll几个操作。分别对应放入连接、获取连接、移除连接、移除所有连接操作。
1). ConnectionPool内部以队列方式存储连接
2). 连接池最多维持5个连接,且每个链接最多活5分钟
3). 每次添加链接的时候回执行一次清理任务,清理空闲的链接(RealConnection)。
4). 在ConnectionPool中维护了一个线程池,来进行回收和复用;connections是一个记录连接的双端队列;routeDatabase是记录路由失败的线路,cleanupRunnable是用来进行自动回收连接的。
5). ConnectionPool: 连接池,类似于CachedThreadPool,需要注意的是这种线程池的工作队列采用了没有容量的SynchronousQueue
static {
executor = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));
}
7. 3 面试官:okhttp是如何实现清理无效的连接?
源码在ConnectionPool:
# RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}
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();
//若连接正在使用,continue,正在使用连接数+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空闲连接数+1
idleConnectionCount++;
// 赋值最长的空闲时间和对应连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//连接没有空闲的,就5分钟后再尝试清理.
return keepAliveDurationNs;
} else {
// 没有连接,不清理
cleanupRunning = false;
return -1;
}
}
//关闭移除的连接
closeQuietly(longestIdleConnection.socket());
//关闭移除后 立刻 进行下一次的 尝试清理
return 0;
}
- 1). 在将连接加入连接池时就会启动定时任务 (定时)* 2). 有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。* 3). 没有空闲连接就等5分钟后再尝试清理。* 没有连接不清理。
总结:清理闲置连接的核心主要是引用计数器List<Reference<StreamAllocation>> 和 选择排序的算法以及excutor的清理线程池。
复用第一要素:ConnectionPool 要点(对链接队列进行添加,移除操作)
复用第二要素:RealConnection 要点
复用第三要素:StreamAllocation 要点(计数对象 )
复用第四要素:Connection 要点, 里面包含:路由,连接,握手,协议
复用第五要素:RouteDatabase
8. okhttp的缓存机制
9. OKHttp框架中用到了哪些设计模式?
构建者模式:OkHttpClient与Request的构建都用到了构建者模式
外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置
享元模式: 享元模式的核心即池中复用,OKHttp复用TCP连接时用到了连接池,同时在异步请求中也用到了线程池
面试官:OkHttp中为什么使用构建者模式?
使用多个简单的对象一步一步构建成一个复杂的对象;
优点: 当内部数据过于复杂的时候,可以非常方便的构建出我们想要的对象,并且不是所有的参数我们都需要进行传递;
缺点: 代码会有冗余
好封装
面试官:OkHttp中模板方法设计模式是怎样的?
10. OKHttp框架中的相关问题
面试官:okhttp,在访问一个界面没有结束,关闭activity。还是会,这个要验证一下,那么下一次进去重新访问接口还是--------
网络请求如何取消?底层是怎么实现的
还是会执行
面试官:多域名如何封装?测试和正式如何封装?
面试官:你是怎么封装okhttp的?
https://blog.csdn.net/weimingjue/article/details/88528373
1).证书
2). 请求头,公共参数
3).请求体,传入的参数
4).错误码的封装
5).请求日志监控---拦截器
6).线程切换,响应之后的回调
7). 数据解析