最近的前端面试中,http 成为一个热门的考察内容,写这篇文章主要为了自己能够对 http1.1的新特性有一个完整的知识体系,同时也为了能够深入的了解下常见的缓存机制。
相比于 HTTP 1.0,HTTP 1.1引入了新的特性:
- 连接可以复用,节省了多次打开TCP连接加载网页文档资源的时间。
- 增加流水线操作,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。
- 支持响应分块。
- 引入额外的缓存控制机制。
- 引入内容协商机制,包括语言,编码,类型等,并允许客户端和服务器之间约定以最合适的内容进行交换。
-
Host
头,能够使不同域名配置在同一个IP地址的服务器上。
连接复用
在 HTTP 1.1 中, Connection: Keep-Alive
字段默认开启,这个字段允许服务端和客户端保持一个长连接,当客户端发送另一个请求时,它会复用同一个连接。这个连接会一直持续到客户端或服务器端认为会话已经结束,其中一方中断连接。中断连接的取决与否取决于 Keep-Alive
字段中的:
- timeout: 决定当前 tcp的最长保持连接时间,单位为
s
,超过设置的时间后断开连接 - max:决定了当前连接最多的可复用次数
流水线操作(http pipelining)
专业的名词解释可以查看中文WIKI:链接
HTTP管线化(英语:HTTP pipelining)是将多个 HTTP 请求(request)整批提交的技术,而在发送过程中不需先等待服务器的回应。
流水线操作建立在长连接之上,可以将所有的 HTTP 请求一次性发出,而无需关心上一次发送请求的状态,虽然说客户端一次性能够发出所有的请求,但是在服务端接收到的请求还是一一进行处理的,如果当服务端返回的其中一个响应阻塞后,接下来的响应也会被阻塞。
响应分块
Content-length 字段
在长连接请求中,无法继续使用之前的方法来判断数据是否完全发送完毕,但是我们可以根据 Content-Length
的长度是否为 0 来判断数据是否传输完毕,请求传输的内容与长度必须保持一致,否则内容将会被截断。并且这种方法有一个弊端,那就是当发送的内容足够大时,服务器都需要去计算 content
的长度,导致性能降低和速度变慢,所以我们需要一个更好的机制来检测长连接的请求开始与结束。
下面一段 node
代码演示了当 Content-Lenth
长度与实际内容长度不一致导致内容被截断的例子
const http = require('http');
const fs = require('fs');
function handle(req, res) {
res.setHeader('Content-Length', '12');
res.end('1234567890123456');
}
const server = http.createServer(handle);
server.listen(3000);
console.log('Server start at port 3000...');
由于 Content-Length 长度超出12,导致内容被截断
Transfer-Encoding 与 Content-Encoding
由于 Content-Length
存在的弊端,我们可以使用 Transfer-Encoding
配合 Content-Encoding
来告诉浏览器传输的结果, Transfer-Encoding
的常用值为 chunked
,而 Content-Encoding
的作用是告知浏览器采用何种编码(压缩),通常使用的是 gzip
。对资源进行压缩后,再对内容进行分块传输。当最后一个分块长度为0时,则代表数据传输完成。
通过下面使用 Node
创建一个服务器的例子可以了解到什么是分块传输
const http = require('http');
function handle(req, res) {
res.setHeader('Content-type', 'text/html; charset=UTF-8');
res.setHeader('Transfer-Encoding', 'chunked');
let i = 0;
const timer = setInterval(()=>{
if(i < 5) {
res.write(`<p>this is ${i} chunk</p>`);
i ++;
}
else {
res.end('<p>this is last chunk</p>');
clearInterval(timer);
}
}, 500);
}
const server = http.createServer(handle);
server.listen(3000);
console.log('Server start at port 3000...');
注: HTTP2 不再支持使用 chunked
分块机制传输数据,有了更好的流传输机制,详细的会在之后的文章提及
新增缓存机制
相比 HTTP 1.0,HTTP 1.1 新增了若干项缓存机制:
- If-Modefied-Since
- If-Unmodified-Since
- If-Match
- If-None-Match
- ET-tag
详细的缓存机制将会在下面的章节详解
HTTP 缓存机制
强缓存
强缓存,是浏览器优先命中的缓存,速度最快。当我们在状态码后面看到 (from memory disk) 时,就表示浏览器从内存种读取了缓存,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。只有当强缓存不被命中的时候,才会进行协商缓存的查找。
Pargma
在 HTTP 1.0 时代,使用的是 Pragma: no-cache
字段来进行缓存的判断,由于现在已经步入 1.1 时代,更多的由 Cache-Control
控制,所以建议只在需要兼容 HTTP/1.0 客户端的场合下应用 Pragma 首部。
Expires
该字段表示的是,设定的时间为缓存的有效时间,当发生请求时,浏览器将会把 Expires
的值与本地时间进行对比,如果本地时间小于设置的时间,则读取缓存。Expires
的值为标准的 GMT 格式:
Expires: Wed, 21 Oct 2015 07:28:00 GMT
因为对比的是本地时间,所以也存在着弊端:当本地时间与服务端时间不一致时,无法达到预期的资源读取结果。
注 :
- 当
Expires
的字段设置为 0 时,代表该资源已经过期 - 如果在
Cache-Control
响应头设置了 "max-age" 或者 "s-max-age" 指令,那么Expires
头会被忽略。
Cache-Control
由于 Expires
的局限性, Cache-Control
登场了 (具体的参数可以点击查看MDN) ,下面说明几个常用的字段
- no-store:缓存不应存储有关客户端请求或服务器响应的任何内容。
- no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。
- max-age:相对过期时间,单位为秒(s),告知服务器资源在多少以内是有效的,无需向服务器请求
补充说明:
1、关于这no-store 和 no-cache 的区别,可以查看 stackoverflow 的这个提问:链接
Exactly checkingLast-Modified
orETag
. Client would ask server if it has new version of data using those headers and if the answer is no it will serve cached data.
也就是说,如果将 Cache-Control
设置为 no-cache
后,那么服务器将去验证 Last-Modeified
、 ETag
等字段,而 no-store
的作用则是不进行资源的缓存
2、max-age 指的是从当前时间开始计算的秒数,比如客户端初次请求某个资源文件的时间是18:00,假设 max-age
为 600秒,那么就代表着这个资源将在 18:10 过期
下面是一个使用 node 创建具有缓存 js 文件服务器的例子
文件结构如下:
favicon.ico
index.html
test.js
package.json
const http = require('http');
const fs = require('fs');
function handle(req, res) {
if(req.url === '/test.js') {
const js = fs.readFileSync('./test.js');
res.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control':'max-age=600'
});
res.end(js);
} else {
res.setHeader('Content-type', 'text/html; charset=UTF-8');
res.setHeader('Transfer-Encoding', 'chunked');
res.setHeader('Cache-Control', 'max-age=600');
const html = fs.readFileSync('./index.html');
res.end(html);
}
}
const server = http.createServer(handle);
server.listen(3000);
console.log('Server start at port 3000...');
我们将 max-age
设置为 600秒,那么这个 js 文件将在10分钟之内都使用缓存
协商缓存
当浏览器没有命中强缓存后,便会命中协商缓存,协商缓存由以下几个 HTTP 字段控制
Last-Modified
服务端将资源传送给客户端的时候,会将资源最后的修改时间以 Last-Modified: GMT
的形式加在实体首部上返回
Last-Modified: Fri, 22 Jul 2019 01:47:00 GMT
客户端接收到后会为此资源信息做上标记,等下次重新请求该资源的时候将会带上时间信息给服务器做检查,若传递的值与服务器上的值一致,则返回 304
,表示文件没有被修改过,若时间不一致,则重新进行资源请求并返回 200
。
那么在重新请求的时候,客户端要用什么方式去传递时间呢?答案是使用 If-Modified-Since
。
If-Modified-Since
在客户端重新请求资源的时候,将会在请求头添加 If-Modifed-Since: GMT
字段传递给服务端,服务端接收后会与当前该文件的最后修改时间对比,如果时间一致则返回 304
,如果不一致则传递最新的资源并返回 200
状态码。
If-Unmodified-Since
该值告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412
(Precondition Failed) 状态码给客户端。 Last-Modified 存在一定问题,如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。
这个字段的一个典型应用场景就是断点续传:当下载的资源内容被改变时,不应该继续返回之前的资源内容。
ETag
为了解决上面资源修改但是内容却没被修改,却依旧返回最新的资源的问题,ETag是一种比较好的解决方案, 服务端通过某种算法(比如 MD5)计算出该资源的唯一标致符,在响应资源的时候,将会添加在实体首部字段连同资源一并返回给客户端
ETag: "5b6d63d2-61afa"
客户端接收到后将会保存该信息,并且在下一次请求中附上该信息给服务端,服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。
如果服务端检测到传递过来的值与服务器端的不一致,则返回最新的资源和 200
状态码,否则返回 304
告知客户端使用缓存文件
那么客户端如何告知服务端 ETag 的相关信息呢?可以通过请求首部字段 If-Match
和 If-Node-Match
来进行传递
If-None-Match
示例为 If-None-Match: "5d8c72a5edda8d6a:3239"
该字段告知服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304
和响应报头即可。 当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。
If-Math
告诉服务器如果没有匹配到ETag,或者收到了 "*" 值而当前并没有该资源实体,则应当返回412
(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。
需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样。
注:If-None-Math 的 优先级比 If-Modified-Since 的优先级更高,有了 If-None-Match 字段后 If-Modified-Since 字段将会被忽略
总结
相关字段
通用首部字段
字段名称 | 说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Pragma | HTTP 1.0 遗留字段,使用 "no-cache" 作为是否缓存依据 |
请求首部字段
字段名称 | 说明 |
---|---|
If-Match | 比较 ETag 是否一致 |
If-None-Match | 比较 ETag 是否不一致 |
If-Modified-Since | 比较资源最后的更新时间是否一致 |
If-Unmodified-Since | 比较资源最后的更新时间是否不一致 |
响应首部字段
字段名称 | 说明 |
---|---|
ETag | 资源的匹配信息(通过强比较算法生成值) |
实体首部字段
字段名称 | 说明 |
---|---|
Expires | HTTP 1.0 遗留字段,资源的过期时间(取决于客户端本地时间) |
Last-Modified | 资源的最后一次修改时间 |
优先级
强缓存 --> 协商缓存Cache-Control
-> Expires
-> ETag
-> Last-Modified
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。