RFC 原文地址:
本文内容
- 概述
- 术语
- HTTP Cache-Control 头
- 可缓存的资源
- 可被高速缓存存储的资源
- 修改基本过期机制
- 缓存重新验证和重新加载的控制
- no-transform 指令
- 缓存控制扩展
- ASP.NET 缓存策略
- 参考资料
- 修改记录
概述
最近做的项目使用了 Ext.Net,由于之前看了很多介绍提高站点性能的文章,这类文章大多趋于理论,或是工程实践,都是三言两语,只能意会,没个直观的理解。很多东西还得靠自己。
开始关注性能问题源于我毕业之初做的一个项目,当时没什么经验,只是一味写代码,功能和设计至上,完全没考虑性能问题。另一部分原因是当时对 Web 应用程序的很多实现细节还不甚了解。知道 Why 和 What,但不知道 How。
这段时间,闲来无事,就针对 Ext.Net Web 应用程序利用网页测试和统计工具,比如 HttpWatch、YSLOW 等工具进行了一些测试。这些测试着实花了我很多时间,但收获巨大。
本文针对利用 HTTP 头 Cache-Control 的缓存问题。之前在网和 MSDN 上搜,没找到几个关于 Cache-Control 头的可能赋值,后来直接去找 RFC 文档,才豁然开朗。
以下是 RFC 2616 文档关于 Cache-Control 的部分,RFC 文档对一些,如 "MUST", "MUST NOT" 等,以及都做了明确的规定。本文文字实在太多,也不容易翻译。因为 RFC 文档侧重理论,使用的概念要比通常的叫法更广。比如用实体可以涵盖很多东西,页面、CSS、脚本、图像等等都可以认为是实体,但本文把它们当作是实体体,而包装后的实体体才叫实体。除了实体,还区分了实体体,实体体是实体的一部分,可以看作是实体的内容;信息概念更侧重于 HTTP 协议里边的相关东西;另外,区分了服务器(server)和源服务器(origin server)。
术语
以下是本文需要的术语(RFC 2616 文档还有其他一些术语)。本来不想列出来的,但想下还是有必要的——了解一个事物,首要的是能区分概念。
消息(message)
HTTP 通信的基本单元,它由一个结构化八进制序列组成,包括消息头、消息体、消息长度、通用消息头域等等(参看 ),并通过网络传输。
请求(request)
一个 HTTP 请求信息。
响应(response)
一个 HTTP 响应信息。
资源(resource)
可以由 URI(Uniform Resource Identifier)识别的一个网络数据对象或服务(参看 )。资源可以可靠的方式进行多种呈现(例如,多语言,数据格式、大小和解析),或是其他变化。
实体(entity)
信息作为一个请求或响应的有效载荷(payload)来传输。一个实体由以实体头域形式的元信息和以实体体形式的内容所组成。
实体(entity)即 HTTP 协议,实体头域(entity-header fields )即 HTTP 头,而实体体(entity-body)即 HTTP 体。
有效载荷的意思是,通常在传输数据时,为了使数据传输更可靠,要把原始数据分批传输,并且在每一批数据的头和尾加上一定辅助信息,比如该批数据量的大小,校验位等,这样为包装原始数据,使原始数据不易丢失,形成了传输通道中基本的传输单元——数据帧或数据包。这些数据帧中的原始数据就是有效载荷数据。
客户端(client)
一个为发送请求建立连接的程序。
用户代理(user agent)
初始化一个请求的客户端。这通常是浏览器、编辑器、网络蜘蛛(网页抓取程序),或其他的终端用户工具。
服务器/服务器端(server)
一个接收连接的应用程序,通过发回响应服务于请求。任何给定的程序都可能既是一个客户端,也是一个服务器。我们使用的这些术语仅指由程序执行一个特定连接的角色,而不是通常程序的功能。同样,基于每个请求的性质来切换行为,任何服务器都可以作为源服务器(origin server)、代理(proxy)、网关(gateway),或是管道(tunnel)。
服务器端或客户端主要是指它们在网络中所处的角色。
源服务器(origin server)
驻留或创建一个给定资源的服务器。换句话说,源服务器是存储资源的地方,比如存放静态资源的服务器,存放 Web 应用程序的服务器等,出于提高客户端响应速度的考虑,会将静态资源单独放到一个域名下。
代理(proxy)
一个作为服务器端和客户端,代表客户端发出请求的中间程序。请求在内部处理,或是通过翻译,把它们发送到其他服务器。代理必须要同时实现这个规范的客户端和服务器端需求。一个“透明代理”是一个不修改超出代理身份验证和识别要求的请求或响应的代理。一个“非透明代理”是一个修改请求或响应,为用户代理提供额外服务(增值服务)的代理,例如组注释服务,媒体类型转换,协议简化,或匿名过滤。除了透明或不透明的行为要明确指出外,HTTP 代理的需求要同时适用于两种类型的代理。
网关(gateway)
一个作为处理其他服务的中介的服务器。与代理不同,网关接收请求,就好像它是为请求资源的源服务器;请求的客户端并没有意识到,它们正在与网关通信。
也就是说,网关与代理都接收用户的请求。往往最终用户并没有意识到,其他它们正在与网关通信。
可缓存(cacheable)
如果一个缓存被允许存储响应信息的副本,以便在回答接下来的请求中使用,那么一个响应就是可缓存的。确定 HTTP 响应缓存能力的规则参看 。即使一个资源是可缓存的,也可能有其他限制,如一个缓存是否可以对一个特定请求使用已缓存的副本。
显式过期时间(explicit expiration time)
这个时间是,源服务器计划,一个实体无需进一步验证不再从缓存返回。
也就是说,当服务器发现请求中的过期时间表明,用户缓存中的资源已经过期,那就不能再从缓存获得资源。
绝对时间(age)
响应的绝对时间是,是自被源服务器发送,或成功验证的开始时间。
响应生命周期(freshness lifetime)
一个响应的产生与它过期时间之间的时间长度。
新的(fresh)
如果一个响应的年龄还没有超过它的响应生命周期,那么这个响应就是新的。
陈旧(stale)
如果一个响应已经超过它的响应生命周期,那么这个响应就是陈旧。
验证器(validator)
一个用于检查一个缓存条目是否为一个等价的实体副本的协议元素,例如一个实体标签(Etags),或是最后修改时间。用于验证一个实体是否过期的方法。
请求/响应链(request/response chain)
request chain ------------------------>UA -------------------v------------------- O<----------------------- response chain
共享缓存(shared cache)
代理服务器上缓存,供所有用户使用。
HTTP Cache-Control 头
Cache-Control 头用于指定缓存指令,所有请求/响应链的缓存机制必须遵守这个指令。该指令规定行为,意在防止缓存受到请求或响应的不利干扰。通常,这些指令可以覆盖默认的缓存算法。缓存指令是单向的,也就是说,在一个请求中存在缓存指令不意味着也在其响应中存在。
注意:HTTP/1.0 缓存没有实现 Cache-Control,只实现了 Pragma: no-cache。
不管缓存指令(Cache directives)对应用程序意义如何重大,它们必须通过代理(浏览器)或是网关应用程序传递,因为该指令可以应用与请求/响应链上的所有接收者。为一个特定的缓存规定缓存指令是不可能的。下面是 Cache-Control 的可能值。
Cache-Control = "Cache-Control" ":" 1#cache-directive
cache-directive = cache-request-directive
| cache-response-directive
cache-request-directive =
"no-cache"
| "no-store"
| "max-age" "=" delta-seconds
| "max-stale" [ "=" delta-seconds ]
| "min-fresh" "=" delta-seconds
| "no-transform"
| "only-if-cached"
| cache-extension
cache-response-directive =
"public"
| "private" [ "=" <;"> 1#field-name <"> ]
| "no-cache" [ "=" <;"> 1#field-name <"> ]
| "no-store"
| "no-transform"
| "must-revalidate"
| "proxy-revalidate"
| "max-age" "=" delta-seconds
| "s-maxage" "=" delta-seconds
| cache-extension
cache-extension = token [ "=" ( token | quoted-string ) ]
当指令中没有 1#field-name 字段名参数时,指令应用于整个请求或响应。当出现 1#field-name 参数时,它仅仅应用于命名的字段,而不会应用于请求或响应的其余部分。该机制支持扩展,以适应未来 HTTP 协议。
cache-control 指令,即以上可能的值,可以被划分成以下几类:
- 可缓存资源的限制。只能由源服务器完成。
- 可被缓存存储的限制。可以由源服务器或用户代理完成。
- 修改基本的过期机制。可以由源服务或用户代理完成。
- 控制缓存重新验证和重新加载。只能由用户代理完成。
- 控制实体传输。
- 扩展缓存系统。
可缓存的资源
默认情况下,如果请求方法、请求头部和响应状态的需要指示是可缓存的,那么一个响应就是可缓存的。概述了默认的缓存功能。下面 Cache-Control 响应指令允许源服务器覆盖一个响应默认的缓存功能:
public
指示响应可以被任何缓存所缓存,即使通常它只是非可缓存或可缓存到一个非共享缓存内。(参考 授权))
private
指示响应信息的全部或部分用于单个用户,而不能用一个共享缓存来缓存。这可以让源服务器指示,响应的特定部分只用于一个用户,而对其他用户的请求则是一个不可靠的响应。一个 private(非共享)缓存可以缓存这样的响应。
注意:使用 private 仅仅控制可以缓存响应的哪里,不能保证信息内容的隐私。
no-cache
如果 no-cache 指令没有规定 field-name,那么一个缓存不能使用响应以满足接下来的、没有与源服务器重新验证的请求。这可以让源服务器防止缓存,甚至是已被配置的缓存,返回给客户端陈旧的响应。
如果 no-cache 指令规定了一个或多个 field-names,那么一个缓存可以使用响应来满足接下来的请求,遵守缓存的其他限制。然而,指定的 field-name 参数不能在响应中被发送给接下来的、没有与源服务器成功重新验证的请求。这可以让源服务器防止重用响应中的某个头,而仍然可以缓存响应的其他部分。
可被缓存存储的资源
no-store
no-store 指令的目的是防止无心发布或是保留了敏感信息(例如,备份)。no-store 指令应用于整个信息,可以在响应或请求中发送。如果是在一个请求中发送,那么缓存不能存储这个请求或任何响应的任何部分给它。如果在一个响应中发送,那么缓存不能存储它引起的响应或请求的任何部分。这个指令可以应用于共享或非共享缓存。在上下文环境中,“不能存储”意思是缓存不能把信息有意地存储在非易失行性储器上,而且,在使用后,必须尽最大努力从易失存储上尽可能快地删除信息。
即使该指令与一个响应一起使用,用户也可能会显式地把这个响应存储到缓存系统之外(例如,"Save As" 对话框,或“导出”)。历史记录缓存可以把响应作为其正常操作的一部分来存储。
该指令的目的是为了满足某些用户和服务作者指定的要求,他们关心的是,通过意外地访问缓存的数据结构,导致的信息意外释放。当使用该指令可以在某些情况下提高隐私,但是需要注意的是,在某种程度上,它是不可靠的,或者说,是个不足以确保隐私的机制。特别是,恶意的缓存可能无法识别或遵守这个指令,这样通讯网络很容易会被窃听。
以上缓存控制指令对应 ASP.NET 的 枚举。
// 摘要:
// 提供用于设置 Cache-Control HTTP 标头的枚举值。
public enum HttpCacheability
{
// 摘要:
// 设置 Cache-Control: no-cache 标头。如果没有字段名,则指令应用于整个请求,且在满足请求前,共享(代理服务器)缓存必须对原始
// Web 服务器强制执行成功的重新验证。如果有字段名,则指令仅应用于命名字段;响应的其余部分可能由共享缓存提供。
NoCache = 1,
//
// 摘要:
// 默认值。设置 Cache-Control: private 以指定响应只能缓存在客户端,而不能由共享(代理服务器)缓存进行缓存。
Private = 2,
//
// 摘要:
// 指定响应仅缓存在源服务器上。与 System.Web.HttpCacheability.NoCache 选项相似。客户机接收 Cache-Control:
// no-cache 指令,但文档是在原始服务器上缓存的。等效于 System.Web.HttpCacheability.ServerAndNoCache。
Server = 3,
//
// 摘要:
// 应用 System.Web.HttpCacheability.Server 和 System.Web.HttpCacheability.NoCache
// 的设置指示在服务器上缓存内容,而对服务器以外的其他对象都显式否定其缓存响应的能力。
ServerAndNoCache = 3,
//
// 摘要:
// 设置 Cache-Control: public 以指定响应能由客户端和共享(代理)缓存进行缓存。
Public = 4,
//
// 摘要:
// 指示响应只能在服务器和客户端缓存。代理服务器不能缓存响应。
ServerAndPrivate = 5,
}
修改基本的过期机制
实体的过期时间可以由源服务器通过 Expires 头来指定(参考 )。另一个方法是,在响应中使用 max-age 指令。当一个已缓存的响应中存在 max-age 缓存指令时,如果当前的绝对时间大于一个新请求该资源的给定时间值,那么该响应就是陈旧的。响应中的 max-age 指令意味着,响应是可缓存的(即,"public"),除非其他的,还有更限制的缓存指令。
如果一个响应既包含 Expires 头,又包含 max-age 指令,那么 max-age 指令会覆盖 Expires 头,即使 Expires 头更有限制性。这个规则允许源服务器,对于一个给定响应,向 HTTP/1.1(或之后)缓存比 HTTP/1.0 提供一个更长的过期时间。如果某个 HTTP/1.0 缓存由于不同步的时钟而不当地计算绝对时间或过期时间,那么这个就会很有用。
很多 HTTP/1.0 缓存的实现会把小于等于响应日期值的过期值当作等价于 Cache-Control 响应指令 "no-cache"。如果一个 HTTP/1.1 缓存接收到这样的响应,并且响应不包含 Cache-Control 头,那么它会考虑把响应作为不可缓存,以便同 HTTP/1.0 兼容。
注意:源服务器可能希望在一个包含不能理解该指令的旧缓存的网络中使用一个相对较新的 HTTP 缓存控制功能,如 "private" 指令。源服务器会把这个新功能与过期结合起来,该过期的值小于等于日期值。这将防止陈旧的缓存不当地缓存的响应。
s-maxage
如果一个响应包含 s-maxage 指令,那么对于共享缓存(而不是对私有缓存),由该指令规定的最大绝对时间会覆盖由 max-age 指令或 Expires 头规定的最绝对时间。s-maxage 指令也隐含 proxy-revalidate 指令的语义(将在本文“控制缓存重新验证和重新加载”小节介绍),也就是说,当共享缓存对接下来的请求的响应变得陈旧后,该请求没有与源服务器重新验证,共享缓存不能使用缓存条目。私有缓存总是忽略 s-maxage 指令。
注意,大多数与上面规范不兼容的旧缓存没有实现任何 cache-control 指令。希望使用 cache-control 指令来限制的源服务器,而不妨碍 HTTP/1.1-compliant 缓存,可以使用 max-age 指令覆盖 Expires 头,事实上,HTTP/1.1-compliant 之前的缓存不检查 max-age 指令。
其他指令可以让用户代理修改基本的过期机制。这些指令可以在一个请求中规定:
max-age
指示客户端愿意接收其绝对时间不大于指定的时间,以秒计。除非还包含 max-stale 指令,否则客户端不期望接收一个陈旧的响应。
min-fresh
指示客户端愿意接收一个其响应生命周期不小于它当前绝对时间,再加上指定的时间的响应,以秒计。也就是说,客户端想要的一个响应,至少在指定的秒数是新的。
max-stale
指示客户端愿意接收一个已经超过其过期时间的响应。如果 max-stale 被分配一个值,那么客户端愿意接收已经超过其过期时间,不超过指定的秒数。如果没有分配给 max-stale 值,那么客户端愿意接收一个任何绝对时间的陈旧的响应。
如果缓存返回一个陈旧的响应,无论是因为一个请求中的 max-stale 指令,还是因为缓存被配置为覆盖一个响应的过期时间,那么,缓存必须把一个警告头 110 加到这个陈旧的响应。
一个缓存可以被配置为返回陈旧的响应,无需验证,但只有与任何需要 "MUST级别" 的缓存验证不冲突时(例如,一个 "must-revalidate" 缓存控制指令)。
如果这新请求和已缓存的条目都包含 "max-age" 指示,那么这两个值中较小的那个用于为请求确定已缓存条目的新的程度。
控制缓存重新验证和重加载
有时,用户代理可能想或需要坚持,一个缓存与源服务器(不仅仅沿着源服务器路径的下一缓存)重新验证它的缓存条目,或是从源服务器重新加载缓存条目。如果缓存或源服务器已过高的估计已缓存的响应的过期时间,那么可能需要点对点重新验证。如果缓存条目处于某种原因已经完全没有用处,那么可能需要点对点重新加载。
可以请求点对点重新验证,当客户端没有属于自己的本地已缓存的副本时,称为 "unspecified end-to-end revalidation",或是当客户端没有本地已缓存的副本时,称为 "specific end-to-end revalidation"。
客户端通过 Cache-Control 请求指令可以规定三种动作:
End-to-end reload
请求包含 "no-cache" 缓存控制指令,或是为了与 HTTP/1.0 客户端兼容的 "Pragma: no-cache"。请求中的 no-cache 指令不能包含 field-name。当响应这样一个请求时,服务器不能使用已缓存的副本。
Specific end-to-end revalidation
请求包含一个 "max-age=0" 缓存控制指令,这会迫使每个沿着源服务器路径的缓存,如果有,重新验证它自己的条目。初始请求包含一个带客户端当前的验证器的缓存验证条件。
Unspecified end-to-end revalidation
请求包含一个 "max-age=0" 缓存控制指令,这会迫使每个沿着源服务器路径的缓存,如果有,重新验证它自己的条目。初始请求不包含缓存验证条件;拥有此资源缓存条目的第一个沿路径的缓存(如果有)包含一个带客户端当前的验证器的缓存验证条件。
max-age
当利用 max-age=0 指令迫使一个中间缓存重新验证它自己的缓存条目,并且客户端已经在请求中提供它自己的验证器,那么,这个所提供的验证器可能与当前存储缓存条目的验证器不同。这种情况下,在不影响语义的情况下,缓存也可以使用它自己的请求中的验证器。
然而,验证器的选择可能会影响性能。对中间缓存(代理)来说,最好的方法是当自己发出请求时,使用它自己的验证器。如果服务器响应 304(Not Modified),那么缓存可以用一个 200 响应返回它已经验证的副本给客户端。然而,如果服务器用一个新的实体和缓存验证器回应,那么,中间缓存使用强比较函数把返回的验证器与客户端请求中提供的进行比较。如果客户端验证器与源服务器的相等,那么中间缓存(代理)就简单地返回 304((Not Modified)响应。否则,用一个 200(OK)响应返回新的实体。
如果一个请求包含 no-cache 指令,那么它不就应该包含 min-fresh、max-stale 或 max-age。
only-if-cached
在某些情况下,如网络连接非常差时,客户端可能需要一个缓存,只返回目前已存储的那些响应,而不是重新加载,或与源服务器重新验证。要做到这一点,客户端可以在一个请求中包含 only-if-cached 指令。如果服务器接收到这个指令,缓存应该,或是使用与其他请求限制一致的缓存项响应,或是用 504(Gateway Timeout)响应。但是,如果一个缓存组在一个统一的具有良好的网络连接的系统内被操作,这样一个请求可能会被在缓存组内转发。
must-revalidate
因为缓存可以配置成忽略服务器指定的过期时间,并且,因为一个客户端请求可以包含 max-stale 指令(具有类似的效果),对源服务器,协议还包括一个机制,需要在接下来的使用中重新验证缓存条目。当 must-revalidate 指令存在于一个已被缓存收到的响应时,响应接下来没有与源服务器初次重新验证的请求的条目变旧之后,缓存不能使用该条目。(即,在只基于源服务器 Expires 头或 max-age 值,如果已缓存的响应旧了,那么缓存必须每次完成一个点对点的重新验证。)
must-revalidate 指令对于某些协议功能的可靠运行是必需的。在任何情况下,HTTP/1.1 缓存必须遵守 must-revalidate 指令;特别是,如果缓存出于某种原因不能到达源服务器,那么它必须产生 504(Gateway Timeout)响应。
服务器应该发送 must-revalidate 指令,当且仅当没有成功重新验证一个实体的请求会导致不正确的操作,例如一个静默未执行的金融事务。接收者不能自动执行任何违反该指令的动作,并且,如果重新验证失败,不能自动提供一个未验证的实体副本。
虽然这是不推荐的,在严格连接限制操作下的用户代理可能会违反该指令,如果是这样,必须显式警告用户,提供了一个未经验证的响应。该警告必须提供给每个未经验证的访问,并且应该要求显式的用户确认信息。
proxy-revalidate
除了 proxy-revalidate 指令不能应用于非共享的用户代理缓存外,它与 must-revalidate 指令含义相同。proxy-revalidate 指令可以被用在响应一个已授权的请求,以便允许用户缓存存储,之后返回无需重新验证的响应(因为它已经被授权一次),但仍然需要代理来为用户重新验证(以确保每个用户已授权)。
注意,这种已授权的响应也需要 public 缓存控制指令,以便让它们完全被缓存。
以上指令对应 ASP.NET 的 枚举。
// 摘要:
// 提供用于设置重新验证特定的 Cache-Control HTTP 标头的枚举值。
public enum HttpCacheRevalidation
{
// 摘要:
// 将 HTTP 标头设置为 Cache-Control: must-revalidate。
AllCaches = 1,
//
// 摘要:
// 将 HTTP 标头设置为 Cache-Control: proxy-revalidate。
ProxyCaches = 2,
//
// 摘要:
// 如果已设置该值,则不发送任何缓存重新验证指令。默认值。
None = 3,
}
No-Transform 指令
no-transform
中间缓存(代理)的实施者已经发现它对转换某个实体体的媒体类型很有用。例如,一个非透明的代理把图像转换格式,以节省缓存空间,或是减少慢速链接中的通信量。
然而,当这些转换被应用到实体体以便某种应用时,就会发生严重的操作性问题。例如,医疗成像、科学数据分析,以及点对点授权的应用程序来说,所有这些都依赖于接收的实体体,每个比特都要与原实体体一致。
因此,如果一个消息包含 no-transform 指令,那么中间缓存或代理就不能改变头(参看 列出的)。这意味着,缓存或代理不能改变由头规定的实体体的任何方面,包括实体体自身的值。
缓存控制扩展
Cache-Control 头可以通过使用一个或多个 cache-extension 标记扩展,并为每个标记分配可选值。可以添加信息扩展(不需要改变缓存行为),而无需改变这些指令的语义。通过把现存的基本缓存指令作为修饰符,设计行为扩展。同时提供新指令和标准指令,不能理解新指令的应用程序将默认采用标准指令的行为,可以理解新指令的那些程序将其与标准指令一起修改要求。通过这种方式,无需改变基本协议,就可以扩展 cache-control 指令。
该扩展机制取决于遵守其本地 HTTP 版本中定义的所有缓存控制指令,以及一定程度的扩展,并忽略所有它不能理解的指令。
例如,假设一个新的称为 "community" 的响应指令,它作为一个 private 指令的修饰符。我们定义这个新指令的含义是,除了任何非共享缓存,只有由指定的 community 成员共享的任何缓存可以缓存响应。源服务器希望允许 UCI community 使用它们缓存中的 private 响应,按如下方式
Cache-Control: private, community="UCI"
一个看到这个头的缓存将执行正确的行为,即使缓存并不明白 community 缓存扩展,因为它也将看到和理解 private 指令,因此去执行默认的安全行为。
无法识别的缓存指令必须被忽略。它假定可能无法被 HTTP/1.1 缓存识别的任何缓存指令将被与标准指令(或响应默认的缓存功能)结合,这样,缓存行为将保持最低限度的正确性,即使缓存不能理解扩展。
ASP.NET 缓存策略
参考
参考资料
Wiki: Hypertext Transfer Protocol
RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
W3.org: A detailed technical history of HTTP
W3.org: Design Issues by Berners-Lee when he was designing the protocol
Wiki: List of HTTP header fields
httpwatch.com: HTTP Headers
microsoft.com: HTTP Response Headers:
RFC 4229: HTTP Header Field Registrations
Internet Explorer and Custom HTTP Headers - EricLaw's IEInternals - Site Home - MSDN Blogs
HTTP Request Header Viewer
HTTP Response Header Viewer - Retrieves the HTTP response headers of any domain
HTTP Header with Privacyinfo - Display your HTTP request and response headers
MSDN metal HTTP-EQUIV
修改记录
- 第一次 2011-01-04