HTTP 协议中 Vary 的一些研究
经常抓包看 HTTP 请求的同学应该对 Vary 这个响应头字段并不陌生,它有什么用?用 PageSpeed 工具检查页面时,经常看到「Specify a Vary: Accept-Encoding header(请指定一个 Vary: Accept-Encoding 标头)」这样的建议,为什么要这样做?本文记录我对 Vary 的一些研究,其中就包含这些问题的答案。
HTTP 内容协商
要了解 Vary 的作用,先得了解 HTTP 的内容协商机制。有时候,同一个 URL 可以提供多份不同的文档,这就要求服务端和客户端之间有一个选择最合适版本的机制,这就是内容协商。
协商方式有两种,一种是服务端把文档可用版本列表发给客户端让用户选,这可以使用 300 Multiple Choices 状态码来实现。这种方案有不少问题,首先多一次网络往返;其次服务端同一文档的某些版本可能是为拥有某些技术特征的客户端准备的,而普通用户不一定了解这些细节。举个例子,服务端通常可以将静态资源输出为压缩和未压缩两个版本,压缩版显然是为支持压缩的客户端而准备的,但如果让普通用户选,很可能选择错误的版本。
所以 HTTP 的内容协商通常使用另外一种方案:服务端根据客户端发送的请求头中某些字段自动发送最合适的版本。可以用于这个机制的请求头字段又分两种:内容协商专用字段(Accept 字段)、其他字段。
首先来看 Accept 字段,详见下表:
请求头字段 | 说明 | 响应头字段 |
---|---|---|
Accept | 告知服务器发送何种媒体类型 | Content-Type |
Accept-Language | 告知服务器发送何种语言 | Content-Language |
Accept-Charset | 告知服务器发送何种字符集 | Content-Type |
Accept-Encoding | 告知服务器采用何种压缩方式 | Content-Encoding |
例如客户端发送以下请求头:
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,en-US;q=0.8,en;q=0.6
表示它可以接受任何 MIME 类型的资源;支持采用 gzip、deflate 或 sdch 压缩过的资源;可以接受 zh-CN、en-US 和 en 三种语言,并且 zh-CN 的权重最高(q 取值 0 - 1,最高为 1,最低为 0,默认为 1),服务端应该优先返回语言等于 zh-CN 的版本。
浏览器的响应头可能是这样的:
Content-Type: text/javascript
Content-Encoding: gzip
表示这个文档确切的 MIME 类型是 text/javascript;文档内容进行了 gzip 压缩;响应头没有 Content-Language 字段,通常说明返回版本的语言正好是请求头 Accept-Language 中权重最高的那个。
有时候,上面四个 Accept 字段并不够用,例如要针对特定浏览器如 IE6 输出不一样的内容,就需要用到请求头中的 User-Agent 字段。类似的,请求头中的 Cookie 也可能被服务端用做输出差异化内容的依据。
由于客户端和服务端之间可能存在一个或多个中间实体(如缓存服务器),而缓存服务最基本的要求是给用户返回正确的文档。如果服务端根据不同 User-Agent 返回不同内容,而缓存服务器把 IE6 用户的响应缓存下来,并返回给使用其他浏览器的用户,肯定会出问题 。
所以 HTTP 协议规定,如果服务端提供的内容取决于 User-Agent 这样「常规 Accept 协商字段之外」的请求头字段,那么响应头中必须包含 Vary 字段,且 Vary 的内容必须包含 User-Agent。同理,如果服务端同时使用请求头中 User-Agent 和 Cookie 这两个字段来生成内容,那么响应中的 Vary 字段看上去应该是这样的:
Vary: User-Agent, Cookie
也就是说 Vary 字段用于列出一个响应字段列表,告诉缓存服务器遇到同一个 URL 对应着不同版本文档的情况时,如何缓存和筛选合适的版本。
有 BUG 的缓存服务
再来看 PageSpeed 的「Specify a Vary: Accept-Encoding header」这个提示,按照上面的说明,Accept-Encoding 属于内容协商专用字段,服务端只需要在响应头中增加 Content-Encoding 字段,用来指明内容压缩格式;或者不输出 Content-Encoding 表明内容未经过压缩就可以了。而缓存服务器,应该针对不同的 Content-Encoding 缓存不同内容,再根据具体请求中的 Accept-Encoding 字段返回最合适的版本。
但是有些实现得有 BUG 的缓存服务器,会忽略响应头中的 Content-Encoding,从而可能给不支持压缩的客户端返回缓存的压缩版本。有两个方案可以避免这种情况发生:
- 将响应头中的 Cache-Control 字段设为 private,告诉中间实体不要缓存它;
- 增加 Vary: Accept-Encoding 响应头,明确告知缓存服务器按照 Accept-Encoding 字段的内容,分别缓存不同的版本;
通常为了更好的利用中间实体的缓存功能,我们都用第二种方案。
对于 css、js 这样的静态资源,只要客户端支持 gzip,服务端应该总是启用它;同时为了避免有 BUG 的缓存服务器给用户返回错误的版本,还应该输出 Vary: Accept-Encoding。
Nginx 和 SPDY
通常,上面说的这些工作,Web Server 都可以帮我们搞定。对于 Nginx 来说,下面这个配置可以自动给启用了 gzip 的响应加上 Vary: Accept-Encoding:
gzip_vary on;
用 curl 验证我博客的 js 文件,响应头如下:
jerry@www:~$ curl --head https://www.imququ.com/.../xx.js
HTTP/1.1 200 OK
Server: nginx
Date: Tue, 31 Dec 2013 16:34:48 GMT
Content-Type: application/x-javascript
Content-Length: 66748
Last-Modified: Tue, 31 Dec 2013 14:30:52 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag: "52c2d51c-104bc"
Expires: Fri, 29 Dec 2023 16:34:48 GMT
Cache-Control: max-age=315360000
Strict-Transport-Security: max-age=31536000
Accept-Ranges: bytes
可以看到,服务端正确输出了「Vary: Accept-Encoding」,一切正常。
但是用 Chrome 自带抓包工具看下,这个响应头却是这样:
HTTP/1.1 200 OK
cache-control: max-age=315360000
content-encoding: gzip
content-type: application/x-javascript
date: Tue, 31 Dec 2013 16:35:27 GMT
expires: Fri, 29 Dec 2023 16:35:27 GMT
last-modified: Tue, 31 Dec 2013 14:30:52 GMT
server: nginx
status: 200
strict-transport-security: max-age=31536000
version: HTTP/1.1
我的博客支持 SPDY/2 协议,用 Chrome 访问我博客会走 SPDY,所以上面的响应头看上有点不同寻常,例如字段名都变成了小写;多了 status、version 等字段,这些变化下次专门介绍(注:见「SPDY 3.1 中的请求 / 响应头」)。神奇的是尽管服务端没任何变化,但响应中的 Vary: Accept-Encoding 却不见了。
SPDY 规定客户端必须支持压缩,这意味着 SPDY 服务器可以直接启用压缩而不用关心请求头中的 Accept-Encoding 字段。下面这段来自 Nginx 支持的 SPDY/2 协议:
User-agents are expected to support gzip and deflate compression. Regardless of the Accept-Encoding sent by the user-agent, the server may select gzip or deflate encoding at any time. [via]
于是,对于支持 SPDY 的客户端来说,Vary: Accept-Encoding 没有用途,Nginx 选择直接去掉它,可以节省一点流量。curl 或其他不支持 SPDY 协议的客户端还是走 HTTP 协议,所以看到的响应头是常规的。
Nginx 的这个做法是否合适一直有争论,实际上并不是所有支持 SPDY 的 Web Server 都会这么做。例如即使通过 SPDY 协议访问 Google 首页的 js 文件,依然可以看到 vary: Accept-Encoding:
HTTP/1.1 200 OK
status: 200 OK
version: HTTP/1.1
age: 25762
alternate-protocol: 443:quic
cache-control: public, max-age=31536000
content-encoding: gzip
content-length: 154614
content-type: text/javascript; charset=UTF-8
date: Tue, 31 Dec 2013 23:23:51 GMT
expires: Wed, 31 Dec 2014 23:23:51 GMT
last-modified: Mon, 16 Dec 2013 21:54:35 GMT
server: sffe
vary: Accept-Encoding
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
另外,现阶段 Chrome 和 Firefox 都支持 SPDY 协议,但 PageSpeed Chrome 版和 Firefox 版都没有针对 SPDY 协议做特别处理,所以用它们测试我的博客,还是会提示「Specify a Vary: Accept-Encoding header」,这有点让人哭笑不得。不过 PageSpeed 在线版 已经更新规则,估计扩展版也快了。
PS:Vary 在 IE 下有很多坑,使用时要格外小心。网上这部分文章比较多,例如 hax 早年写的 IE 与 Vary 头,可以点过去了解下。
本文链接:https://www.imququ.com/post/vary-header-in-http.html
HTTP 协议中 Vary 的一些研究的更多相关文章
- C#的HTTP协议中POST与GET的区别
引言 HTTP协议我想任何IT人士都耳熟能详了,大家都能说出个所以然来.但是如果我问你HTTP协议的请求方法有哪些?POST与GET的差异?GET或POST传送数据量的大小有限制吗?HTTP响应的状态 ...
- HTTP 协议中的 Content-Encoding 和 Transfer-Encoding(内容编码和传输编码)
转自:http://network.51cto.com/art/201509/491335.htm Transfer-Encoding,是一个 HTTP 头部字段,字面意思是「传输编码」.实际上,HT ...
- HTTP协议中状态码的应用
HTTP状态码(HTTP Status Code)是用以表示网页服务器HTTP响应状态的3位数字代码. 所有状态码的第一个数字代表了响应的五种状态之一. Mark from 维基百科 消息 ...
- HTTP协议中PUT/GET/POST/HEAD等介绍
HTTP协议中GET.POST和HEAD的介绍 GET: 请求指定的页面信息,并返回实体主体. HEAD: 只请求页面的首部. POST: 请求服务器接受所指定的文档作为对所标识的URI的新的从属实体 ...
- HTTP协议中的短轮询、长轮询、长连接和短连接
HTTP协议中的短轮询.长轮询.长连接和短连接 引言 最近刚到公司不到一个月,正处于熟悉项目和源码的阶段,因此最近经常会看一些源码.在研究一个项目的时候,源码里面用到了HTTP的长轮询.由于之前没太接 ...
- Http协议中Get和Post的浅谈
起名困难户,每次写文章最愁的就是不知道该如何起个稍具内涵的名字,如果这篇文章我只是写写Get和Post的区别,我可以起个名字“Get和Post的那点事”,如果打算阐述一下Http协议原理性内容,那该叫 ...
- HTTP 协议中的 Transfer-Encoding
HTTP 协议中的 Transfer-Encoding 文章目录 Persistent Connection Content-Length Transfer-Encoding: chunked 本文作 ...
- http协议中content-length 以及chunked编码分析
转载请注明出处 http://blog.csdn.net/yankai0219/article/details/8269922 0.序 1.http/1.1协议中与chunked编码的相关字段 1)E ...
- [转]HTTP 协议中的 Transfer-Encoding
本文作为我的博客「HTTP 相关」专题新的一篇,主要讨论 HTTP 协议中的 Transfer-Encoding.这个专题我会根据自己的理解,以尽量通俗的讲述,结合代码示例和实际场景来说明问题,欢迎大 ...
随机推荐
- 《精通Python设计模式》学习之建造者模式
这种模式,就是将一个最终对象分级分层建造出来. 在软件发布过程中,不同的JAVA,PYTHON,NODE.JS,ZIP压缩包, 就可以使用不同的阶段来使用建造者模式的. from enum impor ...
- Chrome-Adobe Flash 无法正常使用
https://support.google.com/chrome/answer/6258784 该网站因是是google.com,被强了,所以一般打不开. 故将google官方说明记录以下: 如果 ...
- Android 客户端 okhttp3 与服务器之间的双向验证
[原文]https://blog.csdn.net/leng_wen_rou/article/details/58596142 本篇是Android 客户端基于okhttp3的网络框架 和后台服务器之 ...
- drools7 (三、kmodule 介绍)
注意几点: 1. kmodule 有默认的加载路径, 看 KieModuleModelImpl 这个类的源代码 public class KieModuleModelImpl implements ...
- 关于《C++ Templates》
最近买了<C++ Templates>来看,书最开始部分阐述了关于此书的一些编程风格.本人感觉非常好,有些地方之前一直容易搞混,这里却讲述的很清楚.例如: 关于下面几种风格的代码: voi ...
- LoadRunner 一参多用
LoadRunner参数化后的值在脚本中多处位置引用(LoadRunner 一参多用) LoadRunner的参数化给了我们很多便利,但是当一个脚本中同一个值出现多处,并且值都是一致的.这个时候, ...
- 004 关于Java如何扫描指定package下所有的类
q前言: 在工作中看到这个知识点,就顺便参考了百度的一些资料,整理一下,希望以后用的到. 一:理论部分 1.使用场景 写一个MVC框架,需要从包中扫描出组件并注册到容器中,而JDK没有提供现成的从方法 ...
- OSPF详解
OSPF 详解 (1) [此博文包含图片] (2013-02-04 18:02:33) 转载 ▼ 标签: 端的 第二 以太 第一个 正在 目录 序言 初学乍练 循序渐进学习OSPF 朱皓 入门之前 了 ...
- 深入理解SQL的四种连接,左外连接,右外连接,内连接,全连接
1.内联接(典型的联接运算,使用像 = 或 <> 之类的比较运算符).包括相等联接和自然联接. 内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行.例如,检索 stude ...
- Java异常处理中的恢复模型
异常处理理论上有两种基本模型.Java支持终止模型,在这种模型中,假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行.一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行.长久以来,尽管 ...