HTTP/HTTPS 可以通过 HTTP Response 中的 header 字段(如 Cache-Control
、Expires
、Last-Modified
)告诉 client 是否需要对 response 进行缓存。有未过期的缓存,客户端可以使用缓存避免冗余的网络请求。
接下来探讨几个问题:
HTTP 协议如何利用 header 控制缓存的?
iOS(URLSession)对于非幂等的网络请求是否也支持本地缓存(如 POST)? 疑问来自于 rfc2616
Responses to this method are not cacheable, unless the response
includes appropriate Cache-Control or Expires header fields. However,
the 303 (See Other) response can be used to direct the user agent to
retrieve a cacheable resource.
- GET/POST 是否都会进行缓存
- 缓存由哪些 HTTP header 字段影响
- 边界场景
response 大于磁盘空间的5%不会触发willCacheResponse
参考
HTTP 缓存机制
利用缓存机制,HTTP 发请求可以可以分为3种情况:
- client 不发送 HTTP 请求, 直接使用本地的缓存。
- client 发送请求,server 校验请求头中的字段(如
If-Modified-Since
、If-None-Match
)并判断 client 缓存为有效,server 返回 304 状态码通知 client 使用本地缓存,304 的响应头也可以同时更新缓存文档的过期时间。 - 和场景2类似,如果如果 server 判断 client 的缓存已经失效会返回 200 状态码和新的响应数据,client 根据响应头更新本地的缓存。
控制缓存常用的 header 字段
1. Cache-Control
HTTP/1.1定义的
Cache-Control
头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。
在请求中使用:
字段 | 说明 |
---|---|
no-store | 忽略本地缓存也就是拿到 response 不进行保存,每次 client 发起的请求都会下载完整的响应内容 |
no-cache | 每次有请求发出时(即便是有未过期的本地缓存),带上相关字段 (如 If-Modified-Since 、If-None-Match ),server 端会验证请求中所描述的缓存是否过期,若未过期则返回 304 告知 client 使用本地缓存 |
only-if-cached | 告诉(代理)服务器,客户端希望获取缓存的内容(若有),而不用向源服务器发请求。 |
max-age | max-age= 表示资源能够被缓存(保持新鲜)的最大时间,是距离请求发起的时间的秒数。 |
在响应中使用:
字段 | 说明 |
---|---|
no-store | 不直接使用缓存,要求想服务器发起(新鲜度)请求 |
no-cache | 所有内容都不会被保存到缓存 |
must-revalidate | 告诉 client,本地缓存过期前,可以使用缓存;本地缓存一旦过期,必须去 server 进行有效性校验。 |
only-if-cached | 告诉(代理)服务器,客户端希望获取缓存的内容(若有),而不用向源服务器发请求。 |
max-age | 告诉客户端该资源新鲜度的时间周期 |
另外一些如 private
、public
字段比较少用,这里不再阐述。
2. Last-Modified
响应头可以作为一种弱校验器。说它弱是因为它只能精确到一秒。如果响应头里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since
/ If-Unmodified-Since
来验证缓存。
当向服务端发起缓存校验的请求时,服务端会返回 200
ok表示返回正常的结果或者 304
Not Modified(不返回body)表示浏览器可以使用本地缓存文件。304的响应头也可以同时更新缓存文档的过期时间。
- If-Modified-Since : 是一个条件式请求首部,服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为
200
。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的304
响应,而在Last-Modified
首部中会带有上次修改时间。 不同于If-Unmodified-Since
,If-Modified-Since
只可以用在GET
或HEAD
请求中。 - If-Unmodified-Since: HTTP协议中的
If-Unmodified-Since
消息头用于请求之中,使得当前请求成为条件式请求:只有当资源在指定的时间之后没有进行过修改的情况下,服务器才会返回请求的资源,或是接受POST
或其他 non-safe 方法的请求。如果所请求的资源在指定的时间之后发生了修改,那么会返回412
(Precondition Failed) 错误。
If-Modified-Since
和 If-Unmodified-Since
的区别是:
- If-Modified-Since 告诉服务器如果时间一致,返回状态码
304
- If-Unmodified-Since 告诉服务器如果时间不一致,返回状态码
412
3. Etag
ETag
HTTP响应头是资源的特定版本的标识符,类似于 hash 值。HTTP响应包会通过 Etag 把标识符告诉客户端。客户端下次请求时通过 If-None-Match
或 If-Match
带上该值,服务器对该值进行对比校验:如果一致则不要返回资源。
If-None-Match
和 If-Match
的区别是:
- If-None-Match:告诉服务器如果一致,返回状态码
304
,不一致则返回资源 - If-Match:告诉服务器如果不一致,返回状态码
412
有了 Last-Modified 为什么还需要 Etag ?
ETag 是为了解决 Last-modified 的一些缺陷:
一些资源的最后修改时间变了,但是内容没有改变,造成荣誉的请求
Last-modified 只能精确到秒级,如果一秒钟文件被多次修改,可能会导致客户端不能及时更新缓存数据。
Etag
是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。**Last-Modified
与ETag
是可以一起使用的,服务器会优先验证ETag
,一致的情况下,才会继续比对Last-Modified
,最后才决定是否返回 304**。
URLSession 的缓存表现
URLSession 提供的缓存机制有一下几种,可以通过 NSURLSessionConfiguration/NSRequest 对象进行:
1 | typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy) |
本文目的是验证 iOS 的 post 请求是否也会进行缓存,所以这里使用默认的缓存策略,也就是 NSURLRequestUseProtocolCachePolicy
验证步骤分2步:
- app 使用 get 请求,回包分别返回
Cache-control
、Last-Modified
、ETag
的表现 - app 使用 post 请求,回包分别返回
Cache-control
、Last-Modified
、ETag
的表现
为了方便验证,服务端使用 beego 搭建本地简易 server
URLSession GET 请求缓存
客户端主要代码:
1 | - (void)viewDidLoad { |
Cache-Control
- 服务端代码,设置回包数据的新鲜度为 60 秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29func (c *GetController) Get() {
for key, strings := range c.Ctx.Request.Header {
beego.Debug(key, "=", strings)
}
// Cache-Control: max-age=60
interval := 60
maxAge := "max-age=" + strconv.Itoa(interval)
c.Ctx.Output.Header("Cache-Control",maxAge)
c.Data["json"] = &jsonData{
10086,
"message",
}
c.ServeJSON()
}
func (c *PostController) Post() {
for key, strings := range c.Ctx.Request.Header {
beego.Debug(key, "=", strings);
}
interval := 60
maxAge := "max-age=" + strconv.Itoa(interval)
c.Ctx.Output.Header("Cache-Control",maxAge)
c.Data["json"] = &jsonData{
10010,
"message",
}
c.ServeJSON()
}- 请求结果
- GET:60秒内客户端重复触发请求,从第二次开始,会使用本地缓存,不会发出网络请求
- POST:客户端每次会发出网络请求,不会进行缓存
Last-Modified:
服务端代码,设置回包数据的修改时间为 1609430400 秒(2021/1/1 00:00:00)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31func (c *GetController) Get() {
// ...略
// 这里简单处理,客户端如果带上If-Modified-Since,则都返回304告诉其用本地缓存
ims := c.Ctx.Request.Header.Get("If-Modified-Since")
if len(ims) > 0 {
c.Ctx.ResponseWriter.WriteHeader(304)
return
}
// 标记资源过期时间为2020/01/01 00:00:00
c.Ctx.Output.Header("Last-Modified", "Fri, 01 Jan 2021 00:00:00 GMT")
// ...略
}
func (c *PostController) Post() {
// ...略
// 这里简单处理,客户端如果带上If-Modified-Since,则都返回304告诉其用本地缓存
ims := c.Ctx.Request.Header.Get("If-Modified-Since")
if len(ims) > 0 {
c.Ctx.ResponseWriter.WriteHeader(304)
return
}
// 标记资源过期时间为2020/01/01 00:00:00
c.Ctx.Output.Header("Last-Modified", "Fri, 01 Jan 2021 00:00:00 GMT")
// ...略
}请求结果:
- GET:客户端每次都会发出网路请求;第一次响应收到 server 返回的 Last-Modified, 之后的请求中会通过 If-Modified-Since 客户端缓存的修改日期给到 server 端;若 server 端返回 304,则客户端使用本地的缓存,并且 URLSession 向上返回 200 的状态码。
- POST: 和 GET 表现一致
ETag
服务端代码:利用 ETag 把资源进行标记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37func (c *MainController) Get() {
//...略
ims := c.Ctx.Request.Header.Get("If-Modified-Since")
if len(ims) > 0 {
c.Ctx.ResponseWriter.WriteHeader(304)
return
}
inm := c.Ctx.Request.Header.Get("If-None-Match")
if len(inm) > 0 {
c.Ctx.ResponseWriter.WriteHeader(304)
} else {
c.Ctx.Output.Header("ETag", "ETagValueGet")
}
c.Data["json"] = &jsonData{
10086,
"etag-message-get"+inm,
}
c.ServeJSON()
}
func (c *PostController) Post() {
inm := c.Ctx.Request.Header.Get("If-None-Match")
if len(inm) > 0 {
c.Ctx.ResponseWriter.WriteHeader(304)
} else {
c.Ctx.Output.Header("ETag", "ETagValuePost")
}
c.Data["json"] = &jsonData{
10010,
"etag-message-post"+inm,
}
c.ServeJSON()
}请求结果:
- GET: 客户端每次都会发出网路请求;第一次响应收到 server 返回的 ETag, 之后的请求中会通过 If-None-Match 客户端缓存的标记给到 server 端;若 server 端返回 304,则客户端使用本地的缓存,并且 URLSession 向上返回 200 的状态码。
- POST: 和 GET 表现一致
结论
URLSession 使用 NSURLRequestUseProtocolCachePolicy 策略的缓存表现可以概括为:
- 如果 server 端通过
Cache-Control
告诉客户端资源新鲜度,则客户端在资源未过期之前,不会发出网络请求,直接使用本地缓存,只作用于 GET 请求。 - server 端台利用
Last-Modified
标记资源修改时间或者利用 ETag 标记资源版本,下次客户端重新发出网络请求,携带上If-Modified-Since
/If-None-Match
,server 端会通过 HTTP 状态码 200 / 304 告诉客户端是否可以使用本地的缓存,GET / POST 请求表现一致。
回到文章开头的问题:(URLSession)非幂等的网络请求是否也支持缓存(如 POST)?
从上文中简单的测试表现看,结论是不支持的,和 RFC 文档描述存在差异。于是网上搜索相关问题
How to cache an HTTP POST response?
Is it possible to cache POST methods in HTTP?
问题底下的回复其中提及到:POST 请求支持缓存,但是客户端如 Firefox 浏览器实现中不支持
Note, however many browsers, including current Firefox 3.0.10, will not cache POST response regardless of the headers. IE behaves more smartly in this respect.
然后再去查阅 mozilla 的相关文档,POST 请求利用 Content-Location 字段也是可以进行资源新鲜度描述并进行缓存,但是一般很少应用支持。Cacheable
The method used in the request is itself cacheable, that is either a
GET
or aHEAD
method. A response to aPOST
orPATCH
request can also be cached if freshness is indicated and theContent-Location
header is set, but this is rarely implemented. (For example, Firefox does not support it per https://bugzilla.mozilla.org/show_bug.cgi?id=109553.) Other methods, likePUT
orDELETE
are not cacheable and their result cannot be cached.
而 Content-Location
定义是:
Content-Location
首部指定的是要返回的数据的地址选项。最主要的用途是用来指定要访问的资源经过内容协商后的结果的URL。
结合我们的测试表现,只有通过 GET 请求响应包携带的 Cache-Control
才能实现让 URLSession 不发出请求直接使用本地缓存。
参考文章: