重新认识HTTP(三) | 青训营

142 阅读9分钟

重新认识HTTP(三)

可以说,我们浏览网页,下载资源,甚至克隆一个感兴趣的github仓库,都在与HTTP协议打交道。但是,在计算机网络课程和考研中HTTP都不作为重点去讲述,而在面试和实际工作中却经常需要接触。因此更深入的了解HTTP协议显得尤为重要。上一部分主要讲了HTTP连接管理,状态保存和用户认证相关的知识。这一部分主要学习一下HTTP的发展过程,并重点介绍关于HTTP1.1新增的缓存策略。

总览

HTTP是由Tim Berners-Lee 博士和他的团队在 1989-1991 年间创造出它以来,三十多年来,HTTP 已经发生了太多的变化,在保持协议简单性的同时,不断扩展其灵活性。如今,HTTP 已经从一个只在实验室之间交换文件的早期协议进化到了可以传输图片,高分辨率视频和 3D 效果的现代复杂互联网协议。这篇文章简要的概括了HTTP的发展历史。同时下图也简要的概括了HTTP发展过程中发生的重大变化。

image-20230826164236772

HTTP最初的样子

最初版本的 HTTP 协议并没有版本号,后来它的版本号被定位在 0.9 以区分后来的版本。HTTP/0.9 极其简单:请求由单行指令构成,以唯一可用方法GET开头,其后跟目标资源的路径(一旦连接到服务器,协议、服务器、端口号这些都不是必须的)。

GET /mypage.html

响应也极其简单的:只包含响应文档本身。

<html>
  这是一个非常简单的 HTML 页面
</html>

跟后来的版本不同,HTTP/0.9 的响应内容并不包含 HTTP 头。这意味着只有 HTML 文件可以传送,无法传输其他类型的文件。也没有状态码或错误代码。一旦出现问题,一个特殊的包含问题描述信息的 HTML 文件将被发回,供人们查看。

可以看出,这样简单的协议从一开始更像是局域网中获取资源的一种约定。

HTTP1.0

HTTP0.9几乎没有什么可拓展性,而HTTP1增加的这些特性,可以很明显的看出希望HTTP协议的用途更加广泛,并且便于后续进行拓展。

  • 协议版本信息现在会随着每个请求发送(HTTP/1.0 被追加到了 GET 行)。
  • 状态码会在响应开始时发送,使浏览器能了解请求执行成功或失败,并相应调整行为(如更新或使用本地缓存)。
  • 引入了 HTTP 标头的概念,无论是对于请求还是响应,允许传输元数据,使协议变得非常灵活,更具扩展性。
  • 在新 HTTP 标头的帮助下,具备了传输除纯文本 HTML 文件以外其他类型文档的能力(凭借Content-Type 标头)。

HTTP1.1

HTTP/1.1 消除了大量歧义内容并引入了多项改进:

  • 连接可以复用,节省了多次打开 TCP 连接加载网页文档资源的时间。
  • 增加流水线技术,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。
  • 支持响应分块。
  • 引入额外的缓存控制机制。
  • 引入内容协商机制,包括语言、编码、类型等。并允许客户端和服务器之间约定以最合适的内容进行交换。
  • 凭借 Host标头,能够使不同域名配置在同一个 IP 地址的服务器上。

连接复用和流水线技术已经在重新认识HTTP(二)中有所讲述,这里再继续讲解一些其他的一些机制。

缓存策略

关于控制缓存策略的参数,最好的参考资料还是第一手资料。这里对常见的控制策略进行讲解,以便更快的理解常见的情形。

  1. 首先,浏览器端会根据cache-control是否是no-store来判断是否可以对返回的数据进行缓存,如果是no-store表示不允许缓存,之后的请求都不会走缓存,而是重新想向务器端发送请求。
  2. 如果不是no-store,一般情况下就是只返回max-age: xxx来告诉浏览器端可以对数据进行缓存,并且设置缓存的失效时间,通过max-age有时候会搭配no-cache或者must-revalidate一起返回,no-cachemust-revalidate就是控制要去服务器端进行验证数据是否真的有变化。关于两者的区别后面会讲述。
  3. 那如何验证变化呢?就是借助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等字段。

  1. 浏览器刷新

即我们按F5刷新页面的时候,该页面的http请求中会添加:cache-control:max-age:0。注意这时候如果服务器的缓存控制策略没有must-revalidate的话,浏览器还可能继续使用这个缓存,异步地发送一个条件性 GET 请求(带有If-Modified-SinceIf-None-Match头部),以验证资源是否仍然有效。然后如果服务器返回 304 响应,表示资源仍然有效,浏览器会再更新一次过期时间。但是由于这个过程是“先斩后奏”的,用户大部分时候感觉不到这个过程,打开F12也会发现“已缓存”的字样。

  1. 浏览器强制刷新

即我们按ctrl+f5强制刷新页面的时候,该页面的http请求会添加:cache-control:no-cache; 即表示此时要首先去服务器端验证资源是否有更新,如果有更新则直接返回最新资源,如果没有更新,则返回304,然后浏览器端判断是304的话,则从缓存中读取数据。

  1. 浏览器前进后退重定向

当我们点击浏览器的前进后退操作时,这个时候请求中不会有cache-control的字段,没有该字段,就表示会检查缓存,直接利用之前的资源,不再重新请求服务器。

另外,expires是HTTP1.0时的头选项。浏览器会优先使用cache-control来判断缓存是否过期。如果只返回了expires,则浏览器会将expires转换为cache-control来判断缓存是否过期。如果两个响应头的值不一致,则以cache-control为准。

缓存验证

浏览器判断顶多是根据服务器端返回的失效时间去判断,这样并不一定准确,因为很可能出现缓存失效的情况,但其实资源并没有发生变化,这个时候其实也是应该走缓存的,那如何判断资源有没有发生变化呢?这必须交给服务器端来判断了。下面介绍通过Last-Modified/If-Modified-Since,和ETag/If-None-Match两种策略判断。后者是优先于前者的。

  1. Last-Modified/If-Modified-Since

即该字段是服务器端返回给客户端的响应头字段,表示当前请求的资源的最后修改时间,如果响应头中有该字段,那么下次请求的时候,请求头中就会包含If-Modified-Since字段,它的值就是Last-Modified的值,这样服务器端收到该字段的值,就可以和对应的资源最终的修改时间做对比,如果发生变化,则说明资源发生了变化,则返回最新资源(此时状态码是200),如果没有发生变化,则返回304,浏览器从缓存中直接去数据即可。

  1. ETag/If-None-Match

使用资源的最后更改时间作为判断资源是否更改可能会有问题?比如:资源改了之后,又改了回来,这时虽然资源的最后修改时间发生了变化,但其实资源内容本身没有发生变化,其实这种情况也应该是走缓存的,所以才出现了ETag字段,表示资源的唯一标识,那如果响应头中有该字段,则下次请求的时候,请求头中就会有If-None-Match字段,它的值就是ETag的值,服务器端收到以后,就会和当前资源的唯一表识别去对比,如果不一样,则说明资源发生变化,返回最新数据即可(此时状态码是200),如果一样,则说明资源没有变化,返回304,浏览器从缓存中读取数据。

总结

这一部分主要介绍了HTTP发展历史的前面部分,重点对HTTP1.1的缓存控制策略进行了讲解。后面会结合实践深入探索HTTP2的内容,并对https的原理进行介绍。