先回顾一下HTTP的相关知识
HTTP response status codes
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误
HTTP headers
先贴一张图:
Accept
请求头用来告知客户端可以处理的内容类型,这种内容类型用MIME类型来表示。
Content-Type
实体头部用于指示资源的MIME类型 media type 。在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。
Date
是一个通用首部,其中包含了消息生成的日期和时间。
Expires
指定了一个日期/时间, 在这个日期/时间之后,HTTP响应被认为是过时的;无效的日期,比如 0, 代表着一个过去的事件,即该资源已经过期了。如果还有一个 设置了 “max-age” 或者 “s-max-age” 指令的Cache-Control响应头,那么 Expires 头就会被忽略。
The Last-Modified
是一个响应首部,其中包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。
Server
首部包含了处理请求的源头服务器所用到的软件相关信息。
Age
消息头里包含消息对象在缓存代理中存贮的时长,以秒为单位。Age消息头的值通常接近于0。表示此消息对象刚刚从原始服务器获取不久;其他的值则是表示代理服务器当前的系统时间与此应答消息中的通用消息头 Date 的值之差。
Cache-control
通用消息头被用于在http 请求和响应中通过指定指令来实现缓存机制。HTTP/1.1定义的 Cache-Control 头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。缓存指令是单向的, 这意味着在请求设置的指令,在响应中不一定包含相同的指令。
- 缓存请求指令
客户端可以在HTTP请求中使用的标准 Cache-Control 指令。
- 缓存响应指令
服务器可以在响应中使用的标准 Cache-Control 指令。
相关指令解释:
- 可缓存性
- 到期
- 其他
例如:
- 禁止进行缓存
缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。
Cache-Control: no-store
Cache-Control: no-cache, no-store, must-revalidate
强制确认缓存
如下头部定义,此方式下,每次有请求发出时,缓存会将此请求发到服务器(译者注:该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。Cache-Control: no-cache
缓存过期机制
过期机制中,最重要的指令是max-age=<seconds>
,表示资源能够被缓存(保持新鲜)的最大时间。相对Expires而言,max-age是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js等静态资源。Cache-Control: max-age=31536000
Pragma
HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,但是HTTP的响应头不支持这个属性,所以它不能拿来完全替代HTTP/1.1中定义的Cache-control头。通常定义Pragma以向后兼容基于HTTP/1.0的客户端。
ETags
HTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖(“空中碰撞”)。
Accept-Encoding
列出用户代支持的压缩方法
HTTP request methods
GET
请求指定的资源,使用 GET 的请求应该只用于获取数据。
POST
响应的消息体中包含此次请求的结果。
HEAD
请求资源的首部信息, 并且这些首部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源。
以上内容摘录自HTTP文档,想了解完整内容的请点击我。
我们看看怎么在okhttp中使用Cache,下面的例子来自官方文档。
public final class CacheResponse {
private final OkHttpClient client;
public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
client = new OkHttpClient.Builder()
.cache(cache)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
String response1Body;
try (Response response1 = client.newCall(request).execute()) {
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());
}
String response2Body;
try (Response response2 = client.newCall(request).execute()) {
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());
}
System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}
public static void main(String... args) throws Exception {
new CacheResponse(new File("CacheResponse.tmp")).run();
}
}
缓存的策略可以通过Cache-Control来配置:
- 在某些情况下,如用户单击“刷新”按钮,就可能有必要跳过缓存,并直接从服务器获取数据。要强制刷新,添加无缓存的指令:”Cache-Control”: “no-cache”。
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder().noCache().build())
.url("http://publicobject.com/helloworld.txt")
.build();
- 如果缓存只是用来和服务器做验证的,那么可以设置”Cache-Control”:”max-age=0”。
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder()
.maxAge(0, TimeUnit.SECONDS)
.build())
.url("http://publicobject.com/helloworld.txt")
.build();
- 有时你会想立刻显示一些数据在界面。这样你的应用程序就可以在等待最新的数据下载时显示一些东西。那么这时我们需要重定向request到本地缓存资源,添加”Cache-Control”:”only-if-cached”。
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) {
// The resource was cached! Show it.
} else {
// The resource was not cached.
}
缓存控制请求指示只使用缓存,即使缓存的响应是过时的。如果响应在缓存中没有可用,或者需要服务器验证,那么调用将会以504无法满足的请求失败。
- 有时候过期的response比没有response更好,设置最长过期时间来允许过期的response来响应。
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder()
.maxStale(365, TimeUnit.DAYS)
.build())
.url("http://publicobject.com/helloworld.txt")
.build();
如果服务端有Cache-Control这个标签,那么我们就可以按照上面的去设置。没有的话,那就另当别论了。我们来看看缓存的策略是怎么样的。
图片来源于网络
Okhttp的缓存的操作都是在HttpEngine
中完成的,完成Cache有这些关键的步骤:
- Cache:内部维护了一个
DiskLruCache
,负责将Cache写到文件系统中。 - CacheStrategy:指定request和response来描述是通过网络还是缓存获取response,或者是二者同时使用。
这里我只简单说一说关于网络缓存的存取策略,Cache实现比较复杂,我也说不清楚。
当我执行execute()
方法时,会调用Call.getResponse(Request request, boolean forWebSocket)
执行Engine.sendRequest()
和Engine.readResponse()
两个方法。
Engine.sendRequest()
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
if (transport != null) throw new IllegalStateException();
Request request = networkRequest(userRequest);
InternalCache responseCache = Internal.instance.internalCache(client);
//获取缓存
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
long now = System.currentTimeMillis();
//获取缓存策略
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
//如果有缓存,那么就更新一下统计的东西。Request Count,Network Count,Hit Count
if (responseCache != null) {
responseCache.trackResponse(cacheStrategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
if (networkRequest != null) {
// Open a connection unless we inherited one from a redirect.
if (connection == null) {
//连接到服务器
connect();
}
transport = Internal.instance.newTransport(connection, this);
// If the caller's control flow writes the request body, we need to create that stream
// immediately. And that means we need to immediately write the request headers, so we can
// start streaming the request body. (We may already have a request body if we're retrying a
// failed POST.)
if (callerWritesRequestBody && permitsRequestBody() && requestBodyOut == null) {
long contentLength = OkHeaders.contentLength(request);
if (bufferRequestBody) {
if (contentLength > Integer.MAX_VALUE) {
throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
+ "setChunkedStreamingMode() for requests larger than 2 GiB.");
}
if (contentLength != -1) {
// Buffer a request body of a known length.
transport.writeRequestHeaders(networkRequest);
requestBodyOut = new RetryableSink((int) contentLength);
} else {
// Buffer a request body of an unknown length. Don't write request
// headers until the entire body is ready; otherwise we can't set the
// Content-Length header correctly.
requestBodyOut = new RetryableSink();
}
} else {
transport.writeRequestHeaders(networkRequest);
requestBodyOut = transport.createRequestBody(networkRequest, contentLength);
}
}
} else {
// 我们没有使用网络,回收重定向连接
if (connection != null) {
Internal.instance.recycle(client.getConnectionPool(), connection);
connection = null;
}
if (cacheResponse != null) {
// 我们有一个有效的缓存响应。应该立即响应给客户端。
this.userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
} else {
// 我们被禁止使用网络,而且缓存还不够。
this.userResponse = new Response.Builder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_BODY)
.build();
}
userResponse = unzip(userResponse);
}
}
- 缓存策略
CacheStrategy
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// 若本地没有缓存,发起网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 如果当前请求是HTTPS,而缓存没有TLS握手,重新发起网络请求
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//如果当前的缓存策略是不缓存或者是conditional get,发起网络请求
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//返回当前缓存响应的age单位毫秒
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;
CacheControl responseCaching = cacheResponse.cacheControl();
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) {
//缓存不新鲜了,但是缓存继续可以使用,只是在头部添加 110 警告码
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
//响应的使用期超过24小时(有效缓存的设定时间大于24小时的情况下)
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// 发起conditional get请求
Request.Builder conditionalRequestBuilder = request.newBuilder();
if (etag != null) {
conditionalRequestBuilder.header("If-None-Match", etag);
} else if (lastModified != null) {
conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
} else if (servedDate != null) {
conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
}
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest)
? new CacheStrategy(conditionalRequest, cacheResponse)
: new CacheStrategy(conditionalRequest, null);
}
readResponse()
是最终发起缓存的地方。
水平有限,源码还是不分析了。不想作死,对RFC协议不了解,看源码也是费劲。