OKHttp 请求的整体流程是怎样的?
OKHttp 分发器是怎样工作的?
OKHttp 拦截器是如何工作的?
应用拦截器和网络拦截器有什么区别?
OKHttp 如何复用 TCP 连接?
OKHttp 空闲连接如何清除?
OKHttp 有哪些优点?
OKHttp 框架中用到了哪些设计模式?
1. OKHttp请求整体流程介绍
首先来看一个最简单的 Http 请求是如何发送的。
复制
val okHttpClient = OkHttpClient()
val request: RequestRequest = Request.Builder()
.url("https://www.google.com/")
.build()
okHttpClient.newCall(request).enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
}
})
这段代码看起来比较简单,OkHttp 请求过程中最少只需要接触 OkHttpClient、Request、Call、 Response,但是框架内部会进行大量的逻辑处理。
所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。关于分发器与拦截器,我们在这里先简单介绍下,后续会有更加详细的讲解
分发器:内部维护队列与线程池,完成请求调配;
拦截器:五大默认拦截器完成整个请求过程。
整个网络请求过程大致如上所示
通过建造者模式构建 OKHttpClient 与 Request
OKHttpClient 通过 newCall 发起一个新的请求
通过分发器维护请求队列与线程池,完成请求调配
通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作
得到网络请求结果
其他版本:
(1)、当我们通过OkhttpClient创立一个okHttpClient 、Request 、Call,并发起同步或者异步请求时;
(2)、okhttp会通过Dispatcher对我们所有的Call(RealCall实现类)进行统一管理,并通过execute()及enqueue()方法对同步或者异步请求进行执行;
(3)、execute()及enqueue()这两个方法会最终调用RealCall中的getResponseWithInterceptorChain()方法,从阻拦器链中获取返回结果;
(4)、拦截器链中,依次通过ApplicationInterceptor(应用拦截器)、RetryAndFollowUpInterceptor(重定向阻拦器)、BridgeInterceptor(桥接阻拦器)、CacheInterceptor(缓存阻拦器)、ConnectInterceptor(连接阻拦器)、NetwrokInterceptor(网络拦截器)、CallServerInterceptor(请求阻拦器)对请求依次处理,与服务的建立连接后,获取返回数据,再经过上述阻拦器依次解决后,最后将结果返回给调用方。
2. OKHttp分发器是怎样工作的?
分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表, 等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求
而这里同步请求各异步请求又略有不同
同步请求
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可
异步请求
synchronized void enqueue(AsyncCall call) {
//请求数最大不超过64,同一Host请求不能超过5个
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
当正在执行的任务未超过最大限制64,同时同一 Host 的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。每个任务完成后,都会调用分发器的 finished 方法,这里面会取出等待队列中的任务继续执行
3. OKHttp拦截器是怎样工作的?
经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了
复制
# RealCall
@Override
public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.timeoutEnter();
transmitter.callStart();
try {
client.dispatcher().executed(this);
return getResponseWithInterceptorChain();
} finally {
client.dispatcher().finished(this);
}
}
我们再来看下 RealCall的execute方法,可以看出,最后返回了 getResponseWithInterceptorChain ,责任链的构建与处理其实就是在这个方法里面
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
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, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
如上所示,构建了一个 OkHttp 拦截器的责任链
责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。
如上所示责任链添加的顺序及作用,如下表所示:
拦截器 |
作用 |
应用拦截器(app自定义的拦截器) |
拿到的是原始请求,可以添加一些自定义 header、通用参数、参数加密、网关接入等等。 |
RetryAndFollowUpInterceptor |
处理错误重试和重定向 |
BridgeInterceptor |
应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。 |
CacheInterceptor |
缓存拦截器,如果命中缓存则不会发起网络请求。 |
ConnectInterceptor |
连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。 |
networkInterceptors(网络拦截器) |
用户自定义拦截器,通常用于监控网络层的数据传输。 |
CallServerInterceptor |
请求拦截器,在前置准备工作完成后,真正发起了网络请求。 |
我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到 CallServerInterceptor的intercept 方法,此方法会将网络响应的结果封装成一个 Response 对象并 return。之后沿着责任链一级一级的回溯,最终就回到 getResponseWithInterceptorChain 方法的返回,如下图所示:
4. 应用拦截器和网络拦截器有什么区别?
从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置 request 属性后的原始请求,而网络拦截器位于 ConnectInterceptor 和 CallServerInterceptor 之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别
1. 首先,应用拦截器在 RetryAndFollowUpInterceptor 和 CacheInterceptor 之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在 CacheInterceptor 中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
2. 其次,除了 CallServerInterceptor 之外,每个拦截器都应该至少调用一次 realChain.proceed 方法。实际上在应用拦截器这层可以多次调用 proceed 方法(本地异常重试)或者不调用 proceed 方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次 proceed 方法。
3. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。
5. OKHttp如何复用TCP连接?
ConnectInterceptor 的主要工作就是负责建立 TCP 连接,建立 TCP 连接需要经历三次握手四次挥手等操作,如果每个 HTTP 请求都要新建一个 TCP 消耗资源比较多 而 Http1.1 已经支持 keep-alive ,即多个 Http 请求复用一个 TCP 连接,OKHttp 也做了相应的优化,下面我们来看下 OKHttp 是怎么复用 TCP 连接的
ConnectInterceptor 中查找连接的代码会最终会调用到 ExchangeFinder.findConnection 方法,具体如下:
# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
// 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
releasedConnection = transmitter.connection;
result = transmitter.connection;
if (result == null) {
// 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
result = transmitter.connection;
}
}
}
synchronized (connectionPool) {
if (newRouteSelection) {
//3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
// 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
synchronized (connectionPool) {
// 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
//意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果获取到,就关闭我们创建里的连接,返回获取的连接
result = transmitter.connection;
} else {
//最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
connectionPool.put(result);
}
}
return result;
}
上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接:
首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)
若没有已分配的可用连接,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:address 一致—— host、port、代理等一致,且匹配的连接可以接受新的请求。
若从连接池没有获取到,则传入 routes 再次尝试获取,这主要是针对 Http2.0 的一个操作, Http2.0 可以复用 square.com 与 square.ca 的连接
若第二次也没有获取到,就创建 RealConnection 实例,进行 TCP + TLS 握手,与服务端建立连接。
此时为了确保 Http2.0 连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。
第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。
以上就是连接拦截器尝试复用连接的操作,流程图如下:
6. OKHttp空闲连接如何清除?
上面说到我们会建立一个 TCP 连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp 是如何做到的呢?
# 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分钟后再尝试清理。
4. 没有连接不清理。
流程如下图所示:
7. OKHttp有哪些优点?
使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端 OkHttpClient 统一暴露出来。
扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求
功能强大,支持 Spdy、Http1.X、Http2、以及 WebSocket 等多种协议
通过连接池复用底层 TCP(Socket),减少请求延时
无缝的支持 GZIP 减少数据流量
支持数据缓存,减少重复的网络请求
支持请求失败自动重试主机的其他 ip,自动重定向
8. OKHttp框架中用到了哪些设计模式?
构建者模式:OkHttpClient 与 Request 的构建都用到了构建者模式
外观模式:OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端 OkHttpClient 统一暴露出来
责任链模式: OKHttp 的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置
享元模式: 享元模式的核心即池中复用, OKHttp 复用 TCP 连接时用到了连接池,同时在异步请求中也用到了线程池
扩展
http的请求流程
使用DNS域名解析;
发起TCP的3次握手
建立TCP连接后发起http请求;
服务器响应http请求,浏览器得到返回response;
浏览器解析response,并请求其它的资源(如js、css、图片等);
浏览器对页面进行渲染。
Https的请求流程
https://blog.csdn.net/weixin_52834435/article/details/124762740
客户端向服务器发起请求,请求中包含使用的TLS版本号、生成的一个随机数、以及客户端支持的加密方法。
服务器端接收到请求后,确认双方使用的加密方法和TLS版本号、并给出服务器的证书、以及一个服务器生成的随机数。
客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中解密拿到的服务器公钥,加密这个随机数,然后发给服务器。
服务器使用自己的私钥,来解密客户端发送过来的随机数。这样服务器就拿到了第三个随机数。而且只有客户端和服务器端知道这第三个随机数,因为第三个随机数是通过加密传输的。
客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成会话秘钥,以后的对话过程都使用这个秘钥(即会话秘钥)来加密信息。
以后客户端和服务器端都使用这个会话秘钥来加密。
Okhttp防抓包
1、 使用Proxy.NO_PROXY进行防止抓包。
我们在使用OkHttp进行网络请求的时候防止Fiddler抓包可以简单的使用OkHttpClient.Builder中的builder.proxy(Proxy.NO_PROXY);方法就可以避免Fiddler基本抓包。
2、使用builder.proxySelector进行防止抓包。
因为第一种方法只能避免Fiddler基本抓包方式在OkHttp中还有一种方法可以防止抓包。
builder.proxySelector(new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
return Collections.singletonList(Proxy.NO_PROXY);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
}
});
3、验证Https证书
这种方式要在app嵌入证书,以okhttp为例:当okhttp使用X509TrustManager对服务器证书进行校验时,如果服务器证书的 subjectDN 和嵌入证书的 subjectDN 一致,我们再进行签名内容 signature 的比对,如果不一致,抛出异常。示例代码如下:
首先从本地读出证书,获取一个X509Certificate
val myCrt: X509Certificate by lazy {
getCrt(R.raw.my_ca)
}
private fun getCrt(@RawRes raw: Int): X509Certificate {
val certificateFactory = CertificateFactory.getInstance("X.509")
val input = ApplicationContext.resources.openRawResource(raw)
input.use {
return certificateFactory.generateCertificate(input) as X509Certificate
}
}
检查服务器证书时对比嵌入的证书
private fun getTrustManagerInRelease(): X509TrustManager {
return object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String?) {
val myCrt: X509Certificate = myCrt
if (chain[0].subjectDN.name == myCrt.subjectDN.name) {
if (!myCrt.signature!!.contentEquals(chain[0].signature)) {
throw SSLHandshakeException("签名不符!")
}
}
}
}
}
将自定义的 SSLSocketFactory 和 X509TrustManager 将入到 okhttp 客户端
private fun getClient(ssl: SSLSocketFactory, trustManager: X509TrustManager): OkHttpClient {
return OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.proxy(Proxy.NO_PROXY)
.sslSocketFactory(ssl, trustManager)
.build()
}
拦截器
RetryAndFollowUpInterceptor : 创建StreamAllocation对象,处理http的重定向,出错重试。对后续Interceptor的执行的影响:修改request及StreamAllocation。
BridgeInterceptor:补全缺失的一些http header,Cookie设置。对后续Interceptor的执行的影响:修改request。
CacheInterceptor:处理http缓存。对后续Interceptor的执行的影响:若缓存中有所需请求的响应,则后续Interceptor不再执行。
ConnectInterceptor:借助于前面分配的StreamAllocation对象建立与服务器之间的连接(具体建立是在newStream方法中),并选定交互所用的协议是HTTP 1.1还是HTTP 2。对后续Interceptor的执行的影响:创建了httpStream和connection。
CallServerInterceptor:处理IO,与服务器进行数据交换。对后续Interceptor的执行的影响:为Interceptor链中的最后一个Interceptor,没有后续Interceptor。
关联:https://blog.csdn.net/qq_45866344/article/details/125052085
连接池
为什么需要连接池?
频繁的进行建立Sokcet连接和断开Socket是非常消耗网络资源和浪费时间的,所以HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。keepalive机制是什么呢?也就是可以在一次TCP连接中可以持续发送多份数据而不会断开连接。所以连接的多次使用,也就是复用就变得格外重要了,而复用连接就需要对连接进行管理,于是就有了连接池的概念。
OkHttp里面使用ConnectionPool实现连接池,而且默认支持5个并发KeepAlive,默认链路生命为5分钟。 双端队列Deque,双端都能进出,用来存储连接的。OkHttp框架采用的是Socket连接,底层涉及到Http协议的封装和解封,TLS/SSL安全协议的封装等;连接池主要涉及到几大类:ConnectionPool,RealConnection,StreamAllocation,ConnectionInterceptor;
怎么实现的?
1)首先,ConectionPool中维护了一个双端队列Deque,也就是两端都可以进出的队列,用来存储连接。
2)然后在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
returnnull;
}
也就是遍历了双端队列,如果连接有效,就会调用acquire方法计数并返回这个连接。
3)如果没找到可用连接,就会创建新连接,并会把这个建立的连接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了ConectionPool的put方法。
public final class ConnectionPool {
void put(RealConnection connection) {
if (!cleanupRunning) {
//没有连接的时候调用
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
3)其实这个线程池中只有一个线程,是用来清理连接的,也就是上述的cleanupRunnable
private final Runnable cleanupRunnable = new Runnable() {
@Override
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) {
//在timeout时间内释放锁
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
这个runnable会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,然后进入wait等待。
怎么清理的呢?看看源码:
long cleanup(long now) {
synchronized (this) {
//遍历连接
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//检查连接是否是空闲状态,
//不是,则inUseConnectionCount + 1
//是 ,则idleConnectionCount + 1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//如果超过keepAliveDurationNs或maxIdleConnections,
//从双端队列connections中移除
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) { //如果空闲连接次数>0,返回将要到期的时间
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 连接依然在使用中,返回保持连接的周期5分钟
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
也就是当如果空闲连接maxIdleConnections超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
4)这里有个问题,怎样属于空闲连接?
其实就是有关刚才说到的一个方法acquire计数方法:
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
在RealConnection中,有一个StreamAllocation虚引用列表allocations。每创建一个连接,就会把连接对应的StreamAllocationReference添加进该列表中,如果连接关闭以后就将该对象移除。
5)连接池的工作就这么多,主要就是管理双端队列Deque<RealConnection>,可以用的连接就直接用,然后定期清理连接,同时通过对StreamAllocation的引用计数实现自动回收。
在OKHttp中,创建了一个阀值是Integer.MAX_VALUE的线程池,它不保留任何最小线程,随时创建更多的线程数,而且如果线程空闲后,只能多活60秒。所以也就说如果收到20个并发请求,线程池会创建20个线程,当完成后的60秒后会自动关闭所有20个线程。他这样设计成不设上限的线程,以保证I/O任务中高阻塞低占用的过程,不会长时间卡在阻塞上。
网络连接
请简单说一下OKHttp的源码实现?
OKHttp的网络请求分为同步和异步两种,异步请求涉及到任务调度机制,主要涉及的类是Dispatcher。Dispatcher类中维护了一个线程池用于执行网络请求,同时维护了3个队列用于存放不同的网络请求。
OKHttp发起网络请求之前,会通过自己的拦截器链机制,拼接相关的网络请求。拦截器可以用来添加、移除、转换请求或者响应的头部信息。
OKHttp拼接完请求之后,会检查一下自己的缓存。如果有缓存并且可用则用缓存的数据并更新缓存,否则就用网络请求返回的数据。OKHttp发起网络请求并接受服务器相应,主要使用HttpEngine的sendRequest()和readResponse()方法,也是在这两个方法中进行的缓存处理。
由于Http协议是基于TCP协议的,OKHttp对于网络请求的底层也做了一些优化,也就是对Socket连接。主要是通过OKHttp的复用连接池机制,通过对Socket连接的复用,可以减少TCP握手次数,提高网络请求的效率,减少网络请求延迟。
五大拦截器
OkHttp最核心的工作是在 getResponseWithInterceptorChain() 中进行,在进入这个方法分析之前,我们先来了 解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。责任链顾名思义就是由一系列的负责者构成的一个链条,类似于工厂流水线
责任链设计模式
①定义:
它为请求创建了一个接收者对象的链。为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链,当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。(责任链模式也叫职责链模式)
在责任链模式中,每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象 决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的 情况下动态的重 新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象 所接受。
②为什么要使用责任链模式
我觉得原因如下:
解耦
实现单依职责原则
拦截器的工作流程
默认的5大拦截器有哪些?
RetryAndFollowUpInterceptor(重试和重定向拦截器)
第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求
BridgeInterceptor(桥接拦截器)
补全请求,并对响应进行额外处理
CacheInterceptor(缓存拦截器)
请求前查询缓存,获得响应并判断是否需要缓存
ConnectInterceptor(链接拦截器)
与服务器完成TCP连接 (Socket)
CallServerInterceptor(请求服务拦截器)
与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)
RetryAndFollowUpInterceptor拦截器
第一个拦截器: RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试与重定向。
一、重试
请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。
RouteException
catch (RouteException e) {
//todo 路由异常,连接未成功,请求还没发出去
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue; }
IOException
catch (IOException e) {
//todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
// HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue; }
两个异常都是根据 recover 方法判断是否能够进行重试,如果返回 true ,则表示允许重试。
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
//todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
if (!client.retryOnConnectionFailure()) return false;
//todo 2、如果是RouteException,不用管这个条件,
// 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件 if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//todo 3、判断是不是属于重试的异常
if (!isRecoverable(e, requestSendStarted)) return false; //todo 4、有没有可以用来连接的路由路线
if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行 请求的重试。其中某些异常是在 isRecoverable 中进行判断:
private boolean isRecoverable(IOException e, boolean requestSendStarted) { // 出现协议异常,不能重试
if (e instanceof ProtocolException) {
return false;
}
// 如果不是超时异常,不能重试
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// SSL握手异常中,证书出现问题,不能重试
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
} }
// SSL握手未授权异常 不能重试
if (e instanceof SSLPeerUnverifiedException) {
return false;
}
return true;
}
1、协议异常,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来 定义数据,再重试也没用)
2、超时异常,可能由于网络波动造成了Socket连接的超时,可以使用不同路线重试。
3、SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确, 那还怎么重试?
经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比 如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。
二、重定向
如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重 定向的判断。重定向的判断位于 followUpRequest 方法
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 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权 case HTTP_PROXY_AUTH:
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 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” case HTTP_UNAUTHORIZED:
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse); // 308 永久重定向
// 307 临时重定向
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// 300 301 302 303
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// 如果用户不允许重定向,那就返回null
if (!client.followRedirects()) return null;
// 从响应头取出location
String location = userResponse.header("Location");
if (location == null) return null;
// 根据location 配置新的请求 url
HttpUrl url = userResponse.request().url().resolve(location);
// 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
if (url == null) return null;
// 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme()); if (!sameScheme && !client.followSslRedirects()) return null;
Request.Builder requestBuilder = userResponse.request().newBuilder();
/**
* 重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式, * 即只有 PROPFIND 请求才能有请求体
*/
//请求不是get与head
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method); // 除了 PROPFIND 请求之外都改成GET请求
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
// 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉 if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
} }
// 在跨主机重定向时,删除身份验证请求头
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408 客户端请求超时
case HTTP_CLIENT_TIMEOUT:
// 408 算是连接失败了,所以判断用户是不是允许重试 if (!client.retryOnConnectionFailure()) {
return null;
}
// UnrepeatableRequestBody实际并没发现有其他地方用到
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求 了
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
return null;
}
// 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。 if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
// 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求 case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request();
}
return null;
default:
return null;
}
}
整个是否需要重定向的判断内容很多,记不住,这很正常,关键在于理解他们的意思。如果此方法返回空,那就表 示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request ,但是需要注意的是, 我们的followup在拦截器中定义的最大次数为20次。
总结
本拦截器是整个责任链中的第一个,这意味着它会是首次接触到Request与最后接收到 Response 的角色,在这个 拦截器中主要功能就是判断是否需要重试与重定向。
重试的前提是出现了 RouteException 或者 IOException 。一但在后续的拦截器执行过程中出现这两个异常,就会 通过 recover 方法进行判断是否进行连接重试。
重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用 followUpRequest 根据 Response 的响 应码(当然,如果直接请求失败, Response 都不存在就会抛出异常)。 followup 最大发生20次。
BridgeInterceptor 桥接拦截器
连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单。
补全请求头:
在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:
1、 保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的 CookieJar 不提供实现
2、如果使用gzip返回的数据,则使用GzipSource包装便于解析。
总结
桥接拦截器的执行逻辑主要就是以下几点
对用户构建的 Request 进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的 Request 将符合网络 请求规范的Request交给下一个拦截器处理,并获取 Response 如果响应体经过了GZIP压缩,那就需要解压,再构 建成用户可用的 Response 并返回
CacheInterceptor缓存拦截器
在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存 在Get请求的缓存)
步骤为:
1、 从缓存中获得对应请求的响应缓存
2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员: networkRequest 与 cacheResponse 。他们的组合如下:
3、交给下一个责任链继续处理
4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)
缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓 存,或是请求服务器都是通过 CacheStrategy 判断。
想讲清楚CacheInterceptor有些困难,也比较复杂,所以这里我只是简单介绍一下。
ConnectInterceptor连接拦截器
打开与目标服务器的连接,并执行下一个拦截器。它简短的可以直接完整贴在这里:
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
虽然代码量很少,实际上大部分功能都封装到其它类去了,这里只是调用而已。
首先我们看到的 StreamAllocation 这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在
这里。
"当一个请求发出,需要建立连接,连接建立后需要使用流用来读取数据 ";而这个StreamAllocation就是协调请 求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。
这里使用的 newStream 方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的 HttpCodec 中包含了 输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。
StreamAllocation 中简单来说就是维护连接:RealConnection——封装了Socket与一个Socket连接池。可复用 的 RealConnection 需要:
public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// At this point we don't have a hostname match. But we still be able to carry the request
if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;
// 2. The routes must share an IP address. This requires us to have a DNS address for both
// hosts, which only happens after route planning. We can't coalesce connections that use a
// proxy, since proxies don't tell us the origin server's IP address.
if (route == null) return false;
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
// 3. This connection's server certificate's must cover the new host.
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
1、 if (allocations.size() >= allocationLimit || noNewStreams) return false;
连接到达最大并发流或者连接不允许建立新的流;如http1.x正在使用的连接不能给其他人用(最大并发流为:1)或者
连接被关闭;那就不允许复用;
2、
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
DNS、代理、SSL证书、服务器域名、端口完全相同则可复用; 如果上述条件都不满足,在HTTP/2的某些场景下可能仍可以复用(http2先不管)。 所以综上,如果在连接池中找到个连接参数一致并且未被关闭没被占用的连接,则可以复用。
总结
这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。
CallServerInterceptor请求服务连接器
利用 HttpCodec 发出请求到服务器并且解析生成 Response 。
首先调用 httpCodec.writeRequestHeaders(request); 将请求头写入到缓存中(直到调用 flushRequest() 才真正发
送给服务器)。然后马上进行第一个逻辑判断
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();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener().requestBodyEnd(realChain.call(),requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
//HTTP2多路复用,不需要关闭socket,不管!
// 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();
整个if都和一个请求头有关: Expect: 100-continue 。这个请求头代表了在发送请求体之前需要和服务器确定是 否愿意接受客户端发送的请求体。所以 permitsRequestBody 判断为是否会携带请求体的方式(POST),如果命中 if,则会先给服务器发起一次查询是否愿意接收请求体,这时候如果服务器愿意会响应100(没有响应体,responseBuilder即为nul)。这时候才能够继续发送剩余请求数据。但是如果服务器不同意接受请求体,那么我们就需要标记该连接不能再被复用,调用 noNewStreams() 关闭相关的 Socket。
总结
整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截 器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一 样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。
但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结 果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。
当用户发起一个请求后,会由任务分发起 Dispatcher 将请求包装并交给重试拦截器处理。
1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP 压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。
3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处
理。
5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。 在经过了这一系列的流程后,就完成了一次HTTP请求!
response.body().string() 只能调用一次?
我们可能习惯在获取到Response对象后,先response.body().string()打印一遍log,再进行数据解析,却发现第二次直接抛异常,其实直接跟源码进去看就发现,通过source拿到字节流以后,直接给closeQuietly悄悄关闭了,这样第二次再去通过source读取就直接流已关闭的异常了。
public final String string() throws IOException {
BufferedSource source = source();
try {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
} finally {
//这里讲resource给悄悄close了
Util.closeQuietly(source);
}
}
解决方案:1.内存缓存一份response.body().string();2.自定义拦截器处理log。
Okhttp网络缓存如何实现
OKHttp 默认只支持 get 请求的缓存。
第一次拿到响应后根据头信息决定是否缓存。
下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
Okhttp 网络连接怎么实现复用
HttpEngine 在发起请求之前,会先调用nextConnection()来获取一个Connection对象,如果可以从ConnectionPool中获取一个Connection对象,就不会新建,如果无法获取,就会调用createnextConnection()来新建一个Connection对象,这就是 Okhttp 多路复用的核心,不像之前的网络框架,无论有没有,都会新建Connection对象。