本文由
玉刚说写做平台
提供写做赞助java原做者:
竹千代
面试版权声明:本文版权归微信公众号
玉刚说
全部,未经许可,不得以任何形式转载数据库
Http是咱们常常打交道的网络应用层协议,它的重要性可能不须要再强调。可是实际上不少人,包括我本身可能对http了解的并不够深。本文就我本身的学习心得,分享一下我认为须要知道的缓存所涉及到的相关知识点。浏览器
首先咱们来点基础的,看看http报文具体的格式。http报文能够分为请求报文和响应报文,格式大同小异。主要分为三个部分:缓存
请求报文格式:安全
<method> <request-url> <version>
<headers>
<entity-body>
复制代码
响应报文格式bash
<version> <status> <reason-phrase>
<headers>
<entity-body>
复制代码
从请求报文格式和响应报文格式能够看出,二者主要在起始行上有差别。这里稍微解释一下各个标签:服务器
<method> 指请求方法,经常使用的主要是Get、 Post、Head 还有其余一些咱们这里就不说了,有兴趣的能够本身查阅一下
<version> 指协议版本,如今一般都是Http/1.1了
<request-url> 请求地址
<status> 指响应状态码, 咱们熟悉的200、404等等
<reason-phrase> 缘由短语,200 OK 、404 Not Found 这种后面的描述就是缘由短语,一般没必要太关注。
复制代码
咱们知道请求方法最经常使用的有Get 和Post两种,面试时也经常会问到这二者有什么区别,一般什么状况下使用。这里咱们来简单说一说。微信
两个方法之间在传输形式上有一些区别,经过Get方法发起请求时,会将请求参数拼接在request-url尾部,格式是url?param1=xxx¶m2=xxx&[...]。网络
咱们须要知道,这样传输参数会使得参数都暴露在地址栏中。而且因为url是ASCII编码的,因此参数中若是有Unicode编码的字符,例如汉字,都会编码以后传输。另外值得注意的是,虽然http协议并无对url长度作限制,可是一些浏览器和服务器可能会有限制,因此经过GET方法发起的请求参数不可以太长。而经过POST方法发起的请求是将参数放在请求体中的,因此不会有GET参数的这些问题。
另一点差异就是方法自己的语义上的。GET方法一般是指从服务器获取某个URL资源,其行为能够看做是一个读操做,对同一个URL进行屡次GET并不会对服务器产生什么影响。而POST方法一般是对某个URL进行添加、修改,例如一个表单提交,一般会往服务器插入一条记录。屡次POST请求可能致使服务器的数据库中添加了多条记录。因此从语义上来说,二者也是不能混为一谈的。
常见的状态码主要有
200 OK 请求成功,实体包含请求的资源
301 Moved Permanent 请求的URL被移除了,一般会在Location首部中包含新的URL用于重定向。
304 Not Modified 条件请求进行再验证,资源未改变。
404 Not Found 资源不存在
206 Partial Content 成功执行一个部分请求。这个在用于断点续传时会涉及到。
在请求报文和响应报文中均可以携带一些信息,经过与其余部分配合,可以实现各类强大的功能。这些信息位于起始行之下与请求实体之间,以键值对的形式,称之为首部。每条首部以回车换行符结尾,最后一个首部额外多一个换行,与实体分隔开。
这里咱们重点关注一下
Date
Cache-Control
Last-Modified
Etag
Expires
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match
Http的首部还有不少,但限于篇幅咱们不一一讨论。这些首部都是Http缓存会涉及到的,在下文中咱们会来讲说各自的做用。
请求发送的资源,或是响应返回的资源。
当咱们发起一个http请求后,服务器返回所请求的资源,这时咱们能够将该资源的副本存储在本地,这样当再次对该url资源发起请求时,咱们能快速的从本地存储设备中获取到该url资源,这就是所谓的缓存。缓存既能够节约没必要要的网络带宽,又能迅速对http请求作出响应。
先摆出几个概念:
- 新鲜度检测
- 再验证
- 再验证命中
咱们知道,有些url所对应的资源并非一成不变的,服务器中该url的资源可能在必定时间以后会被修改。这时本地缓存中的资源将与服务器一侧的资源有差别。
既然在必定时间以后可能资源会改变,那么在某个时间以前咱们能够认为这个资源没有改变,从而放心大胆的使用缓存资源,当请求时间超过来该时间,咱们认为这个缓存资源可能再也不与服务器端一致了。因此当咱们发起一个请求时,咱们须要先对缓存的资源进行判断,看看究竟咱们是否能够直接使用该缓存资源,这个就叫作新鲜度检测
。即每一个资源就像一个食品同样,拥有一个过时时间,咱们吃以前须要先看看有没有过时。
若是发现该缓存资源已经超过了必定的时间,咱们再次发起请求时不会直接将缓存资源返回,而是先去服务器查看该资源是否已经改变,这个就叫作再验证
。若是服务器发现对应的url资源并无发生变化,则会返回304 Not Modified
,而且再也不返回对应的实体。这称之为再验证命中
。相反若是再验证未命中,则返回200 OK
,并将改变后的url资源返回,此时缓存能够更新以待以后请求。
咱们看看具体的实现方式:
- 新鲜度检测
咱们须要经过检测资源是否超过必定的时间,来判断缓存资源是否新鲜可用。那么这个必定的时间怎么决定呢?实际上是由服务器经过在响应报文中增长Cache-Control:max-age
,或是Expire
这两个首部来实现的。值得注意的是Cache-Control是http1.1的协议规范,一般是接相对的时间,即多少秒之后,须要结合last-modified
这个首部计算出绝对时间。而Expire是http1.0的规范,后面接一个绝对时间。
- 再验证
若是经过新鲜度检测发现须要请求服务器进行再验证,那么咱们至少须要告诉服务器,咱们已经缓存了一个什么样的资源了,而后服务器来判断这个缓存资源究竟是不是与当前的资源一致。逻辑是这样没错。那怎么告诉服务器我当前已经有一个备用的缓存资源了呢?咱们能够采用一种称之为条件请求
的方式实现再验证。
- Http定义了5个首部用于条件请求:
If-Modified-Since
If-None-Match
If-Unmodified-Since
If-Range
If-Match
If-Modified-Since 能够结合Last-Modified
这个服务器返回的响应首部使用,当咱们发起条件请求时,将Last-Modified首部的值做为If-Modified-Since首部的值传递到服务器,意思是查询服务器的资源自从咱们上一次缓存以后是否有修改。
If-None-Match 须要结合另外一个Etag
的服务器返回的响应首部使用。Etag首部实际上能够认为是服务器对文档资源定义的一个版本号。有时候一个文档被修改了,可能所作的修改极为微小,并不须要全部的缓存都从新下载数据。或者说某一个文档的修改周期极为频繁,以致于以秒为时间粒度的判断已经没法知足需求。这个时候可能就须要Etag这个首部来代表这个文档的版号了。发起条件请求时可将缓存时保存下来的Etag的值做为If-None-Match首部的值发送至服务器,若是服务器的资源的Etag与当前条件请求的Etag一致,代表此次再验证命中。
其余三个与断点续传涉及到的相关知识有关,本文暂时不讨论。待我以后写一篇文章来说讲断点续传。
缓存的Http理论知识大体就是这么些。咱们从OkHttp的源码来看看,这些知名的开源库是如何利用Http协议实现缓存的。这里咱们假设读者对OkHttp的请求执行流程有了大体的了解,而且只讨论缓存相关的部分。对于OkHttp代码不熟悉的同窗,建议先看看相关代码或是其余文章。
咱们知道OkHttp的请求在发送到服务器以前会通过一系列的Interceptor,其中有一个CacheInterceptor便是咱们须要分析的代码。
final InternalCache cache;
@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;
......
}
复制代码
方法首先经过InternalCache 获取到对应请求的缓存。这里咱们不展开讨论这个类的具体实现,只须要知道,若是以前缓存了该请求url的资源,那么经过request对象能够查找到这个缓存响应。
将获取到的缓存响应,当前时间戳和请求传入CacheStrategy,而后经过执行get方法执行一些逻辑最终能够获取到strategy.networkRequest,strategy.cacheResponse。若是经过CacheStrategy的判断以后,咱们发现此次请求没法直接使用缓存数据,须要向服务器发起请求,那么咱们就经过CacheStrategy为咱们构造的networkRequest来发起此次请求。咱们先来看看CacheStrategy作了哪些事情。
CacheStrategy.Factory.java
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);
}
}
}
}
复制代码
CacheStrategy.Factory的构造方法首先保存了传入的参数,并将缓存响应的相关首部解析保存下来。以后调用的get方法以下
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; } 复制代码
get方法很简单,主要逻辑在getCandidate中,这里的逻辑是若是返回的candidate所持有的networkRequest不为空,表示咱们此次请求须要发到服务器,此时若是请求的cacheControl要求本次请求只使用缓存数据。那么此次请求恐怕只能以失败了结了,这点咱们等会儿回到CacheInterceptor中能够看到。接着咱们看看主要getCandidate的主要逻辑。
private CacheStrategy getCandidate() {
// No cached response.
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake. 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);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
......
}
复制代码
上面这段代码主要列出四种状况下须要忽略缓存,直接想服务器发起请求的状况:
这些状况下直接构造一个包含networkRequest,可是cacheResponse为空的CacheStrategy对象返回。
private CacheStrategy getCandidate() {
......
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
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());
}
......
}
复制代码
若是缓存响应的Cache-Control首部包含immutable,那么说明该资源不会改变。客户端能够直接使用缓存结果。值得注意的是immutable并不属于http协议的一部分,而是由facebook提出的扩展属性。
以后分别计算ageMills、freshMills、minFreshMills、maxStaleMills这四个值。
若是响应缓存没有经过Cache-Control:No-Cache 来禁止客户端使用缓存,而且
ageMillis + minFreshMillis < freshMillis + maxStaleMillis
复制代码
这个不等式成立,那么咱们进入条件代码块以后最终会返回networkRequest为空,而且使用当前缓存值构造的CacheStrtegy。
这个不等式到底是什么含义呢?咱们看看这四个值分别表明什么。
ageMills 指这个缓存资源自响应报文在源服务器中产生或者过时验证的那一刻起,到如今为止所通过的时间。用食品的保质期来比喻的话,比如当前时间距离生产日期已通过去了多久了。
freshMills 表示这个资源在多少时间内是新鲜的。也就是假设保质期18个月,那么这个18个月就是freshMills。
minFreshMills 表示我但愿这个缓存至少在多久以后依然是新鲜的。比如我是一个比较讲究的人,若是某个食品只有一个月就过时了,虽然并无真的过时,但我依然以为食品不新鲜从而不想再吃了。
maxStaleMills比如我是一个不那么讲究的人,即便食品已通过期了,只要不是过时好久了,好比2个月,那我以为问题不大,还能够吃。
minFreshMills 和maxStatleMills都是由请求首部取出的,请求能够根据本身的须要,经过设置
Cache-Control:min-fresh=xxx、Cache-Control:max-statle=xxx
复制代码
来控制缓存,以达到对缓存使用严格性的收紧与放松。
private CacheStrategy getCandidate() {
......
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
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); // No condition! Make a regular request.
}
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);
}
复制代码
若是以前的条件不知足,说明咱们的缓存响应已通过期了,这时咱们须要经过一个条件请求对服务器进行再验证操做。接下来的代码比较清晰来,就是经过从缓存响应中取出的Last-Modified
,Etag
,Date
首部构造一个条件请求并返回。
接下来咱们返回CacheInterceptor
// If we're forbidden from using the network and the cache is insufficient, fail. 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(); } 复制代码
能够看到,若是咱们返回的networkRequest
和cacheResponse
都为空,说明咱们即没有可用的缓存,同时请求经过Cache-Control:only-if-cached
只容许咱们使用当前的缓存数据。这个时候咱们只能返回一个504的响应。接着往下看,
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
复制代码
若是networkRequest为空,说明咱们不须要进行再验证了,直接将cacheResponse做为请求结果返回。
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());
}
}
// If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { 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(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). 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) { if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { // Offer this request to the cache. CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); } if (HttpMethod.invalidatesCache(networkRequest.method())) { try { cache.remove(networkRequest); } catch (IOException ignored) { // The cache cannot be written. } } } return response; 复制代码
若是networkRequest存在不为空,说明此次请求是须要发到服务器的。此时有两种状况,一种cacheResponse不存在,说明咱们没有一个可用的缓存,此次请求只是一个普通的请求。若是cacheResponse存在,说明咱们有一个可能过时了的缓存,此时networkRequest是一个用来进行再验证的条件请求。
无论哪一种状况,咱们都须要经过networkResponse=chain.proceed(networkRequest)获取到服务器的一个响应。不一样的只是若是有缓存数据,那么在获取到再验证的响应以后,须要cache.update(cacheResponse, response)去更新当前缓存中的数据。若是没有缓存数据,那么判断这次请求是否能够被缓存。在知足缓存的条件下,将响应缓存下来,并返回。
OkHttp缓存大体的流程就是这样,咱们从中看出,整个流程是遵循了Http的缓存流程的。最后咱们总结一下缓存的流程:
OAuth是一个用于受权第三方获取相应资源的协议。与以往的受权方式不一样的是,OAuth的受权能避免用户暴露本身的用户密码给第三方,从而更加的安全。OAuth协议经过设置一个受权层,以区分用户和第三方应用。用户自己能够经过用户密码登录服务提供商,获取到帐户全部的资源。而第三方应用只能经过向用户请求受权,获取到一个Access Token,用以登录受权层,从而在指定时间内获取到用户受权访问的部分资源。
OAuth定义的几个角色:
Role | Description |
---|---|
Resource Owner | 能够受权访问某些受保护资源的实体,一般就是指用户 |
Client | 能够经过用户的受权访问受保护资源的应用,也就是第三方应用 |
Authorization server | 在认证用户以后给第三方下发Access Token的服务器 |
Resource Server | 拥有受保护资源的服务器,能够经过Access Token响应资源请求 |
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
复制代码
从上图能够看出,一个OAuth受权的流程主要能够分为6步:
简单的说 Http + 加密 + 认证 + 完整性保护 = Https
传统的Http协议是一种应用层的传输协议,Http直接与TCP协议通讯。其自己存在一些缺点:
所以,在一些须要保证安全性的场景下,好比涉及到银行帐户的请求时,Http没法抵御这些攻击。
Https则能够经过增长的SSL\TLS,支持对于通讯内容的加密,以及对通讯双方的身份进行验证。
近代密码学中加密的方式主要有两类:
对称秘钥加密是指加密与解密过程使用同一把秘钥。这种方式的优势是处理速度快,可是如何安全的从一方将秘钥传递到通讯的另外一方是一个问题。
非对称秘钥加密是指加密与解密使用两把不一样的秘钥。这两把秘钥,一把叫公开秘钥,能够随意对外公开。一把叫私有秘钥,只用于自己持有。获得公开秘钥的客户端可使用公开秘钥对传输内容进行加密,而只有私有秘钥持有者自己能够对公开秘钥加密的内容进行解密。这种方式克服了秘钥交换的问题,可是相对于对称秘钥加密的方式,处理速度较慢。
SSL\TLS的加密方式则是结合了两种加密方式的优势。首先采用非对称秘钥加密,将一个对称秘钥使用公开秘钥加密后传输到对方。对方使用私有秘钥解密,获得传输的对称秘钥。以后双方再使用对称秘钥进行通讯。这样即解决了对称秘钥加密的秘钥传输问题,又利用了对称秘钥的高效率来进行通讯内容的加密与解密。
SSL\TLS采用的混合加密的方式仍是存在一个问题,即怎么样确保用于加密的公开秘钥确实是所指望的服务器所分发的呢?也许在收到公开秘钥时,这个公开秘钥已经被别人篡改了。所以,咱们还须要对这个秘钥进行认证的能力,以确保咱们通讯的对方是咱们所指望的对象。
目前的作法是使用由数字证书认证机构颁发的公开秘钥证书。服务器的运营人员能够向认证机构提出公开秘钥申请。认证机构在审核以后,会将公开秘钥与共钥证书绑定。服务器就能够将这个共钥证书下发给客户端,客户端在收到证书后,使用认证机构的公开秘钥进行验证。一旦验证成功,便可知道这个秘钥是能够信任的秘钥。
总结 Https的通讯流程: