共计 4210 个字符,预计需要花费 11 分钟才能阅读完成。
在上一篇文章中介绍了 TCP 协议是如何保证数据完整传输的,以及 TCP 连接过程包括了建立连接、传输数据和断开连接三个阶段。我们还介绍了 http 的发展历程。
这篇文章我们深入 HTTP 的请求过程,并通过分析一个 HTTP 请求过程中每一步的状态来带你了解完整的 HTTP 请求过程。
客户端发起请求
1. 构建请求
首先,浏览器会构建类似下面这样的请求行信息,构建好后,浏览器准备发起网络请求。
GET /index.html HTTP1.1
2. 查找缓存
在发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件(浏览器缓存是在本地保存资源副本)。
当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。如果缓存查找失败,就会进入网络请求过程了。
3.DNS 域名解析
从上文我们知道,HTTP 的内容是从 TCP 传输层的传输阶段来传递的,并且数据包通过 IP 地址传输给接收方的。那么我们现在在浏览器中输入的 URL 可不是 IP,浏览器是怎么知道他的 IP 呢。这就用到了 DNS(Domain Name System)技术。—— 负责把域名和 IP 地址做一一映射关系。
浏览器还提供了 DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。
这样知道 IP 之后,接下来就需要获取端口号了。通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。
4. 建立 TCP 连接
具体的三次握手建立 TCP 连接请参考上文
5. 发送 HTTP 请求
浏览器通过发送请求行,请求头和请求体的方式来告诉服务器我们的“诉求”。
服务端返回请求
1. 返回请求
服务端通过发送响应行、响应头和响应体的方式来相应浏览器的“诉求”
2. 断开连接
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接(四次挥手)。不过如果浏览器或者服务器在其头信息中加入了:Connection:Keep-Alive(http/1.1)或者 http2.0 时代。那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。
3. 重定向
通常情况服务端的工作在上一步就“结束了”,不过还有一个特殊情况,就是服务端返回状态码 301 或者 302,表示我们请求的页面需要进行重定向。这就是为什么我们可能请求一个网址 http://www.test.com 但是最终打开的是 http://www.test-redirect.com。
浏览器缓存
上面铺垫了这么多的内容,我们可以看到有两个地方是用到了缓存技术 —— DNS 缓存和页面资源缓存
其中,DNS 缓存比较简单,它主要就是在浏览器本地把对应的 IP 和域名关联起来,这里就不做过多分析了,我贴上一个 DNS 的查找过程。
览器缓存 -> 系统缓存 -> 本地 host 文件(容易被恶意窜开,所以尽量设置为只读)->DNS 系统调用请求本地域名服务器 localDNS(LDNS)来解析域名(只到这里 80% 以上的情况都能找到了,也是浏览器能做的所有事情了)-> 后续
下面我们重点说下浏览器的资源缓存。
强缓存
不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cache 或 from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和 Last-modified 结合使用。Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
Cache-Control
Cache-Control 是 HTTP/1.1 的产物。比如如下设置
Cache-Control:max-age=300
则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。
关于 cache-control 的更多选项,参考 MDN 的文档 https://developer.mozilla.org…
值得一提的是,有两个选项我们经常会弄混,我们说明下:
no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control 的缓存控制方式做前置验证,而是使用 Etag 或者 Last-Modified 字段来控制缓存。需要注意的是,no-cache 这个名字有一点误导。设置了 no-cache 之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。
no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
两者对比
其实这两者差别不大,区别就在于 Expires 是 http1.0 的产物,Cache-Control 是 http1.1 的产物,两者同时存在的话,Cache-Control 优先级高于 Expires;所以 Expires 其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况
- 协商缓存生效,返回 304 和 Not Modified
- 协商缓存失效,返回 200 和请求结果
协商缓存同样可以通过设置两种 HTTP header 实现 Last-Modified 和 ETag
Last-Modified 和 If-Modified-Since
浏览器在第一次访问资源时,服务器返回资源的同时,在 response header 中添加 Last-Modified 的 header,表示这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和 header;
Last-Modified: Sun, 31 May 2020 05:28:04 GMT
浏览器下一次请求这个资源,浏览器检测到有 Last-Modified 这个 header,于是添加 If-Modified-Since 这个 header,值就是 Last-Modified 中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回 304 和空的响应体,直接从缓存读取,如果 If-Modified-Since 的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和 200 状态码
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
- 因为 Last-Modified 只能以秒计时,如果在 1s 时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
既然根据文件修改时间来决定是否缓存有缺陷,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag
和If-None-Match
ETag 和 If-None-Match
流程类似上面。Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接返回 200,将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304,客户端直接使用本地缓存即可。
两者对比
- 同样 Last-Modified 是 HTTP/1.0 的产物,而 Etag 是 HTTP/1.1 的产物
- 在精确度上,Etag 要优于 Last-Modified。
Last-Modified 的时间单位是秒,如果某个文件在 1 秒内改变了多次,那么他们的 Last-Modified 其实并没有体现出来修改,但是 Etag 每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的 Last-Modified 也有可能不一致。
- 在性能上,Etag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个 hash 值。
- 在优先级上,服务器校验优先考虑 Etag
缓存机制
了解了强缓存和协商缓存后,我们来总结下缓存机制。
强制缓存优先于协商缓存,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。具体流程图如下:
用户行为对浏览器缓存的影响
这里我们分析下用户在操作浏览器时,会触发怎样的缓存策略。主要有 3 种:
- 打开网页,地址栏输入地址:查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
- 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-control: no-cache
(为了兼容,还带了Pragma: no-cache
), 服务器直接返回 200 和最新内容。
最后,涉及到浏览器缓存还有两个 Service-work 和 Push Cache,在这里我们就不多介绍了,有兴趣的同学可以自行去翻阅 MDN 的文档