OkHttp 源码解析
OkHttp是一个高效的客户端HTTP请求框架,在Android客户端开发被广泛使用。
最近学习了HTTP协议的相关知识,学习一下OKHttp是如何使用HTTP协议请求网络的。
学习OKhttp源码就要先看全局,然后根据使用需要再研究细节,体会其精妙之处。
了解它的原理,并加以实践,可以实现一个简单的HTTP请求客户端。
实现HTTPS请求-响应思路
根据TCP/IP五层网络模型自下而上分别是物理层、数据链路层、网络层、传输层、应用层。
我们在实际开发中接触最多的就是传输层协议TCP/UDP和应用层协议HTTP,如果是HTTPS在传输层和应用层之间还有安全接口SSL/TLS协议。
请求-响应思路如下,整个OkHTTP整体实现思路基本也是一样的,具体的实现细节则需要探寻源码了。
- 根据URL主机名,通过DNS协议获取IP地址。
- 通过三次握手客户端和服务端建立TCP连接。
- TLS/SSL协议握手,确定加密方式、密钥等,保证传输安全。
- 通过HTTP协议构建请求,发送请求内容。
- 服务端处理返回,读取HTTP响应内容,渲染展示。
OkHttp使用的设计模式
OkHttp实现请求-响应的整体过程主要运用了两种设计模式Builder模式和责任链模式。
Builder模式
Builder模式主要用于构建复杂对象,将创建过程和参数表示进行解耦。
不是所有的复杂对象都适合使用Builder模式,比如数据实体Modle对象。
Builder模式适用条件:
- 如果复杂对象中很多参数只能内部创建,不能由调用者外部构建参数,传入参数值。
- 参数创建顺序不同导致结果不同,比如装配汽车,先要把车身组建好再安装车轮,而不是先安装车轮。
- 相同的参数可以被多个对象使用,最终构建的对象是不同的。比如不同品牌的电脑都是用英特尔的CPU。
Builder模式把创建细节封装在内部,可以定制的参数再开放给外部,交由外部创建,最终按照一定的顺序进行创建对象。
使用Builder模式创建的对象,内部都有一个Builder
对象用于实现创建细节。
OkHttp中的很多对象都使用了Builder模式,比如Request
、Reponse
、OkHttpClient
等等,在OkHttp源码中可以细细探究。
责任链模式
OkHttpClient
OkHttpClient
是Http请求-响应的客户端类,可以理解成HTTP协议中的客户端,如浏览器,负责管理一个应用中所有HTTP请求和响应。
HTTP客户端需要有自己的武器库,通过组合使用,实现客户端请求响应的功能。
下面一一介绍OkHttpClient
武器库中的武器,在分析源码中,再看看他们是如何应用的。
Dispatcher
调度器,负责请求的调度,管理线程池和请求队列,默认的最大请求量为64。
Okhttp分为同步请求和异步请求。
同步请求:使用当前线程请求,会堵塞当前线程。
异步请求:使用另外一个线程请求,当请求完成,将响应结果通过回调函数传递。
Dispatcher
主要是对异步请求的调度,使用线程池中的线程发起请求,并通过异步请求队列进行管理。Dispatcher
通过同步请求队列管理请求,但是线程调度由外部负责。
private ExecutorService executorService;
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
Dns
Dns负责将调用Socket中的方法,将主机名解析成IP地址。
SocketFactory
socket工厂类,可以使用多种创建方式完成socket的创建。
socket是操作系统提供给的网络接口,我们将应用层的数据,交给Socket,由操作系统实现TCP/IP协议栈,完成数据的传输。
SSLSocketFactory
SSLSocket工厂类,SSLSocket在socket基础上,实现TLS/SSL协议的安全接口。
ConnectionSpec
SSLSocket的连接配置,HTTPS完成三次握手连接之后,还要进行TLS/SSL协议握手。ConnectionSpec
根据TLS版本不同,配置不同的连接参数,主要包括客户端支持的TLS版本和密码套件。
CertificatePinner
证书限定,限制信任哪些证书和证书颁发机构。客户端只信任配置的数字证书,其他数字证书一律不信任,防止黑客使用中间机构颁发的数字证书伪装骗取信任,获取数据,破坏传输安全。
不过可能会出现服务端证书更新,由于证书固定导致不被客户端信任,所以服务端如果没有要求,一般情况下不使用证书限定。
CertificateChainCleaner
证书清除器,服务器传递的证书????
HostnameVerifier
在TLS握手过程中,如果URL的主机名和请求服务器的主机名不匹配,则需要使用验证接口进行校验,数字证书中包含主机名可以通过证书校验,或者其他校验方式验证。
Authenticator
身份认证器,如果服务端返回401状态码,则客户端要向服务器验证自己的身份。
OkHttpClient包含目标服务器身份认证authenticator
和代理服务器身份认证proxyAuthenticator
。
ConnectionPool
连接池,如果客户端和请求的服务器已经建立连接,OkHttp不会新建连接,而是复用连接池中已有连接对象,以减少网络延迟。
Protocol
枚举类,使用的协议管理,如HTTP/1.0、HTTP/1.1、HTTP/2等
HTTP_1_0("http/1.0"),
HTTP_1_1("http/1.1"),
SPDY_3("spdy/3.1"),
HTTP_2("h2"),
H2_PRIOR_KNOWLEDGE("h2_prior_knowledge"),
QUIC("quic");
Proxy
表示请求代理设置,有三种类型
DIRECT
,直连模式,直接和目标服务器通信,中间没有代理服务器HTTP
, 代理模式,通过HTTP服务器和目标服务器通信。HTTP代理服务器根据HTTP协议转发HTTP数据包。SOCKS
,代理模式,通过Socks服务器和目标服务器通信,Socks代理只是简单地转发数据包(传输层),而不必关心是何种应用协议(比如FTP、HTTP和NNTP请求)。
ProxySelector
代理选择器,根据你要连接的URL自动选择最合适的代理,如果需要编写定制代理选择器,需要继承并实现select
方法。
CookieJar
Cookie管理,如果服务器返回的响应中返回cookie,则会CookieJar则会保存在本地。
当请求服务器时,请求头会带上对应域名下保存的cookie。
默认情况不保存cookie,配置对应属性才能保存。
Cache
缓存器,将响应结果保存到本地,主要通过DiskLruCache
实现HTTP的缓存。
Interceptor
拦截器,拦截请求和响应,通过拦截请求和响应的各个阶段,对请求参数和响应结果进行处理。
OkHttp提供了RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
、ConnectInterceptor
、NetWorkInterceptor
、CallServerInterceptor
官方拦截,负责请求和响应的处理。
我们也可以编写自定义拦截器,实现接口Interceptor
,对请求和结果进行处理。
EventListener.Factory
EventListener
工厂类,负责创建EventListener
。EventListener
负责监听HTTP或HTTPS从请求开始到响应结束的各个阶段,通过回调函数获取请求、响应等对象。
public void callStart(Call call);
public void dnsStart(Call call, String domainName);
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList);
public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy);
public void secureConnectStart(Call call);
public void secureConnectEnd(Call call, @Nullable Handshake handshake);
public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
@Nullable Protocol protocol) ;
public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
@Nullable Protocol protocol, IOException ioe) ;
public void connectionAcquired(Call call, Connection connection);
public void connectionReleased(Call call, Connection connection);
public void requestHeadersStart(Call call);
public void requestHeadersEnd(Call call, Request request);
public void requestBodyStart(Call call);
public void requestBodyEnd(Call call, long byteCount);
public void responseHeadersStart(Call call);
public void responseHeadersEnd(Call call, Response response);
public void responseBodyStart(Call call);
public void responseBodyEnd(Call call, long byteCount);
public void callEnd(Call call);
分析源码
请求过程
了解OkHttpClient
中的参数,下面深入源码OkHttp如何完成一次HTTP的请求和响应的。
Okhttp有同步和异步两种方式,请求过程基本一致,但是异步请求需要Dispatcher
调度。
我们先通过同步请求了解OkHttp的请求过程,再分析异步请求的调度处理。
由于OkHttp中需要处理的东西比较多,因此会对源码做删减,只关注核心。
请求流程如下图所示,
发起请求
同步请求代码如下所示,没有配置任何参数,是一个最小化同步请求客户端。
首先创建Request
对象,传入请求url,这里请求的是百度,使用get请求方法。
创建OkHttpClient
对象,调用netCall
方法创建一个Call
对象,并发起请求。
Call
对象负责获取请求参数,发起请求,接收响应结果,返回给客户端,如同浏览器打开了一个新窗口,输入网址发起请求,接收结果渲染页面。Call
只是一个接口,OkHttp中真正使用的实现类RealCall
。
调用RealCall
中的execute()
方法执行,开始发起请求。
Request request = new Request.Builder()
.url("百度一下,你就知道")
.get()
.build();
OkHttpClient okHttpClient = new OkHttpClient();
Response response = okHttpClient.newCall(request).execute();
RealCall
中的execute()
方法,执行步骤:
- 调用监听函数,告诉调用者请求已经开始
- 使用
Dispather
调度器,将Call
加入到同步队列当中,就如同浏览器的窗口管理,如果浏览器关闭,统一将所有请求断开,关闭窗口。 - 调用
getResponseWithInterceptorChain()
通过责任链模式将多个拦截器连接,自上而下处理请求,自下而上处理响应。 - 返回响应结果,关闭
Call
对象。
getResponseWithInterceptorChain()
方法完成了OkHttp的请求和响应的具体工作。
// RealCall
public Response execute() throws IOException {
eventListener.callStart(this);
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
client.dispatcher().finished(this);
}
// Dispatcher
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
责任链处理请求
RealCall
中的getResponseWithInterceptorChain()
方法将自定义的拦截器和OkHttp提供的拦截器依次添加到拦截器容器中。
创建RealInterceptorChain
责任链对象,传入参数为拦截器容器,将要调用的拦截器索引,请求对象和超时时间,其他参数初始化时无法提供所以暂时为空。
最后调用责任链对象中proceed
方法依次调用拦截器处理请求。
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.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
RealInterceptorChain
中的proceed()
方法又创建了一个责任链对象,根据索引获取当前处理请求的拦截器,调用拦截器中的intercept()
方法处理请求。Interceptor
中的intercept()
方法,处理完请求之后,又调用了责任链对象的proceed()
方法。
// RealInterceptorChain中的proceed
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
...
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
...
return response;
}
// Interceptor中的intercept方法
public Response intercept(Chain chain) throws IOException {
// 处理请求
...
response = realChain.proceed(request, streamAllocation, null, null);
// 处理响应
...
return response;
}
看到这里,大脑就比较混乱了,下面整理思绪,细细分析一下。
责任链模式的核心就是将对象顺着责任链依次加工,如下图所示,可以责任链对象看成工厂,拦截器是车间。
- 责任链对象
Chain
调用当前拦截器的intercept()
方法,并将整个责任链对象传递过去。 - 拦截器可以使用
Chain
中的所有参数如Request
、Call
等进行处理,表示车间可以使用工厂已有的所有原料。 - 拦截器处理完毕,再调用责任链对象的
proceed()
方法,再将处理完毕的参数再回传给Chain
,表示车间将原料加工成半成品或产生了新产品,交给工厂。 proceed()
方法中又创建了新的责任链对象,但是整个拦截器链没有发生变化,只是原料换成了半成品和新产品,所以可看成工厂更新了原料,并且使用下一个拦截器加工。- 按照上述做法,责任链按照顺序依次调用拦截器对参数进行处理,完成HTTP请求和响应。
接下来按照顺序分析每个拦截器的intercept()
方法是如何处理请求参数,最终完成HTTP请求。
RetryAndFollowUpInterceptor处理请求
RetryAndFollowUpInterceptor
是处理参数的第一个拦截器,主要工作如下:
创建了StreamAllocation
对象,负责协调请求、连接和流之间的关系,请求通过新建或复用连接,在连接上流按照一定的逻辑顺序传输数据,调用StreamAllocation
对象完成具体工作由其他拦截器处理。
接下来进入一个while
死循环,调用Chain
的proceed
执行调用下一个拦截器处理,回传新建StreamAllocation
对象。
如果接下来的拦截器处理出错,由于死循环,会从头开始,请求会继续重试。
如果catch
异常处理中,恢复失败,则抛出异常终止重试。
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
while (true) {
Response response;
boolean releaseConnection = true;
try {
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
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;
}
}
// 响应结果处理
...
}
BridgeInterceptor处理请求
BridgeInterceptor
对Request
对象进行加工,补充请求头信息,并将传入的参数对象转换需要的头部字段。
我们在开发时构建的Request
对象仅仅传入了url和确定请求方式,而HTTP请求需要的其他头部字段,都需要在BridgeInterceptor
中添加。
下面看看为请求头添加了哪些头部字段
- 判断是否请求上传实体,如果有上传,调用者传入了
contentType
,将其转换为头部字段Content-Type
字符串。
实体长度确定,添加Content-Length
说明实体长度。
实体长度不确定,添加Transfer-Encoding:chunked
说明分块阐述。 - 添加
Host
说明请求服务器的主机名。 - 添加
Connection:Keep-Alive
使用长连接。 - 判断是否请求部分资源,如果不是添加
Accept-Encoding:gzip
,期望服务器对返回资源进行压缩。 - 加载域名下的本地cookie,如果存在则添加
Cookie:本地cookie信息
。 - 添加
User-Agent
,说明客户端的信息。
Request
对象头部信息添加完毕,传递给责任链对象,调用下一个拦截器进行处理。
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
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");
}
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
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());
// 处理响应
return responseBuilder.build();
}
缓存控制
OkHttp根据HTTP协议处理客户端缓存,所以想要了解OkHttp中对缓存的处理,需要先了解HTTP协议的缓存机制。
OkHttp中缓存处理的组件主要有Cache
、CacheStrategy
和CacheIntercepter
。Cache
:负责缓存的存取等操作。CacheStrategy
:负责缓存策略的制定。CacheIntercepter
:负责将请求和将资源缓存到客户端。客户端和服务器根据一定的机制(策略CacheStrategy ),在需要的时候使用缓存的数据作为网络响应,节省了时间和宽带。
下面详细看看OkHttp是如何进行缓存控制的,对上面的类一一分析。
CacheIntercepter
Http请求如果不需要使用缓存,则直接调用下一个拦截器处理。CacheStrategy
主要制定4种缓存策略,具体策略如何生成的,下面会具体分析
networkRequestcacheResponse缓存策略NULLNULL只能使用本地缓存,但是无缓存可用,又不向服务器请求新资源NULLNOT NULL使用本地缓存,不向服务器请求新资源NOT NULLNOT NULL不使用本地缓存,向服务器请求新资源NOT NULLNOT NULL使用本地缓存,向服务器验证缓存是否可用
根据缓存策略做出不同处理。
如果第1种策略返回状态码504(Unsatisfiable Request (only-if-cached)),只能使用缓存,但是客户端无缓存可用。
如果第2种策略,则直接返回客户端缓存的资源。
第3或第4种策略,则不管是否使用缓存,都要向服务器请求,则交由下一个拦截器进行处理。
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
...
// 第一种
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();
}
// 第二种
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 第三种
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 处理响应
...
return response;
}
CacheStrategy
在CacheInterceptor
中通过下面代码创建CacheStrategy
缓存策略。
cacheCandidate
会先从cache
中根据请求获取缓存资源,如果存在直接赋值,如果不存在则为null。
调用CacheStrategy
的Factory()
方法创建工厂,调用get()
方法,通过工厂模式创建CacheStrategy
对象。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
在CacheStrategy
的Factory()
方法中,如果cacheResponse
不为空,即在客户端缓存的资源。
根据cacheResponse
对象的响应头,获取跟缓存控制相关头部的值。
sentRequestMillis
:表示上次客户端向服务器请求该资源的时间。receivedResponseMillis
:表示上次客户端从服务端收到包含该资源响应的时间。servedDate
:表示服务端返回资源生成响应报文的时间。expires
:表示缓存资源过期时间。lastModified
:表示缓存资源被服务器修改的时间。etag
:表示缓存资源的实体标签。ageSeconds
:表示缓存资源在代理服务器中存储的时间,如果没有代理服务器值接近为0。
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
调用Factory
中的get
方法获取并创建缓存策略对象CacheStrategy
。
通过调用getCandidate()
方法根据上面获取的缓存字段确定不同的缓存策略,返回对应CacheStrategy
对象。
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
getCandidate()
代码比较复杂,需要分步来看
第一部分代码,如果在以下情况下,缓存策略为不使用缓存资源,向服务器请求最新资源,返回CacheStrategy(request, null)
。
- 如果客户端没该请求下的缓存资源。
- 如果请求为HTTPS,但是缺少TLS/SSL的握手信息。
- 如果不能使用缓存。
- 如果使用缓存之前需要验证,但是请求头中的条件控制头部
If-Modified-Since
和If-None-Match
都为空。
private CacheStrategy getCandidate() {
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
}
第二部分代码如下所示,如果缓存不需要向服务器验证且在使用时间范围内,则可以使用缓存,返回CacheStrategy(null, cacheResponse)
。
CacheControl
保存就是HTTP协议中的头部字段Cache-Control
中的值。
ageMillis
:表示缓存资源产生的时间,从服务器产生资源返回给客户端起,包括停留在代理服务器的时间、传输花费的时间、客户端存储的时间。freshMillis
:表示缓存资源可以存储的时间,从服务器产生资源开始算,超过这个时间段,则会过期,相当于保质期。minFreshMillis
:表示请求头要求缓存资源在指定时间内不能过期maxStaleMillis
:表示请求头要求缓存如果过期了,还能使用的时间。
然后通过判断,如果缓存资源使用不要向服务器验证,通过公式ageMillis + minFreshMillis < freshMillis + maxStaleMillis
判断是否使用缓存。
就是判断缓存是否过期了,如果过期了就不使用,没过期则使用。
举个例子,ageMillis=10,minFreshMillis=3, freshMillis = 20, maxStaleMillis = 2。
缓存已经产生了10秒,客户端要求缓存资源在3秒后也不能过期。
缓存过期时间为20秒,并且如果过期了,在2秒内还能使用。
所以即使3秒后,缓存也才存在了13秒,没有超过缓存的使用期限22秒,所以客户端可以使用缓存。
private CacheStrategy getCandidate() {
CacheControl responseCaching = cacheResponse.cacheControl();
long ageMillis = cacheResponseAge();
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
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());
}
}
第三部分,如果缓存需要向服务器验证是否可以使用,客户端需要构建条件请求头部。
等待服务器验证,验证通过则客户端使用服务器缓存,验证不通过服务器返回最新资源,返回CacheStrategy(conditionalRequest, cacheResponse)
。
conditionName
和conditionValue
为条件请求头部字段和对应值。
相关字段已经在HTTP协议缓存控制介绍了,最后构建对应的请求头。
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);
}
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);
ConnectInterceptor
ConnectInterceptor
负责和服务端建立连接,打开传输流。
拦截器中代码确实不长,建立连接的核心代码为HttpCodec httpCodec=streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
。
HttpCodec
从连接对象中获取输入流和输出流,负责对对HTTP请求报文进行编码写入输入流,从输出流获取字节进行HTTP解码获取响应报文。
最后将连接对象connection
和http编解码对象httpCodec
交给下一个拦截器处理。
下面我们将分析streamAllocation
中的newStream
方法。
@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, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
在StreamAllocation
对象中调用findHealthyConnection()
。findHealthyConnection()
方法中进入一个死循环,调用findConnection()
。
如果连接池中可以复用当前请求连接,直接复用,并检查连接是否可用,如果可用直接返回,不可用继续寻找。
如果successCount=0
,即连接还没有被使用过,是一个全新的可用连接直接返回。
下面分析findConnection()
方法,了解其寻找并建立连接的思路。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
...
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
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);
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
findConnection
代码很长,分为两步,复用已有的连接或建立新连接。
先看findConnection()
方法中使用的局部变量
result
:表示获取并返回的连接对象。selectedRoute
:选择路由,网络数据包客户端->代理服务器->源服务器之间的传输路径,包含客户端、代理服务器、源服务器的InetSocketAddress
对象,可以简单理解为把客户端、代理服务器和源服务器的IP地址连接起来,如同你要从福州到合肥,路径为福州->武夷山->黄山->合肥。releasedConnection
:表示需要释放的连接对象。toClose
:需要关闭的socket对象。
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;
首先来如何复用已有的连接。
releasedConnection
对象获取当前的连接对象。
调用releaseIfNoNewStreams()
方法,如果当前连接存在没有被释放,并且不能创建新的连接流了,如HTTP2多个请求共用一个传输流,每个请求不能创建新流,则释放当前连接,返回对应套接字关闭。
如果当前连接可用,没有被释放,则作为当前请求的连接对象。
如果当前连接不可用,根据目标主机名从连接池中获取连接对象。
最终找到可用连接对象直接返回。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
...
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
releasedConnection = null;
}
if (result == null) {
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);
...
if (result != null) {
return result;
}
}
第二部分,根据连接主机名无法从连接池获取缓存连接对象,那再根据路由从连接池中获取。路由保存了传输端点的IP地址,一个IP地址可以绑定多个主机名,所以有可能目标主机名虽然变化了但是目标IP地址没变,还可以使用缓存连接对象。
routeSelector
为路由选择器,调用routeSelector.next()
方法。
路由选择器使用DNS
对象,获取到了目标服务器或代理服务器的所有IP地址。
根据目标服务器IP地址或代理服务器IP地址创建多个路由,表示客户端和目标服务器所有传输路径。
添加完毕之后,返回路由集合routeSelection
。
从routeSelection
中获取所有路由,依次遍历,根据每个路由从连接池中获取缓存连接对象。
如果找到,foundPooledConnection=true
表示从连接池中找到连接对象,直接跳出循环,最终返回缓存连接对象。foundPooledConnection=false
表示没有从连接池中找到连接对象,那么先创建一个新的连接对象,并且从路由集合中选择一个路由,建立TCP连接,传输数据包。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
}
创建完新的连接对象,result为RealConnection
类型,调用connect()
方法和目标服务器建立TCP连接,如果为HTTPS建立TCS连接。
连接建立完毕,将连接对象添加到连接池中,如果HTTP协议版本为HTTP/2,则释放该连接,使用RealConnection
中的Http2Connection
连接对象。
最后返回连接对象。
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
Internal.instance.put(connectionPool, result);
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
CallServerInterceptor
CallServerInterceptor
主要将请求报文进行编码,添加到输出流中。
调用HttpCodec
对象的writeRequestHeaders()
方法,将请求头编码为一行字符串,添加到输出流中。
如果请求头部字段Expect
为100-continue
,一般用于POST请求提交请求体。
客户端先发送请求, 包含一个Expect:100-continue
, 询问服务端是否愿意接受数据,所以调用httpCodec.flushRequest();
将输入流刷新,将请求头(不包含请求体)发送给服务端,获取响应构造器responseBuilder
。
如果不需要提前发送请求向服务器验证,如果有请求体,对请求体也进行编码,并添加到输出流中,最后刷新输出流,发送给服务端。
@Override public Response intercept(Chain chain) throws IOException {
...
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
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()) {
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
// 处理响应
...
}