在《OkHttpClient源码:同步/异步请求》中,介绍了同步/异步请求流程。现在我们一起来看看OkHttp中连接池和缓存实现。
一、连接池
RealConnectionPool类
在OkHttpClient.Builder()
中,为Client创建了连接池,起初其中没有任何连接(尚未发生任何http请求)。 ConnectionPool
是装饰类,真正的连接池是RealConnectionPool
,默认最大空闲连接数为5,存活时间为5分钟。 RealConnectionPool的关键属性如下,使用ArrayDeque保存连接,队列最大尺寸为Integer.MAX_VALUE
(相当于无限制)。
java
public final class RealConnectionPool {
// 后台线程,用于清除连接池中过期的连接。
private static final Executor executor = new ThreadPoolExecutor(0 ,
Integer.MAX_VALUE , 60L , TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));
// 最大空闲连接数,默认为5
private final int maxIdleConnections;
// 存活时间,默认为5分钟
private final long keepAliveDurationNs;
// 清理逻辑
private final Runnable cleanupRunnable;
// 连接队列
private final Deque<RealConnection> connections = new ArrayDeque<>();
// 进行清理标志,防止并发
boolean cleanupRunning;
// 省略......
}
RealConnection类
它的关键属性如下,其中可以看到熟悉的java.net.Socket
对象,该连接指向的目标地址即route。因为只能对访问同一主机的多个HTTP请求间,从能复用同一个socket对象。
java
public final class RealConnection extends Http2Connection.Listener implements Connection {
public final RealConnectionPool connectionPool;
// 即目标服务器Addresses
private final Route route;
// TCP层面的Socket
private Socket rawSocket;
// application层面的Socket,比如SSLSocket
private Socket socket;
private Protocol protocol;
private Http2Connection http2Connection;
// 失败计数
int routeFailureCount;
// 成功计数
int successCount;
// 关联的transmitter
final List<Reference<Transmitter>> transmitters = new ArrayList<>();
// 省略......
}
可以通过HTTP proxy或IP address来创建Route,指明要访问的Web Server。
java
public final class Route {
final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
}
ConnectInterceptor类
当执行一个同步或异步请求,拦截器链执行到ConnectInterceptor
时,会为该请求创建一个Exchange
对象,此时需要先创建ExchangeCodec
对象。
java
// 先获取一个连接;如果是http 2.0版本,则返回Http2ExchangeCodec;否则返回Http1ExchangeCodec
ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
ExchangeFinder会从连接池中获取一个与当前请求Route相同的健康连接。
- 先尝试从连接池中获取连接:遍历连接池所有连接,返回第一个匹配的连接来复用。
java
if (result == null) {
// 从连接池获取连接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
foundPooledConnection = true;
result = transmitter.connection;
} else {
selectedRoute = previousRoute;
}
}
匹配逻辑RealConnection.isEligible():对于HTTP1.0请求,只检查host是否相同。
java
// 本次请求的host,与RealConnection的route属性匹配
if (address.url().host().equals(this.route().address().url().host())) {
return true;
}
如果从池中没有找到可复用的,则创建一个新的RealConnection对象,调用RealConnection.connect()
与目标主机建立Socket连接,并放入连接池。 将获取的连接赋值给Transmitter.connection属性,将在最后一个拦截器CallServerInterceptor
中,用来完成网络I/O。
类图
注意,处理每一个Request时,都会创建一个Transmitter、Interceptor.Chain。
二、响应缓存
启用缓存功能
OkHttp实现了一个可选的、默认关闭的缓存,在创建OkHttpClient
时可以启用它。
java
public static final File CACHE_DIRECTORY = new File("D:/okhttp/cache");
// 50MB
public static final long MAX_SIZE = 50L * 1024L * 1024L;
public static final OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(CACHE_DIRECTORY, MAX_SIZE))
.build();
缓存内容保存在本地磁盘,驱逐策略是LRU。
java
public final class Cache implements Closeable, Flushable {
// 缓存url、响应行、响应头的文件的后缀
private static final int ENTRY_METADATA = 0;
// 缓存请求体的文件后缀
private static final int ENTRY_BODY = 1;
private static final int ENTRY_COUNT = 2;
// 缓存(用磁盘文件)
final DiskLruCache cache;
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
// 省略......
}
缓存的实现
先来看看缓存现象。当执行一个查询请求时,如http://localhost:8010/user/queryAll
,该接口返回用户信息。会发现目录中创建了这些文件。其中,dac35e41fc4294e5d0737d4916452906是根据request.url()
计算获得的key。 dac35e41fc4294e5d0737d4916452906.0中内容包括请求url、响应行、响应头,如下。 dac35e41fc4294e5d0737d4916452906.1中内容是响应体,如下。
再来看源码,Cache类承担着新增、删除、驱逐、查找缓存等功能。
我们来看看查找缓存的逻辑。
java
// 计算key
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
// 查找缓存,命中时返回Response
Response get(Request request) {
// 用url计算key
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 {
// 从key.0文件读取响应行、响应头
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
// 从key.1文件读取响应体
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
GET请求命中缓存
在处理一个请求的拦截器链中,在ConnectInterceptor
之前是CacheInterceptor
。 new CacheInterceptor()
的入参是对InternalCache
接口的一个匿名实现,被Cache类
创建和持有,所有功能都由Cache
类承载。 CacheInterceptor
的拦截逻辑如下:
- 根据请求url查找响应缓存,创建缓存使用策略;
- 命中缓存且无需验证其有效性时,用缓存响应构建Response并返回;
- 未命中缓存,或命中了但需要向服务器验证其是否有效时,向服务器发起http请求;
- 如果状态码为304,即本地缓存未过期,将使用缓存作为响应;
- 如果状态码不是304,此时服务器会返回最新资源,则更新本地缓存。
java
@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) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// 发起Http请求
networkResponse = chain.proceed(networkRequest);
}
if (cacheResponse != null) {
// 服务器响应304: Not Modified。缓存仍有效
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());
}
}
// 构建响应
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
// 响应有响应体,且允许使用缓存(状态码满足要求、请求头和响应头Cache-Control不是no-store)时,将响应缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
// 非GET请求则移除缓存
if (HttpMethod.invalidatesCache(networkRequest.method())) {
cache.remove(networkRequest);
}
}
return response;
}
通过分析可知,OkHttp的缓存实现与浏览器端缓存大体一样,通过响应头Cache-control判断是否缓存、如何使用缓存,通过请求头If-Modified-Since、If-None-Match验证缓存是否有效。这一点在官网介绍中也有说明:
翻译:OkHttp实现了一个可选的,默认关闭的缓存。OkHttp的目标是RFC正确和实用的缓存行为,遵循现实世界中常见的浏览器(如Firefox/Chrome)和服务器行为。
对浏览器缓存感兴趣的小伙伴,可以浏览这篇文章《看懂浏览器缓存》。
GET请求缓存策略
CacheControl
类实现了对HTTP协议Cache-control请求头的封装。在创建某个请求时,可以设置不同的缓存策略。看看该类的属性,是不是与Cache-control头几乎一样。
- 跳过缓存,直接从服务器获取数据时,可以使用no-cache指令:
scss
Request request = new Request.Builder()
// 或者使用CacheControl.FORCE_NETWORK
.cacheControl(new CacheControl.Builder().noCache().build())
.url("http://publicobject.com/helloworld.txt")
.build();
- 需要使用缓存时强制向服务器验证缓存有效性时,可以使用max-age=0指令,它与no-cache不同之处在于,当服务器发现请求资源无更新时,响应状态码304即
Not Modified
且无响应体,提示客户端放心地使用缓存。
scss
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder().maxAge(0, TimeUnit.SECONDS).build())
.url("http://publicobject.com/helloworld.txt")
.build();
- 要限制请求只能使用本地缓存的资源时,可以添加only-if-cached指令:
scss
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder().onlyIfCached().build())
.url("http://publicobject.com/helloworld.txt")
.build();
Response forceCacheResponse = client.newCall(request).execute();
if (forceCacheResponse.code() != 504) {
// 命中缓存,使用它
} else {
// 资源未被缓存
}
- 当允许使用过期缓存作为响应时,使用max-stale指令,指定最大过期时间:
scss
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder().maxStale(10, TimeUnit.DAYS).build())
.url("http://publicobject.com/helloworld.txt")
.build();
当一个GET请求不设置缓存策略时,okhttp会根据请求头来生成CacheControl属性,其中部分属性默认值如图,可知此时也会保存响应、生成缓存文件 。
非GET请求是否缓存
对于非GET请求设置CacheControl,或者不主动设置CacheControl(此时noStore=false)时,是否会生成缓存文件呢?答案是不会的,来看源码。
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.
翻译:不要缓存非get响应。从技术上讲,我们可以缓存HEAD请求和一些POST请求,但是这样做的复杂性很高,而收益却很低。
当启用缓存功能时,对获取动态资源到请求,可主动为请求设置CacheControl.FORCE_NETWORK或new CacheControl.Builder().noStore().build()
;否则将生成缓存文件占用有限的缓存空间。