重新认识HTTP/1.1
重新认识HTTP/1.1
可以说,我们浏览网页,下载资源,甚至克隆一个感兴趣的github仓库,都在与HTTP协议打交道。但是,在计算机网络课程和考研中HTTP都不作为重点去讲述,而在面试和实际工作中却经常需要接触。因此更深入的了解HTTP协议显得尤为重要。上一部分主要讲了HTTP最基本的东西,包括HTTP的结构,以及连接管理,状态保存和用户认证相关的知识。上述内容基本不脱离课程所学,属于是我们必须熟知的最最基本的东西。这一部分主要从HTTP的发展过程来进行HTTP的学习,标题也改成了“重新认识HTTP”。
总览
HTTP是由Tim Berners-Lee 博士和他的团队在 1989-1991 年间创造出它以来,三十多年来,HTTP 已经发生了太多的变化,在保持协议简单性的同时,不断扩展其灵活性。如今,HTTP 已经从一个只在实验室之间交换文件的早期协议进化到了可以传输图片,高分辨率视频和 3D 效果的现代复杂互联网协议。这篇文章简要的概括了HTTP的发展历史。同时下图也简要的概括了HTTP发展过程中发生的重大变化。
HTTP最初的样子
最初版本的 HTTP 协议并没有版本号,后来它的版本号被定位在
0.9
以区分后来的版本。HTTP/0.9
极其简单:请求由单行指令构成,以唯一可用方法GET开头,其后跟目标资源的路径(一旦连接到服务器,协议、服务器、端口号这些都不是必须的)。
GET /mypage.html
响应也极其简单的:只包含响应文档本身。
<html>
这是一个非常简单的 HTML 页面
</html>
跟后来的版本不同,HTTP/0.9
的响应内容并不包含 HTTP
头。这意味着只有 HTML
文件可以传送,无法传输其他类型的文件。也没有状态码或错误代码。一旦出现问题,一个特殊的包含问题描述信息的
HTML 文件将被发回,供人们查看。
可以看出,这样简单的协议从一开始更像是局域网中获取资源的一种约定。
HTTP1.0
HTTP/0.9
几乎没有什么可拓展性,而HTTP/1.0
增加的这些特性,可以很明显的看出希望HTTP协议的用途更加广泛,并且便于后续进行拓展。
- 协议版本信息现在会随着每个请求发送(
HTTP/1.0
被追加到了GET
行)。 - 状态码会在响应开始时发送,使浏览器能了解请求执行成功或失败,并相应调整行为(如更新或使用本地缓存)。
- 引入了 HTTP 标头的概念,无论是对于请求还是响应,允许传输元数据,使协议变得非常灵活,更具扩展性。
- 在新 HTTP 标头的帮助下,具备了传输除纯文本 HTML
文件以外其他类型文档的能力(凭借
Content-Type
标头)。
HTTP1.1
HTTP/1.1
消除了大量歧义内容并引入了多项改进:
- 连接可以复用,节省了多次打开 TCP 连接加载网页文档资源的时间。
- 增加流水线技术,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。
- 支持响应分块。
- 引入额外的缓存控制机制。
- 引入内容协商机制,包括语言、编码、类型等。并允许客户端和服务器之间约定以最合适的内容进行交换。
- 凭借
Host
标头,能够使不同域名配置在同一个 IP 地址的服务器上。
连接复用和流水线技术已经在认识HTTP(二)中有所讲述,这里再继续讲解一些其他的一些机制。
缓存策略
关于控制缓存策略的参数,最好的参考资料还是第一手资料。这里对常见的控制策略进行讲解,以便更快的理解常见的情形。
- 首先,浏览器端会根据
cache-control
是否是no-store
来判断是否可以对返回的数据进行缓存,如果是no-store
表示不允许缓存,之后的请求都不会走缓存,而是重新想向务器端发送请求。 - 如果不是
no-store
,一般情况下就是只返回max-age: xxx
来告诉浏览器端可以对数据进行缓存,并且设置缓存的失效时间,通过max-age
有时候会搭配no-cache
或者must-revalidate
一起返回,no-cache
和must-revalidate
就是控制要去服务器端进行验证数据是否真的有变化。关于两者的区别后面会讲述。 - 那如何验证变化呢?就是借助
Last-Modified/if-Modified-Since
,或者ETag/If-None-Match
来判断,如果确实有变化,则返回最新数据,如果没有变化,则返回304
,同时更新缓存的失效时间。
下面对上述提到的概念进行详细的表述,可以对照查看。
缓存控制
缓存控制是通过cache-control
这个字段进行的。
服务器
max-age
:表示缓存在几s后会失效,是一个相对时间,但是要注意的是,该时间是从响应报文创建的时间就开始计时。no-store
: 表示不允许缓存,通常一些频繁变化的页面,需要设置该选项。no-cache
: 该字段表示允许缓存,但是使用缓存之前必须要先去服务器端验证是否过期,如果没过期,则使用缓存,如果过期了,则返回最新数据。must-revalidate
: 表示允许缓存,并且如果缓存不过期的话,先使用缓存,如果缓存过期的话,再去服务器端进行验证,(如果验证有变化,则返回最新资源,如果验证没变化,则返回304 Not Modified
,然后更新max-age
的失效时间。同时注意,并不是缓存过期了浏览器就一定需要删除缓存)。
客户端
服务器端如何在响应头中添加响应的字段来浏览来是否可以使用缓存,同样,客户端自己也可以控制,即浏览器也可以在请求中中添加cache-control
等字段。
- 浏览器刷新
即我们按F5刷新页面的时候,该页面的http请求中会添加:cache-control:max-age:0
。注意这时候如果服务器的缓存控制策略没有must-revalidate
的话,浏览器还可能继续使用这个缓存,异步地发送一个条件性
GET
请求(带有If-Modified-Since
或If-None-Match
头部),以验证资源是否仍然有效。然后如果服务器返回
304
响应,表示资源仍然有效,浏览器会再更新一次过期时间。但是由于这个过程是“先斩后奏”的,用户大部分时候感觉不到这个过程,打开F12也会发现“已缓存”的字样。
- 浏览器强制刷新
即我们按ctrl+f5强制刷新页面的时候,该页面的http请求会添加:cache-control:no-cache
;
即表示此时要首先去服务器端验证资源是否有更新,如果有更新则直接返回最新资源,如果没有更新,则返回304,然后浏览器端判断是304的话,则从缓存中读取数据。
- 浏览器前进后退重定向
当我们点击浏览器的前进后退操作时,这个时候请求中不会有cache-control
的字段,没有该字段,就表示会检查缓存,直接利用之前的资源,不再重新请求服务器。
另外,expires
是HTTP1.0时的头选项。浏览器会优先使用cache-control
来判断缓存是否过期。如果只返回了expires
,则浏览器会将expires
转换为cache-control
来判断缓存是否过期。如果同时返回了cache-control
和expires
两个响应头,浏览器会优先使用cache-control
来判断缓存是否过期
缓存验证
浏览器判断顶多是根据服务器端返回的失效时间去判断,这样并不一定准确,因为很可能出现缓存失效的情况,但其实资源并没有发生变化,这个时候其实也是应该走缓存的,那如何判断资源有没有发生变化呢?这必须交给服务器端来判断了。下面介绍通过Last-Modified/If-Modified-Since
,和ETag/If-None-Match
两种策略判断。后者是优先于前者的。
Last-Modified/If-Modified-Since
即该字段是服务器端返回给客户端的响应头字段,表示当前请求的资源的最后修改时间,如果响应头中有该字段,那么下次请求的时候,请求头中就会包含If-Modified-Since
字段,它的值就是Last-Modified
的值,这样服务器端收到该字段的值,就可以和对应的资源最终的修改时间做对比,如果发生变化,则说明资源发生了变化,则返回最新资源(此时状态码是200),如果没有发生变化,则返回304,浏览器从缓存中直接去数据即可。
ETag/If-None-Match
使用资源的最后更改时间作为判断资源是否更改可能会有问题?比如:资源改了之后,又改了回来,这时虽然资源的最后修改时间发生了变化,但其实资源内容本身没有发生变化,其实这种情况也应该是走缓存的,所以才出现了ETag
字段,表示资源的唯一标识,那如果响应头中有该字段,则下次请求的时候,请求头中就会有If-None-Match
字段,它的值就是ETag
的值,服务器端收到以后,就会和当前资源的唯一表识别去对比,如果不一样,则说明资源发生变化,返回最新数据即可(此时状态码是200),如果一样,则说明资源没有变化,返回304,浏览器从缓存中读取数据。
内容协商
概述
我们在抓包或者写爬虫时经常注意到有很多以Accept
开头的请求首部,还有q=xxx
之类的东西,这些个东西都是什么意思?
以Accept-Language
为例,我们容易猜测到是用户浏览器向服务器请求的语言选项。一个URL常常需要代表若干不同的资源。例如那种需要以多种语言提供其内容的网站站点。如果某个站点有说中文的和说英语的两种用户,它可能想用这两种语言提供网站站点信息。理想情况下,服务器应当向英语用户发送英文版,向中文用户发送中文版——用户只要访问网站主页就可以得到相应语言的内容。
HTTP提供了内容协商方法,允许客户端和服务器作这样的决定。通过这些方法,单一的URL就可以代表不同的资源(比如,同一个网站页面的中文版和英语版),这些不同的版本称为变体。
方式
一共有3种不同的方法可以决定服务器上哪个页面最适合客户端:客户端来选择、服务器自动判定、让中间代理来选。这三种技术分别称为客户端驱动的协商、服务器驱动的协商以及透明协商。
客户端驱动
客户端发起请求,服务器发送返回一个页面,该页面包含指向该资源所有可用表示的链接,客户端作出选择后再发送第二次请求。
- 优点:比较自然,服务器提供内容,由客户进行选择,减少Header冗余
- 缺点:增加了时延,至少要发送两次请求,第一次请求获取资源列表,第二次获取选择的副本;同时HTTP 标准也没有明确指定提供可选资源链接的页面的格式,无法进行无感知的自动化选择。
服务器驱动
服务器检查客户端的请求首部集并决定提供哪个版本的页面。
- 优点:比客户端驱动的协商要快。HTTP提供了q机制,允许服务器近似匹配。
- 缺点:首部集不匹配,服务器要做猜测;
目前浏览器是采用服务器驱动的协商策略。当然服务器仍然可以在页面上提供语言选项供用户手动选择,就像大多数多语言的网页一样。
内容协商首部
客户端
客户端可以用下面列出的内容协商首部集发送用户的偏好信息:
Accept:告知服务器发送何种媒体类型;
Accept-Language:告知服务器发送何种语言;
Accept-Charset:告知服务器发送何种字符集;
Accept-Encoding:告知服务器采用何种编码;
服务器
服务器用下面列出的实体首部集来匹配客户端的Accept首部集:
Accept首部 实体首部
Accept Content-Type
Accept-Language Content-Language
Accept-Charset Content-Type
Accept-Encoding Content-Encoding
同时,实体首部也不是必须的。比如,如果没有
Content-Language
,则默认为内容适用于所有语言受众。
q值
这里的q是指Quality values。
q值的范围从0.0~1.0(1.0优先级最高)
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
这个首部表示:用户最愿意接受中文(zh),繁体的也行,英文次之。其他语言不接受(或以q=0.0表示)。如果是表示不特指的话是会以星号*
表示。
vary
前面提到,客户端驱动是很自然的思路,也就是服务端提供,客户选择。实际广泛采用的情况是服务端驱动,也就是客户端提供多种接受的选项,服务器提供一个最符合的。
但是,服务端有时候只能提供一种内容,比如我就只有中文版的网页供用户查看。有而且,实际上大多数时候会存在中间缓存服务器来缓解内容提供服务器的压力。vary
就主要用在这种情形下。
HTTP的vary
响应首部中列出了所有客户端请求首部,缓存服务器可以用这些首部来选择文档或者产生定制的内容。比如:若给客户端的响应内容取决于Accept-Encoding
,vary
首部就必须包含Accept-Encoding
。回到上面说的情况,既然只有中文版的网页,vary
首部就不包含Accept-Language
,这样缓存服务器就不用根据用户的Accept-Language
来进行内容选择了,可以省很多工作。
相应的,为了实现透明协商,缓存服务器必须为每个已缓存变体保存客户端请求首部和相应的服务器响应首部。也就是指明vary
的内容,就必须要缓存。
实例
以访问我自己的博客为例:
请求头(客户端):
响应头(服务器)
vary
字段为Accept-Encoding
。
更多
内容协商策略为改善互联网用户的体验,提高传输效率提供了很大的帮助。但是内容协商使HTTP头增大了不少,而且在每一次请求中都必须发送这些首部。在首部很少的时候,这并不是问题,但是随着数量的增多,消息体的体积会导致性能的下降。带有精确信息的首部发送的越多,信息熵就会越大,也就准许了更多 HTTP 指纹识别行为,以及与此相关的隐私问题的发生。另外,如果希望更详细的了解内容协商算法,可以参见Apache 服务器的内容协商算法。
总结
这一部分主要介绍了HTTP发展历史的前面部分,重点对HTTP/1.1的缓存控制策略和内容协商进行了讲解。后面会结合实践深入探索HTTP/2的内容。